/**
 * Copyright 2016 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.hystrix.contrib.javanica.test.common.fallback;


import com.netflix.hystrix.HystrixEventType;
import com.netflix.hystrix.HystrixInvokableInfo;
import com.netflix.hystrix.HystrixRequestLog;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.command.AsyncResult;
import com.netflix.hystrix.contrib.javanica.exception.FallbackDefinitionException;
import com.netflix.hystrix.contrib.javanica.test.common.BasicHystrixTest;
import com.netflix.hystrix.contrib.javanica.test.common.domain.Domain;
import com.netflix.hystrix.contrib.javanica.test.common.domain.User;
import com.netflix.hystrix.exception.HystrixBadRequestException;
import org.apache.commons.lang3.Validate;
import org.junit.Before;
import org.junit.Test;
import rx.Observable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static com.netflix.hystrix.contrib.javanica.test.common.CommonUtils.getHystrixCommandByKey;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public abstract class BasicCommandFallbackTest extends BasicHystrixTest {

    private UserService userService;

    protected abstract UserService createUserService();

    @Before
    public void setUp() throws Exception {
        userService = createUserService();
    }

    @Test
    public void testGetUserAsyncWithFallback() throws ExecutionException, InterruptedException {
        Future<User> f1 = userService.getUserAsync(" ", "name: ");

        assertEquals("def", f1.get().getName());
        assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest()
                .getAllExecutedCommands().iterator().next();
        assertEquals("getUserAsync", command.getCommandKey().name());

        // confirm that 'getUserAsync' command has failed
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // and that fallback waw successful
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));

    }

    @Test
    public void testGetUserSyncWithFallback() {
        User u1 = userService.getUserSync(" ", "name: ");

        assertEquals("def", u1.getName());
        assertEquals(1, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> command = HystrixRequestLog.getCurrentRequest()
                .getAllExecutedCommands().iterator().next();

        assertEquals("getUserSync", command.getCommandKey().name());
        // confirm that command has failed
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // and that fallback was successful
        assertTrue(command.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));

    }


    /**
     * * **************************** *
     * * * TEST FALLBACK COMMANDS * *
     * * **************************** *
     */
    @Test
    public void testGetUserAsyncWithFallbackCommand() throws ExecutionException, InterruptedException {
        Future<User> f1 = userService.getUserAsyncFallbackCommand(" ", "name: ");

        assertEquals("def", f1.get().getName());

        assertEquals(3, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> getUserAsyncFallbackCommand = getHystrixCommandByKey(
                "getUserAsyncFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo firstFallbackCommand = getHystrixCommandByKey("firstFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo secondFallbackCommand = getHystrixCommandByKey("secondFallbackCommand");

        assertEquals("getUserAsyncFallbackCommand", getUserAsyncFallbackCommand.getCommandKey().name());
        // confirm that command has failed
        assertTrue(getUserAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // confirm that first fallback has failed
        assertTrue(firstFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // and that second fallback was successful
        assertTrue(secondFallbackCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
    }

    @Test
    public void testGetUserAsyncFallbackAsyncCommand() throws ExecutionException, InterruptedException {
        Future<User> f1 = userService.getUserAsyncFallbackAsyncCommand(" ", "name: ");

        assertEquals("def", f1.get().getName());

        assertEquals(4, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> getUserAsyncFallbackAsyncCommand = getHystrixCommandByKey(
                "getUserAsyncFallbackAsyncCommand");
        com.netflix.hystrix.HystrixInvokableInfo firstAsyncFallbackCommand = getHystrixCommandByKey("firstAsyncFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo secondAsyncFallbackCommand = getHystrixCommandByKey("secondAsyncFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo thirdAsyncFallbackCommand = getHystrixCommandByKey("thirdAsyncFallbackCommand");
        assertEquals("getUserAsyncFallbackAsyncCommand", getUserAsyncFallbackAsyncCommand.getCommandKey().name());
        // confirm that command has failed
        assertTrue(getUserAsyncFallbackAsyncCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // confirm that first fallback has failed
        assertTrue(firstAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // and that second fallback was successful
        assertTrue(secondAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));

        assertTrue(thirdAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        assertTrue(thirdAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
    }

    @Test
    public void testGetUserSyncWithFallbackCommand() {
        User u1 = userService.getUserSyncFallbackCommand(" ", "name: ");

        assertEquals("def", u1.getName());
        assertEquals(3, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> getUserSyncFallbackCommand = getHystrixCommandByKey(
                "getUserSyncFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo firstFallbackCommand = getHystrixCommandByKey("firstFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo secondFallbackCommand = getHystrixCommandByKey("secondFallbackCommand");

        assertEquals("getUserSyncFallbackCommand", getUserSyncFallbackCommand.getCommandKey().name());
        // confirm that command has failed
        assertTrue(getUserSyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // confirm that first fallback has failed
        assertTrue(firstFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        // and that second fallback was successful
        assertTrue(secondFallbackCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
    }

    @Test
    public void testAsyncCommandWithAsyncFallbackCommand() throws ExecutionException, InterruptedException {
        Future<User> userFuture = userService.asyncCommandWithAsyncFallbackCommand("", "");
        User user = userFuture.get();
        assertEquals("def", user.getId());
        assertEquals(2, HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
        HystrixInvokableInfo<?> asyncCommandWithAsyncFallbackCommand = getHystrixCommandByKey("asyncCommandWithAsyncFallbackCommand");
        com.netflix.hystrix.HystrixInvokableInfo asyncFallbackCommand = getHystrixCommandByKey("asyncFallbackCommand");
        // confirm that command has failed
        assertTrue(asyncCommandWithAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FAILURE));
        assertTrue(asyncCommandWithAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
        // and that second fallback was successful
        assertTrue(asyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.SUCCESS));
    }

    @Test(expected = FallbackDefinitionException.class)
    public void testAsyncCommandWithAsyncFallback() {
        userService.asyncCommandWithAsyncFallback("", "");
    }

    @Test(expected = FallbackDefinitionException.class)
    public void testCommandWithWrongFallbackReturnType() {
        userService.commandWithWrongFallbackReturnType("", "");
    }

    @Test(expected = FallbackDefinitionException.class)
    public void testAsyncCommandWithWrongFallbackReturnType() {
        userService.asyncCommandWithWrongFallbackReturnType("", "");
    }

    @Test(expected = FallbackDefinitionException.class)
    public void testCommandWithWrongFallbackParams() {
        userService.commandWithWrongFallbackParams("1", "2");
    }

    @Test(expected = FallbackDefinitionException.class)
    public void testCommandWithFallbackReturnSuperType() {
        userService.commandWithFallbackReturnSuperType("", "");
    }

    @Test
    public void testCommandWithFallbackReturnSubType() {
        User user = (User) userService.commandWithFallbackReturnSubType("", "");
        assertEquals("def", user.getName());
    }

    @Test
    public void testCommandWithFallbackWithAdditionalParameter() {
        User user = userService.commandWithFallbackWithAdditionalParameter("", "");
        assertEquals("def", user.getName());
    }

    @Test(expected = HystrixBadRequestException.class)
    public void testCommandThrowsHystrixBadRequestExceptionWithNoCause() {
        try {
            userService.commandThrowsHystrixBadRequestExceptionWithNoCause(null, null);
        } finally {
            HystrixInvokableInfo<?> asyncCommandWithAsyncFallbackCommand = getHystrixCommandByKey("commandThrowsHystrixBadRequestExceptionWithNoCause");
            assertFalse(asyncCommandWithAsyncFallbackCommand.getExecutionEvents().contains(HystrixEventType.FALLBACK_SUCCESS));
        }
    }

    public static class UserService {

        @HystrixCommand(fallbackMethod = "fallback")
        public Future<User> getUserAsync(final String id, final String name) {
            validate(id, name); // validate logic can be inside and outside of AsyncResult#invoke method
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    // validate(id, name); possible put validation logic here, in case of any exception a fallback method will be invoked
                    return new User(id, name + id); // it should be network call
                }
            };
        }

        @HystrixCommand(fallbackMethod = "fallback")
        public User getUserSync(String id, String name) {
            validate(id, name);
            return new User(id, name + id); // it should be network call
        }

        private User fallback(String id, String name) {
            return new User("def", "def");
        }

        @HystrixCommand(fallbackMethod = "firstFallbackCommand")
        public Future<User> getUserAsyncFallbackCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    validate(id, name);
                    return new User(id, name + id); // it should be network call
                }
            };
        }


        @HystrixCommand(fallbackMethod = "firstFallbackCommand")
        public User getUserSyncFallbackCommand(String id, String name) {
            validate(id, name);
            return new User(id, name + id); // it should be network call
        }

        // FALLBACK COMMANDS METHODS:
        // This fallback methods will be processed as hystrix commands

        @HystrixCommand(fallbackMethod = "secondFallbackCommand")
        private User firstFallbackCommand(String id, String name) {
            validate(id, name);
            return new User(id, name + id); // it should be network call
        }

        @HystrixCommand(fallbackMethod = "staticFallback")
        private User secondFallbackCommand(String id, String name) {
            validate(id, name);
            return new User(id, name + id); // it should be network call
        }

        @HystrixCommand(fallbackMethod = "firstAsyncFallbackCommand")
        public Future<User> getUserAsyncFallbackAsyncCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    throw new RuntimeException("getUserAsyncFallbackAsyncCommand failed");
                }
            };
        }

        @HystrixCommand(fallbackMethod = "secondAsyncFallbackCommand")
        private Future<User> firstAsyncFallbackCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    throw new RuntimeException("firstAsyncFallbackCommand failed");
                }
            };
        }

        @HystrixCommand(fallbackMethod = "thirdAsyncFallbackCommand")
        private Future<User> secondAsyncFallbackCommand(final String id, final String name, final Throwable e) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    if ("firstAsyncFallbackCommand failed".equals(e.getMessage())) {
                        throw new RuntimeException("secondAsyncFallbackCommand failed");
                    }
                    return new User(id, name + id);
                }
            };
        }

        @HystrixCommand(fallbackMethod = "fallbackWithAdditionalParam")
        private Future<User> thirdAsyncFallbackCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    throw new RuntimeException("thirdAsyncFallbackCommand failed");
                }
            };
        }

        private User fallbackWithAdditionalParam(final String id, final String name, final Throwable e) {
            if (!"thirdAsyncFallbackCommand failed".equals(e.getMessage())) {
                throw new RuntimeException("fallbackWithAdditionalParam failed");
            }
            return new User("def", "def");
        }

        @HystrixCommand(fallbackMethod = "asyncFallbackCommand", commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100000")
        })
        public Future<User> asyncCommandWithAsyncFallbackCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    validate(id, name);
                    return new User(id, name + id); // it should be network call
                }
            };
        }

        @HystrixCommand(fallbackMethod = "asyncFallback", commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100000")
        })
        public Future<User> asyncCommandWithAsyncFallback(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    validate(id, name);
                    return new User(id, name + id); // it should be network call
                }
            };
        }

        public Future<User> asyncFallback(final String id, final String name) {
            return Observable.just(new User("def", "def")).toBlocking().toFuture();
        }

        @HystrixCommand
        public Future<User> asyncFallbackCommand(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    return new User("def", "def"); // it should be network call
                }
            };
        }

        @HystrixCommand(fallbackMethod = "fallbackWithAdditionalParameter")
        public User commandWithFallbackWithAdditionalParameter(final String id, final String name) {
            validate(id, name);
            return new User(id, name + id);
        }

        public User fallbackWithAdditionalParameter(final String id, final String name, Throwable e) {
            if (e == null) {
                throw new RuntimeException("exception should be not null");
            }
            return new User("def", "def");
        }

        @HystrixCommand(fallbackMethod = "fallbackWithStringReturnType")
        public User commandWithWrongFallbackReturnType(final String id, final String name) {
            validate(id, name);
            return new User(id, name);
        }

        @HystrixCommand(fallbackMethod = "fallbackWithStringReturnType")
        public Future<User> asyncCommandWithWrongFallbackReturnType(final String id, final String name) {
            return new AsyncResult<User>() {
                @Override
                public User invoke() {
                    return new User("def", "def"); // it should be network call
                }
            };
        }

        @HystrixCommand(fallbackMethod = "fallbackWithoutParameters")
        public User commandWithWrongFallbackParams(final String id, final String name) {
            return new User(id, name);
        }

        @HystrixCommand(fallbackMethod = "fallbackReturnSubTypeOfDomain")
        public Domain commandWithFallbackReturnSubType(final String id, final String name) {
            validate(id, name);
            return new User(id, name);
        }

        @HystrixCommand(fallbackMethod = "fallbackReturnSuperTypeOfDomain")
        public User commandWithFallbackReturnSuperType(final String id, final String name) {
            validate(id, name);
            return new User(id, name);
        }

        @HystrixCommand(fallbackMethod = "staticFallback")
        public User commandThrowsHystrixBadRequestExceptionWithNoCause(final String id, final String name){
            throw new HystrixBadRequestException("invalid arguments");
        }

        private User fallbackReturnSubTypeOfDomain(final String id, final String name) {
            return new User("def", "def");
        }

        private Domain fallbackReturnSuperTypeOfDomain(final String id, final String name) {
            return new User("def", "def");
        }

        private String fallbackWithStringReturnType(final String id, final String name) {
            return null;
        }

        private User fallbackWithoutParameters() {
            return null;
        }

        private User staticFallback(String id, String name) {
            return new User("def", "def");
        }

        private void validate(String id, String name) {
            Validate.notBlank(id);
            Validate.notBlank(name);
        }

    }
}
