/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2021 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 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 org.mockito.Mockito;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import static javax.swing.JOptionPane.NO_OPTION;
import static javax.swing.JOptionPane.YES_OPTION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.refEq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

public class NavigatorTest {
    private static final String A_PASSWORD = "foo";
    private static final String A_WARNING = "something's wrong";
    private static final String FIRST_SCREEN = "first";
    private static final String SECOND_SCREEN = "second";
    private static final String THIRD_SCREEN = "third";

    private List<Screen> screens = new ArrayList<>();
    private Map<String, Screen> screenNameMap = new HashMap<>();
    private NavigationState navState = new NavigationState();
    private ScreenManager screenMgr = new ScreenManager(screens, screenNameMap);
    private Consumer<Screen> updater = mock(Consumer.class);
    private final DialogManager dialogManager = mock(DialogManager.class);
    private ActionExecutor executor = mock(ActionExecutor.class);
    private Navigator nav = new Navigator(screenMgr, navState, dialogManager, updater, executor, new InstallationData());

    @Test
    public void moveToNextDisplaysNewScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(secondScreen).load(any(InstallationData.class), any(ScreenManager.class));
        verify(updater, Mockito.times(2)).accept(any(Screen.class));
        verify(updater).accept(secondScreen);
        assertEquals(secondScreen, screenMgr.current());
    }

    @Test
    public void validationErrorPreventsNextScreenTransition() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.error(A_WARNING));
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(updater, Mockito.times(1)).accept(any(Screen.class));
        assertEquals(firstScreen, screenMgr.current());
    }

    @Test
    public void validationWarningShowsWarningDialog_ifAcceptedTransitionsToNextPage() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.warning(A_WARNING));
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        when(dialogManager.validationWarning(anyList())).thenReturn(YES_OPTION);

        nav.startNavigation();
        nav.moveToNext();

        verify(dialogManager).validationWarning(List.of(A_WARNING));
        assertEquals(secondScreen, screenMgr.current());
    }

    @Test
    public void validationWarningShowsWarningDialog_ifRejectedStaysOnCurrentPage() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.warning(A_WARNING));
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        when(dialogManager.validationWarning(anyList())).thenReturn(NO_OPTION);

        nav.startNavigation();
        nav.moveToNext();

        verify(dialogManager).validationWarning(List.of(A_WARNING));
        assertEquals(firstScreen, screenMgr.current());
    }

    @Test
    public void screenDataIsRecordedBeforeLoadingNextScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        doAnswer(inv-> {
            InstallationData data = inv.getArgument(0);
            data.setPassword(A_PASSWORD);
            return null;
        }).when(firstScreen).record(any(InstallationData.class), any(ScreenManager.class));
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(firstScreen).record(any(InstallationData.class), any(ScreenManager.class));
        assertEquals(secondScreen, screenMgr.current());
        assertEquals(A_PASSWORD, nav.getInstallationData().getPassword());
    }

    @Test
    public void screenDataIsRolledBackBeforeLoadingPreviousScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        screens.addAll(Arrays.asList(firstScreen, secondScreen));
        nav.startNavigation();
        nav.moveToNext();
        nav.moveToPrevious();
        verify(secondScreen).rollback(any(InstallationData.class));
        assertEquals(firstScreen, screenMgr.current());


    }

    @Test
    public void allButtonsAreEnabledOnNextScreenByDefault() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        when(secondScreen.prevalidate()).thenReturn(true);
        when(secondScreen.validate()).thenReturn(ValidationResult.ok());
        when(secondScreen.isReversible()).thenReturn(true);
        final Screen thirdScreen = defaultScreen(THIRD_SCREEN);

        screens.addAll(Arrays.asList(firstScreen, secondScreen, thirdScreen));

        nav.startNavigation();
        nav.moveToNext();

        assertTrue(navState.isNextEnabled());
        assertTrue(navState.isPreviousEnabled());
        assertTrue(navState.isNextVisible());
        assertTrue(navState.isPreviousVisible());
    }

    @Test
    public void onlyDoneButtonIsVisibleOnLastScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        when(secondScreen.prevalidate()).thenReturn(true);
        when(secondScreen.validate()).thenReturn(ValidationResult.ok());
        when(secondScreen.isReversible()).thenReturn(true);

        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        assertFalse(navState.isPreviousVisible());
        assertFalse(navState.isNextVisible());
        assertFalse(navState.isQuitVisible());
        assertTrue(navState.isDoneVisible());
    }

    @Test
    public void previousButtonIsDisabledOnNonReversibleScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        final Screen thirdScreen = defaultScreen(THIRD_SCREEN);
        when(secondScreen.prevalidate()).thenReturn(true);
        when(secondScreen.validate()).thenReturn(ValidationResult.ok());
        when(secondScreen.isReversible()).thenReturn(false);

        screens.addAll(Arrays.asList(firstScreen, secondScreen, thirdScreen));

        nav.startNavigation();
        nav.moveToNext();

        assertTrue(navState.isPreviousVisible());
        assertFalse(navState.isPreviousEnabled());
    }

    @Test
    public void initialScreenHidesPreviousButton() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        screens.addAll(Arrays.asList(firstScreen));
        when(firstScreen.isReversible()).thenReturn(true);

        nav.startNavigation();

        assertTrue(navState.isNextVisible());
        assertTrue(navState.isNextEnabled());
        assertFalse(navState.isPreviousVisible());
    }

    @Test
    public void startingNavigationDisplaysFirstScreen() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        screens.addAll(Arrays.asList(firstScreen));
        when(firstScreen.isReversible()).thenReturn(true);

        nav.startNavigation();

        assertEquals(firstScreen, screenMgr.current());
        verify(firstScreen).load(nav.getInstallationData(), screenMgr);
        verify(updater).accept(firstScreen);
    }

    @Test
    public void postLoadActionIsExecutedAfterAfterLoadingNextScreen() {
        InstallerAction action = (s) -> ActionResult.success();
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        when(secondScreen.prevalidate()).thenReturn(true);
        when(secondScreen.validate()).thenReturn(ValidationResult.ok());
        when(secondScreen.getPostLoadAction(any())).thenReturn(action);

        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(executor).execute(refEq(action), any(NavigationState.class));
    }

    @Test
    public void exceptionInMoveToNextDisplaysSystemErrorAndDisablesNavigation() {
        // override ScreenManager to throw exception in next
        final InstallerRuntimeException expectedException = new InstallerRuntimeException("");
        final ScreenManager screenMgr = new ScreenManager(screens, screenNameMap) {
            @Override
            public Screen next() {
                throw expectedException;
            }
        };
        // mock simple screens to allow navigation
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        screens.addAll(Arrays.asList(firstScreen, secondScreen));
        // override Navigator to use our ScreenManager
        Navigator nav = new Navigator(screenMgr, navState, dialogManager, updater, executor, new InstallationData());

        // try navigation forward
        nav.startNavigation();
        nav.moveToNext();

        // verify there's a user error
        verify(dialogManager).systemError(expectedException);
        assertFalse(navState.isNextEnabled());
        assertFalse(navState.isPreviousEnabled());
    }

    @Test
    public void stopCurrentActionPassedToExecutor() {
        nav.stopCurrentAction();

        verify(executor).stopCurrentAction();
    }

    @Test
    public void allowNextScreenToRequestFocus() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(true);
        when(firstScreen.validate()).thenReturn(ValidationResult.ok());
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(secondScreen).getDefaultFocusComponent();
        assertEquals(secondScreen, screenMgr.current());
    }

    @Test
    public void abandonNextPageWhenPrevalidateFails() {
        final Screen firstScreen = defaultScreen(FIRST_SCREEN);
        when(firstScreen.prevalidate()).thenReturn(false);
        final Screen secondScreen = defaultScreen(SECOND_SCREEN);
        screens.addAll(Arrays.asList(firstScreen, secondScreen));

        nav.startNavigation();
        nav.moveToNext();

        verify(firstScreen, never()).validate();
        verifyNoInteractions(dialogManager);
        assertEquals(firstScreen, screenMgr.current());
    }

    // navigation button state
    // moveToPrevious tests
    // postload action tests

    private Screen defaultScreen(String name) {
        final Screen firstScreen = mock(Screen.class);
        when(firstScreen.toString()).thenReturn("Screen["+name+"]");
        when(firstScreen.isActive()).thenReturn(true);
        return firstScreen;
    }
}
