package org.jboss.installer.core;

import org.jboss.installer.actions.ActionResult;
import org.jboss.installer.actions.InstallerAction;
import org.jboss.installer.navigation.NavigationState;
import org.junit.Test;

import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.assertj.core.api.Assertions.assertThat;
import static org.jboss.installer.core.DefaultActionExecutor.ERROR_TASK_STATE;
import static org.jboss.installer.core.DefaultActionExecutor.EXECUTOR_STATE_PROPERTY;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class DefaultActionExecutorTest {

    private static final int TIMEOUT = 5000;
    private static final String AN_ERROR = "something's wrong";
    private final NavigationState navState = new NavigationState();

    // add the latch to catch when the action is completed
    private CountDownLatch completedLatch = new CountDownLatch(1);
    private DialogManager dialogManager = mock(DialogManager.class);
    private DefaultActionExecutor executor = new DefaultActionExecutor(dialogManager) {
        @Override
        protected void actionCompleted() {
            completedLatch.countDown();
        }
    };

    @Test
    public void actionIsExecuted() throws Exception {
        AtomicBoolean executed = new AtomicBoolean(false);
        InstallerAction action = (s) -> {
            executed.set(true);
            return ActionResult.success();
        };

        execute(action);

        assertTrue(executed.get());
    }

    @Test
    public void nextButtonIsDisabledDuringExecutingAction() throws Exception {
        // make navState loose focus - need to mock in test
        navState.lostFocus();

        CountDownLatch startedLatch = new CountDownLatch(1);
        CountDownLatch proceedLatch = new CountDownLatch(1);
        InstallerAction action = (s) -> {
            // signal that the action has started
            startedLatch.countDown();
            // and pause it until test resumes
            try {
                awaitLatch(proceedLatch);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return ActionResult.success();
        };

        // start executing action
        executor.execute(action, navState);

        // when the action has been started, validate the next button is disabled and let the action resume
        awaitLatch(startedLatch);
        assertFalse(navState.isNextEnabled());
        proceedLatch.countDown();

        // await for the processing to finish and validate the button state is enabled again
        awaitLatch(completedLatch);
        assertTrue(navState.isNextEnabled());
        assertFalse(navState.isErrorState());
        assertTrue("Navigation should have focus after action execution finishes", navState.isFocus());
    }

    @Test
    public void nextButtonIsNotEnabledAfterActionIfItWasDisabledBefore() throws Exception {
        CountDownLatch startedLatch = new CountDownLatch(1);
        CountDownLatch proceedLatch = new CountDownLatch(1);
        InstallerAction action = (s) -> {
            // signal that the action has started
            startedLatch.countDown();
            // and pause it until test resumes
            try {
                awaitLatch(proceedLatch);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return ActionResult.success();
        };

        navState.setNextEnabled(false);
        // start executing action
        executor.execute(action, navState);

        // when the action has been started, validate the next button is disabled and let the action resume
        awaitLatch(startedLatch);
        assertFalse(navState.isNextEnabled());
        proceedLatch.countDown();

        // await for the processing to finish and validate the button state is enabled again
        awaitLatch(completedLatch);
        assertFalse(navState.isNextEnabled());
    }

    @Test
    public void failedActionDisplaysErrorMessage() throws Exception {
        InstallerAction action = (s) -> ActionResult.error(AN_ERROR);

        execute(action);

        verify(dialogManager).actionError(AN_ERROR);
    }

    @Test
    public void failedActionDisablesNavigation() throws Exception {
        // make navState loose focus - need to mock in test
        navState.lostFocus();

        InstallerAction action = (s) -> ActionResult.error(AN_ERROR);

        execute(action);

        assertFalse(navState.isNextEnabled());
        assertFalse(navState.isPreviousEnabled());
        assertTrue(navState.isErrorState());
        assertTrue("Navigation should have focus after an error is emitted", navState.isFocus());
    }

    @Test
    public void stopRunningAction() throws Exception {
        CountDownLatch startedLatch = new CountDownLatch(1);
        CountDownLatch proceedLatch = new CountDownLatch(1);
        AtomicBoolean cancelled = new AtomicBoolean(false);
        InstallerAction action = (s) -> {
            // signal that the action has started
            startedLatch.countDown();
            // and pause it until test resumes
            try {
                awaitLatch(proceedLatch);
            } catch (InterruptedException e) {
                cancelled.set(true);
                throw new RuntimeException(e);
            }
            return ActionResult.success();
        };

        // start executing action
        executor.execute(action, navState);

        // when the action has been started, validate the next button is disabled and let the action resume
        awaitLatch(startedLatch);

        // cancel the action
        executor.stopCurrentAction();

        // check that it was really cancelled
        awaitLatch(completedLatch);
        assertTrue(cancelled.get());
    }

    @Test
    public void stopNoActionHasNoEffect() throws Exception {
        executor.stopCurrentAction();
        // no assertion - just verify no exceptions
    }

    @Test
    public void stopAfterCompleteHasNoEffect() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        InstallerAction action = (s) -> {
            latch.countDown();
            return ActionResult.success();
        };

        execute(action);

        if (!latch.await(50, TimeUnit.SECONDS)) {
            fail("Timeout waiting for action to complete");
        }

        executor.stopCurrentAction();
    }

    @Test
    public void canOnlySubmitOneJobAtATime() throws Exception {
        CountDownLatch startedLatch = new CountDownLatch(1);
        CountDownLatch proceedLatch = new CountDownLatch(1);
        AtomicBoolean executed = new AtomicBoolean(false);
        InstallerAction action = (s) -> {
            // signal that the action has started
            startedLatch.countDown();
            // and pause it until test resumes
            try {
                awaitLatch(proceedLatch);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return ActionResult.success();
        };
        InstallerAction action2 = (s) -> {
            executed.set(true);
            return ActionResult.success();
        };

        // start executing action
        executor.execute(action, navState);

        // when the action has been started, validate the next button is disabled and let the action resume
        awaitLatch(startedLatch);

        assertThrows(IllegalStateException.class, () -> execute(action2));
        assertFalse(executed.get());

        proceedLatch.countDown();

        awaitLatch(completedLatch);

        completedLatch = new CountDownLatch(1);
        execute(action2);
        awaitLatch(completedLatch);
        assertTrue(executed.get());
    }

    @Test
    public void testFailedTaskSendsErrorState() throws Exception {
        List<PropertyChangeEvent> events = new ArrayList<>();

        InstallerAction action = (s) -> {
            s.addPropertyChangeListener(events::add);
            return ActionResult.error(AN_ERROR);
        };

        execute(action);

        assertThat(events).anyMatch(e->e.getPropertyName().equals(EXECUTOR_STATE_PROPERTY) && e.getNewValue().equals(ERROR_TASK_STATE));
    }

    private void execute(InstallerAction action) throws InterruptedException {
        executor.execute(action, navState);
        // await for the processing to finish
        awaitLatch(completedLatch);
    }

    private void awaitLatch(CountDownLatch completedLatch) throws InterruptedException {
        assertTrue("Latch timed out - failing test.", completedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS));
    }

}
