package com.braintree.test;

import de.hybris.bootstrap.annotations.IntegrationTest;
import de.hybris.platform.core.Registry;
import de.hybris.platform.payment.commands.factory.CommandFactory;
import de.hybris.platform.payment.commands.factory.impl.DefaultCommandFactoryRegistryImpl;
import de.hybris.platform.processengine.definition.ProcessDefinitionFactory;
import de.hybris.platform.processengine.enums.ProcessState;
import de.hybris.platform.processengine.impl.DefaultBusinessProcessService;
import de.hybris.platform.processengine.model.BusinessProcessModel;
import de.hybris.platform.processengine.model.ProcessTaskModel;
import de.hybris.platform.processengine.spring.Action;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.spring.ctx.ScopeTenantIgnoreDocReader;
import de.hybris.platform.task.TaskModel;
import de.hybris.platform.task.impl.DefaultTaskService;
import de.hybris.platform.testframework.HybrisJUnit4Test;
import de.hybris.platform.testframework.TestUtils;
import de.hybris.platform.util.Utilities;
import com.braintree.test.actions.TestActionTemp;
import org.apache.log4j.Logger;
import org.junit.BeforeClass;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.After;
import org.junit.Before;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;

@IntegrationTest
public class ProcessFlowTest extends HybrisJUnit4Test {

    private static final Logger LOG = Logger.getLogger(ProcessFlowTest.class);

    private static final String ORDER_PROCESS_TEST = "order-process-test";
    private static final String TEST_CHECK_AUTHORIZE_ORDER_PAYMENT_ACTION = "test.checkAuthorizeOrderPaymentAction";
    private static final String TEST_RESERVE_ORDER_AMOUNT_ACTION = "test.reserveOrderAmountAction";
    private static final String TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION = "test.fraudCheckOrderInternalAction";
    private static final String CHECK_ORDER = "checkOrder";
    private static final String CHECK_AUTHORIZE_ORDER_PAYMENT = "checkAuthorizeOrderPayment";
    private static final String RESERVE_AMOUNT = "reserveAmount";
    private static final String CHECK_TRANSACTION_REVIEW_STATUS = "checkTransactionReviewStatus";
    private static final String FRAUD_CHECK = "fraudCheck";
    private static final String SEND_ORDER_PLACED_NOTIFICATION = "sendOrderPlacedNotification";
    private static final String TAKE_PAYMENT = "takePayment";
    private static final String PROCESS_STATE = "Process state";
    private static final String FRAUD = "FRAUD";
    private static final String TEST_SCHEDULE_FOR_CLEAN_UP_ACTION = "test.scheduleForCleanUpAction";
    private static final String NOTIFY_CUSTOMER = "notifyCustomer";
    private static final String WAIT_FOR_CLEAN_UP = "waitForCleanUp";
    private static final String SCHEDULE_FOR_CLEAN_UP = "scheduleForCleanUp";
    private static final String ORDER_MANUAL_CHECKED = "orderManualChecked";
    private static final String IS_PROCESS_COMPLETED = "isProcessCompleted";

    private static TaskServiceStub taskServiceStub;

    private static DefaultBusinessProcessService processService;
    private static ProcessDefinitionFactory definitonFactory;
    private static ModelService modelService;


    @BeforeClass
    public static void prepare() throws IOException //NOPMD
    {
        Registry.activateStandaloneMode();
        Utilities.setJUnitTenant();
        LOG.debug("Preparing...");

        final ApplicationContext appCtx = Registry.getApplicationContext();

        assertTrue("Application context of type " + appCtx.getClass() + " is not a subclass of "
            + ConfigurableApplicationContext.class, appCtx instanceof ConfigurableApplicationContext);

        final ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) appCtx;
        final ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        assertTrue("Bean Factory of type " + beanFactory.getClass() + " is not of type " + BeanDefinitionRegistry.class,
            beanFactory instanceof BeanDefinitionRegistry);
        final XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
        xmlReader.setDocumentReaderClass(ScopeTenantIgnoreDocReader.class);
        xmlReader.loadBeanDefinitions(new ClassPathResource(
            "/braintreefulfilmentprocess/test/braintreefulfilmentprocess-spring-test.xml"));
        xmlReader
            .loadBeanDefinitions(
                new ClassPathResource("/braintreefulfilmentprocess/test/process/order-process-spring.xml"));
        xmlReader.loadBeanDefinitions(new ClassPathResource(
            "/braintreefulfilmentprocess/test/process/consignment-process-spring.xml"));

        modelService = (ModelService) getBean("modelService");
        processService = (DefaultBusinessProcessService) getBean("businessProcessService");
        definitonFactory = processService.getProcessDefinitionFactory();

        LOG.warn("Prepare Process Definition factory...");
        definitonFactory.add("classpath:/braintreefulfilmentprocess/test/process/order-process.xml");
        definitonFactory.add("classpath:/braintreefulfilmentprocess/test/process/consignment-process.xml");
        LOG.warn("loaded 'order-process-test':" + definitonFactory.getProcessDefinition(ORDER_PROCESS_TEST)
            + " in factory "
            + definitonFactory);

        //setup command factory to mock
        taskServiceStub = appCtx.getBean(TaskServiceStub.class);
        processService.setTaskService(taskServiceStub);

        final DefaultCommandFactoryRegistryImpl commandFactoryReg = appCtx
            .getBean(DefaultCommandFactoryRegistryImpl.class);
        commandFactoryReg.setCommandFactoryList(Arrays.asList((CommandFactory) appCtx.getBean("mockupCommandFactory")));

    }

    @Before
    public void setupActions() {
        setResultForAction(TEST_CHECK_AUTHORIZE_ORDER_PAYMENT_ACTION, "OK");
        setThrowExceptionForAction(TEST_RESERVE_ORDER_AMOUNT_ACTION, false);
        setResultForAction(TEST_RESERVE_ORDER_AMOUNT_ACTION, "OK");
        setResultForAction("test.checkTransactionReviewStatusAction", "OK");
        setResultForAction(TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION, "OK");
        setResultForAction("test.fraudCheckOrderAction", "OK");
        setResultForAction("test.receiveConsignmentStatusAction", "OK");
        setResultForAction("test.takePaymentAction", "OK");
        setResultForAction("test.waitBeforeTransmissionAction", "OK");
    }

    @AfterClass
    public static void removeProcessDefinitions() {
        LOG.debug("cleanup...");

        final ApplicationContext appCtx = Registry.getApplicationContext();

        assertTrue("Application context of type " + appCtx.getClass() + " is not a subclass of "
            + ConfigurableApplicationContext.class, appCtx instanceof ConfigurableApplicationContext);

        final ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) appCtx;
        final ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        assertTrue("Bean Factory of type " + beanFactory.getClass() + " is not of type " + BeanDefinitionRegistry.class,
            beanFactory instanceof BeanDefinitionRegistry);
        final XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
        xmlReader.loadBeanDefinitions(new ClassPathResource(
            "/braintreefulfilmentprocess/test/braintreefulfilmentprocess-spring-testcleanup.xml"));

        //cleanup command factory
        final Map<String, CommandFactory> commandFactoryList = applicationContext.getBeansOfType(CommandFactory.class);
        commandFactoryList.remove("mockupCommandFactory");
        final DefaultCommandFactoryRegistryImpl commandFactoryReg = appCtx
            .getBean(DefaultCommandFactoryRegistryImpl.class);
        commandFactoryReg.setCommandFactoryList(commandFactoryList.values());

        processService.setTaskService(appCtx.getBean(DefaultTaskService.class));
        definitonFactory = null;
        processService = null;
    }

    @After
    public void resetServices() {
        final List<TaskModel> tasks = taskServiceStub.cleanup();
        final StringBuilder msg = new StringBuilder();
        for (final TaskModel task : tasks) {
            final ProcessTaskModel processTask = (ProcessTaskModel) task;

            msg.append(processTask.getAction()).append(", ");
        }
    }

    @Test
    public void testErrorCode() throws InterruptedException {
        setResultForAction("test.checkOrderAction", Action.ERROR_RETURN_CODE);
        try {
            final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);
            TestUtils.disableFileAnalyzer(400);
            assertStep(process, CHECK_ORDER);

        } finally {
            setResultForAction("test.checkOrderAction", "OK");
            TestUtils.enableFileAnalyzer();
        }
    }

    @Test
    public void testProcessPaymentNotTaken() throws InterruptedException {
        setResultForAction("test.takePaymentAction", "NOK");

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, SEND_ORDER_PLACED_NOTIFICATION);
        assertStep(process, TAKE_PAYMENT);
        assertStep(process, "sendPaymentFailedNotification");
    }

    @Test
    public void testErrorInProcess() throws InterruptedException {
        setResultForAction(TEST_RESERVE_ORDER_AMOUNT_ACTION, Action.ERROR_RETURN_CODE);

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        TestUtils.disableFileAnalyzer(400);
        try {
            assertStep(process, RESERVE_AMOUNT);
        } finally {
            TestUtils.enableFileAnalyzer();
        }
        Thread.sleep(1000);

        modelService.refresh(process);
        assertEquals(PROCESS_STATE, ProcessState.ERROR, process.getProcessState());
    }

    @Test
    public void testExceptionInProcess() throws InterruptedException {
        setThrowExceptionForAction(TEST_RESERVE_ORDER_AMOUNT_ACTION, true);

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        TestUtils.disableFileAnalyzer(400);
        try {
            assertStep(process, RESERVE_AMOUNT);
        } finally {
            TestUtils.enableFileAnalyzer();
        }

        Thread.sleep(1000);

        modelService.refresh(process);
        assertEquals(PROCESS_STATE, ProcessState.ERROR, process.getProcessState());
    }


    @Test
    public void testConsignmentStatusCancel() throws InterruptedException {
        setResultForAction(TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION, FRAUD);
        setResultForAction("test.receiveConsignmentStatusAction", "CANCEL");
        setResultForAction(TEST_SCHEDULE_FOR_CLEAN_UP_ACTION, "NOK");

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, NOTIFY_CUSTOMER);
        assertStep(process, WAIT_FOR_CLEAN_UP);
        assertStep(process, SCHEDULE_FOR_CLEAN_UP);
        assertStep(process, ORDER_MANUAL_CHECKED);
        assertStep(process, SEND_ORDER_PLACED_NOTIFICATION);
        assertStep(process, TAKE_PAYMENT);
        assertStep(process, "splitOrder");
        assertStep(process, "waitForWarehouseSubprocessEnd");
        assertStep(process, "waitBeforeTransmission");
        assertStep(process, IS_PROCESS_COMPLETED);
        assertStep(process, "sendConsignmentToWarehouse");
        assertStep(process, "waitForWarehouse");
        assertStep(process, "receiveConsignmentStatus");
        assertStep(process, "cancelConsignment");
        assertStep(process, "sendCancelMessage");
        assertStep(process, "subprocessEnd");
        assertStep(process, IS_PROCESS_COMPLETED);
        assertStep(process, "sendOrderCompletedNotification");
    }

    @Test
    public void testProcessFraudFinalFraudScheduledForCleanup() throws InterruptedException {
        setResultForAction(TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION, FRAUD);
        setResultForAction(TEST_SCHEDULE_FOR_CLEAN_UP_ACTION, "OK");
        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, NOTIFY_CUSTOMER);
        assertStep(process, WAIT_FOR_CLEAN_UP);
        assertStep(process, SCHEDULE_FOR_CLEAN_UP);
        assertStep(process, "cancelOrder");
    }

    @Test
    public void testProcessFraudFinalFraudNotScheduledForCleanup() throws InterruptedException {
        setResultForAction(TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION, FRAUD);
        setResultForAction(TEST_SCHEDULE_FOR_CLEAN_UP_ACTION, "NOK");
        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, NOTIFY_CUSTOMER);
        assertStep(process, WAIT_FOR_CLEAN_UP);
        assertStep(process, SCHEDULE_FOR_CLEAN_UP);
        assertStep(process, ORDER_MANUAL_CHECKED);
        assertStep(process, SEND_ORDER_PLACED_NOTIFICATION);
    }

    @Test
    public void testProcessFraudFinalPotential() throws InterruptedException {
        setResultForAction(TEST_FRAUD_CHECK_ORDER_INTERNAL_ACTION, "POTENTIAL");
        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, "manualOrderCheckCSA");
        assertStep(process, "waitForManualOrderCheckCSA");
        assertStep(process, ORDER_MANUAL_CHECKED);
        assertStep(process, SEND_ORDER_PLACED_NOTIFICATION);
    }

    @Test
    public void testProcessPaymentFailed() throws InterruptedException {
        setResultForAction(TEST_RESERVE_ORDER_AMOUNT_ACTION, "NOK");

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, "sendPaymentFailedNotification");
    }

    @Test
    public void testProcessAuthorizationFailed() throws InterruptedException {
        setResultForAction(TEST_CHECK_AUTHORIZE_ORDER_PAYMENT_ACTION, "NOK");

        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, "authorizationFailedNotification");
    }


    @Test
    public void testProcessOk() throws InterruptedException {
        final BusinessProcessModel process = createProcess(ORDER_PROCESS_TEST);

        assertStep(process, CHECK_ORDER);
        assertStep(process, CHECK_AUTHORIZE_ORDER_PAYMENT);
        assertStep(process, RESERVE_AMOUNT);
        assertStep(process, CHECK_TRANSACTION_REVIEW_STATUS);
        assertStep(process, FRAUD_CHECK);
        assertStep(process, SEND_ORDER_PLACED_NOTIFICATION);
        assertStep(process, TAKE_PAYMENT);
        assertStep(process, "splitOrder");
        assertStep(process, "waitForWarehouseSubprocessEnd");
        assertStep(process, "waitBeforeTransmission");
        assertStep(process, IS_PROCESS_COMPLETED);
        assertStep(process, "sendConsignmentToWarehouse");
        assertStep(process, "waitForWarehouse");
        assertStep(process, "receiveConsignmentStatus");
        assertStep(process, "allowShipment");
        assertStep(process, "sendDeliveryMessage");
        assertStep(process, "subprocessEnd");
        assertStep(process, IS_PROCESS_COMPLETED);
        assertStep(process, "sendOrderCompletedNotification");
    }

    protected BusinessProcessModel createProcess(final String processName) {
        final String id = "Test" + (new Date()).getTime();
        final BusinessProcessModel process = processService.startProcess(id, processName);
        assertProcessState(process, ProcessState.RUNNING);
        modelService.save(process);
        return process;
    }

    protected void setResultForAction(final String action, final String result) {
        final TestActionTemp tmp = (TestActionTemp) getBean(action);
        tmp.setResult(result);
    }

    protected void setThrowExceptionForAction(final String action, final boolean throwException) {
        final TestActionTemp tmp = (TestActionTemp) getBean(action);
        tmp.setThrowException(throwException);
    }


    protected void assertStep(final BusinessProcessModel process, final String bean) throws InterruptedException {
        LOG.info("assertStep action = " + bean);

        final ProcessTaskModel processTaskModel = taskServiceStub.runProcessTask(bean);

        if (processTaskModel == null) {
            final StringBuilder found = new StringBuilder();

            for (final TaskModel task : taskServiceStub.getTasks()) {
                if (task instanceof ProcessTaskModel) {
                    found.append(((ProcessTaskModel) task).getAction()).append("; ");
                }
            }

            assertNotNull("No task found for bean " + bean + ", action(s): " + found, processTaskModel);
        }
    }

    protected static Object getBean(final String name) {
        return Registry.getApplicationContext().getBean(name);
    }

    protected void assertProcessState(final BusinessProcessModel process, final ProcessState state) {
        modelService.refresh(process);
        assertEquals(PROCESS_STATE, state, process.getState());
    }

}
