package com.atlassian.stash.internal.web.setup;

import com.atlassian.bitbucket.hamcrest.SimpleTypeSafeMatcher;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.jira.*;
import com.atlassian.bitbucket.license.LicenseService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionAdminService;
import com.atlassian.bitbucket.permission.SetPermissionRequest;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.bitbucket.user.DummySecurityService;
import com.atlassian.bitbucket.user.UserAdminService;
import com.atlassian.bitbucket.user.UserService;
import com.atlassian.stash.internal.i18n.LocaleHolder;
import com.atlassian.stash.internal.language.InternalLanguageService;
import com.atlassian.stash.internal.migration.MigrationService;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import com.atlassian.stash.internal.spring.AbstractModelAndViewMatcher;
import com.atlassian.stash.internal.user.InternalNormalUser;
import com.atlassian.stash.internal.user.InternalPermissionService;
import com.atlassian.stash.internal.web.admin.DbConfigForm;
import com.google.common.collect.*;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

import static com.atlassian.stash.internal.web.setup.SetupController.*;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.notNull;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class SetupControllerTest {

    private static final URI BASE_URL = URI.create("http://localhost:7990/bitbucket");
    private static final String LICENSE = "STASH ENCODED LICENSE";
    private static final String REDIRECT_VIEW = "redirect:/";
    private static final String KEY_BASE_URL = "baseUrl";

    @InjectMocks
    private SetupController controller;
    @Mock
    private Errors errors;
    @Mock
    private JiraSetupService jiraSetupService;
    @Mock
    private LicenseService licenseService;
    @Mock
    private InternalPermissionService permissionService;
    @Mock
    private PermissionAdminService permissionAdminService;
    @Mock
    private InternalApplicationPropertiesService propertiesService;
    @Mock
    private MigrationService migrationService;
    @Mock
    private NavBuilder navBuilder;
    @Spy
    private DummySecurityService securityService;
    @Mock
    private HttpSession session;
    @Mock
    private UserService userService;
    @Mock
    private UserAdminService userAdminService;
    @Mock
    private InternalLanguageService languageService;
    @Mock
    private LocaleHolder localeHolder;
    @Mock
    private HttpServletRequest request;
    @Mock
    private PluginFormFragmentsFactory fragmentFactory;
    @Mock
    private PluginFormFragments fragments;
    @Spy
    private SimpleI18nService simpleI18nService = new SimpleI18nService();

    @Before
    public void setUp() {
        //noinspection unchecked
        when(navBuilder.setup()).thenReturn(mock(NavBuilder.Builder.class));
        when(userService.getUserByName(notNull(String.class))).thenAnswer(invocation -> new InternalNormalUser.Builder().name((String) invocation.getArguments()[0]).build());
        when(session.getAttribute(SetupController.HAS_SETUP_DB_NAME)).thenReturn(Boolean.TRUE);
        when(localeHolder.getLocales()).thenReturn(ImmutableList.of(Locale.US));
        when(propertiesService.getMode()).thenReturn(ApplicationMode.DEFAULT);
        when(fragmentFactory.forKey(any(), any())).thenReturn(fragments);
    }

    @Test
    public void testSetupWhenAlreadyCompleted() {
        when(propertiesService.isSetup()).thenReturn(true);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals("redirect:/", modelAndView.getViewName());
    }

    @Test
    public void testSetupWhenBlankState() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(false);
        when(propertiesService.getBaseUrl()).thenReturn(null);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(LICENSE_AND_SETTINGS_VIEW, modelAndView.getViewName());
        assertThat(modelAndView.getModel(), hasKey("serverId"));
        assertThat(modelAndView.getModel(), hasKey("generateEvalUrl"));
    }

    @Test
    public void testSetupWhenLicenseIsSet() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(true);
        when(licenseService.getAsString()).thenReturn(LICENSE);
        when(propertiesService.getBaseUrl()).thenReturn(null);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(LICENSE_AND_SETTINGS_VIEW, modelAndView.getViewName());
        assertThat(modelAndView.getModel(), hasKey("serverId"));
        assertThat(modelAndView.getModel(), hasKey("generateEvalUrl"));
        assertThat(modelAndView.getModel(), hasKey("form"));

        assertEquals(LICENSE, ((SettingsForm) modelAndView.getModel().get("form")).getLicense());
    }

    @Test
    public void testSetupWhenBaseUrlIsSet() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(false);
        when(propertiesService.getBaseUrl()).thenReturn(BASE_URL);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(LICENSE_AND_SETTINGS_VIEW, modelAndView.getViewName());
        assertThat(modelAndView.getModel(), hasKey("serverId"));
        assertThat(modelAndView.getModel(), hasKey("generateEvalUrl"));
        assertEquals(BASE_URL.toString(), ((SettingsForm) modelAndView.getModel().get("form")).getBaseUrl());
    }

    @Test
    public void testSetupWhenLicenseAndBaseUrlSet() {
        when(propertiesService.getBaseUrl()).thenReturn(BASE_URL);
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(true);
        when(permissionService.getUsersWithPermission(eq(Permission.SYS_ADMIN))).thenReturn(Collections.<String>emptySet());

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(ADMIN_USER_VIEW, modelAndView.getViewName());
    }

    @Test
    public void testSetupWhenAdminUserSet() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(true);
        when(propertiesService.getBaseUrl()).thenReturn(BASE_URL);
        when(permissionService.getUsersWithPermission(eq(Permission.SYS_ADMIN)))
                .thenReturn(Collections.singleton("supreme.overlord"));

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(JIRA_INTEGRATION_VIEW, modelAndView.getViewName());
    }

    @Test
    public void testSetupWhenInMirrorModeEmptyData() {
        String mirrorFragment = "mirrorFragment";
        String requestUrl = "https://example.com";
        inMirrorMode().withMirrorFragments(mirrorFragment).withRequestUrl(requestUrl);

        ModelAndView modelAndView = controller.setup(session);

        assertThat(modelAndView, mirrorView().withSettings(null, requestUrl).withFragments(mirrorFragment));
    }

    @Test
    public void testSetupWhenInMirrorExistingData() {
        String baseUrl = "https://example.com";
        String title = "AU Mirror";

        inMirrorMode();
        mirrorConfig().withBaseUrl(baseUrl).withTitle(title);

        ModelAndView modelAndView = controller.setup(session);

        assertThat(modelAndView, mirrorView().withSettings(title, baseUrl).withoutSslGloablError());
    }

    @Test
    public void testSetupShowsHttpsErrorOnLoad() {
        String baseUrl = "http://example.com";
        String title = "AU Mirror";

        inMirrorMode();
        mirrorConfig().withBaseUrl(baseUrl).withTitle(title);

        ModelAndView modelAndView = controller.setup(session);

        assertThat(modelAndView, mirrorView().withSettings(title, baseUrl).withSslGlobalError().withBaseUrlSslError());
    }

    @Test
    public void testSaveSettings() {
        SettingsForm form = new SettingsForm();
        form.setApplicationTitle("appTitle");
        form.setBaseUrl(URI.create("http://example.com:8080"));
        form.setLicense("007");

        controller.enterSettings(form, errors);
        verify(propertiesService).setDisplayName(eq("appTitle"));
        verify(propertiesService).setBaseURL(eq(URI.create("http://example.com:8080")));
        verify(licenseService).set(eq("007"));
    }

    @Test
    public void testSaveAdminUser() {
        AdminUserForm form = new AdminUserForm();
        form.setUsername("abc");
        form.setFullname("fullname");
        form.setEmail("abc@example.com");
        form.setPassword("secret");
        form.setSetupJira(Boolean.FALSE.toString());

        controller.addFirstAdminUser(form, errors);
        verify(userAdminService).createUser(eq("abc"), eq("secret"), eq("fullname"), eq("abc@example.com"));
        verify(permissionAdminService).setPermission(argThat(new SimpleTypeSafeMatcher<SetPermissionRequest>() {
            protected boolean matchesSafely(SetPermissionRequest request) {
                return request.getPermission() == Permission.SYS_ADMIN
                        && "abc".equals(Iterables.getOnlyElement(request.getUsers()).getName())
                        && request.getGroups().isEmpty();
            }
        }));
    }

    @Test
    public void testSaveJiraSettings() throws Exception {
        URI jiraUrl = new URI("http://mycompany.com/jira");
        URI stashUrl = new URI("http://mycompany.com/stash");
        JiraIntegrationForm form = new JiraIntegrationForm();
        form.setSkip(false);
        form.setStashBaseUrl(stashUrl.toASCIIString());
        form.setJiraBaseUrl(jiraUrl.toASCIIString());
        form.setJiraAdminUsername("admin-jira");
        form.setJiraAdminPassword("secret");
        form.setAddUserDirectory(true);

        controller.setupJiraIntegration(form, errors);
        verify(jiraSetupService).createLink(eq(jiraUrl), eq(stashUrl), eq("admin-jira"), eq("secret"), eq(true));
    }

    @Test(expected = IllegalStateException.class)
    public void testCallbackFromMacWhenAlreadySetup() {
        when(propertiesService.isSetup()).thenReturn(true);
        controller.callbackForMac(mock(SettingsForm.class), mock(Errors.class));
    }

    // ========================================== Verify Setup Tracking ================================================

    /*
     * Initial Setup screen
     */
    @Test
    public void testDataAttributesPresentOnSetupStart() {
        when(propertiesService.getJdbcDriver()).thenReturn("org.h2.Driver");
        when(languageService.getLanguages()).thenReturn(new TreeSet<>());
        when(propertiesService.getLocale()).thenReturn(Locale.US);
        when(session.getAttribute(SetupController.HAS_SETUP_DB_NAME)).thenReturn(Boolean.FALSE);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(DATABASE_VIEW, modelAndView.getViewName());
        assertSetupTrackingDataAttributes(modelAndView);
    }

    /*
     * Settings and License screen
     */
    @Test
    public void testDataAttributesPresentOnLicenseAndSettings() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(false);

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(LICENSE_AND_SETTINGS_VIEW, modelAndView.getViewName());
        assertSetupTrackingDataAttributes(modelAndView);
    }

    /*
     * Admin user screen
     */
    @Test
    public void testDataAttributesPresentOnAdminUser() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(true);
        when(propertiesService.getBaseUrl()).thenReturn(BASE_URL);
        when(permissionService.getUsersWithPermission(eq(Permission.SYS_ADMIN))).thenReturn(Collections.<String>emptySet());

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(ADMIN_USER_VIEW, modelAndView.getViewName());
        assertSetupTrackingDataAttributes(modelAndView);
    }

    /*
     * JIRA Integration screens - this only tests the initial screen and error conditions
     * as a successful form submission will result in a redirect to the dashboard
     */
    @Test
    public void testDataAttributesPresentOnJiraIntegrationScreen() {
        when(propertiesService.isSetup()).thenReturn(false);
        when(licenseService.isPresent()).thenReturn(true);
        when(propertiesService.getBaseUrl()).thenReturn(BASE_URL);
        when(permissionService.getUsersWithPermission(eq(Permission.SYS_ADMIN)))
                .thenReturn(Collections.singleton("supreme.overlord"));

        ModelAndView modelAndView = controller.setup(session);
        assertEquals(JIRA_INTEGRATION_VIEW, modelAndView.getViewName());
        assertSetupTrackingDataAttributes(modelAndView);
    }

    @Test
    public void testDataAttributesPresentDuringJiraIntegrationNotAJiraServerException() throws Exception {
        doThrow(new NotAJiraServerException("")).
                when(jiraSetupService).
                createLink(any(URI.class), any(URI.class), anyString(), anyString(), anyBoolean());

        submitAndVerifyJiraFormDataAttributes();
    }

    @Test
    public void testDataAttributesPresentDuringJiraIntegrationJiraUserCredentialsException() throws Exception {
        doThrow(new JiraUserCredentialsException("")).
                when(jiraSetupService).
                createLink(any(URI.class), any(URI.class), anyString(), anyString(), anyBoolean());

        submitAndVerifyJiraFormDataAttributes();
    }

    @Test
    public void testDataAttributesPresentDuringJiraIntegrationIncorrectStashBaseUrlException() throws Exception {
        doThrow(new IncorrectApplicationBaseUrlException("", new RuntimeException())).
                when(jiraSetupService).
                createLink(any(URI.class), any(URI.class), anyString(), anyString(), anyBoolean());

        submitAndVerifyJiraFormDataAttributes();
    }

    @Test
    public void testDataAttributesPresentDuringJiraIntegrationStashApplicationTypeNotInstalledException() throws Exception {
        doThrow(new ApplicationTypeNotInstalledException("", new RuntimeException())).
                when(jiraSetupService).
                createLink(any(URI.class), any(URI.class), anyString(), anyString(), anyBoolean());

        submitAndVerifyJiraFormDataAttributes();
    }

    @Test
    public void testDataAttributesPresentDuringJiraIntegrationJiraSetupException() throws Exception {
        doThrow(new JiraSetupException("", new RuntimeException())).
                when(jiraSetupService).
                createLink(any(URI.class), any(URI.class), anyString(), anyString(), anyBoolean());

        submitAndVerifyJiraFormDataAttributes();
    }

    @Test
    public void testConfigureDatabaseFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.configureDatabase(request, session, true, null, new DbConfigForm(), null));
    }

    @Test
    public void testConfigureDatabaseFailsWhenInMirrorMode() {
        assertFailInMirrorMode(() -> controller.configureDatabase(request, session, true, null, new DbConfigForm(), null));
    }

    @Test
    public void testCallbackForMacFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.callbackForMac(new SettingsForm(), null));
    }

    @Test
    public void testCallbackForMacFailsWhenInMirrorMode() {
        assertFailInMirrorMode(() -> controller.callbackForMac(new SettingsForm(), null));
    }

    @Test
    public void testEnterSettingsFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.enterSettings(new SettingsForm(), null));
    }

    @Test
    public void testEnterSettingsFailsWhenInMirrorMode() {
        assertFailInMirrorMode(() -> controller.enterSettings(new SettingsForm(), null));
    }

    @Test
    public void testAddFirstAdminUserFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.addFirstAdminUser(new AdminUserForm(), null));
    }

    @Test
    public void testAddFirstAdminUserFailsWhenInMirrorMode() {
        assertFailInMirrorMode(() -> controller.addFirstAdminUser(new AdminUserForm(), null));
    }

    @Test
    public void testSetupJiraIntegrationFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.setupJiraIntegration(new JiraIntegrationForm(), null));
    }

    @Test
    public void testSetupJiraIntegrationFailsWhenInMirrorMode() {
        assertFailInMirrorMode(() -> controller.setupJiraIntegration(new JiraIntegrationForm(), null));
    }

    @Test
    public void testSetupMirrorFailsWhenNotSetup() {
        assertFailWhenAlreadySetup(() -> controller.setupMirror(new MirrorForm(), null, request));
    }

    @Test
    public void testSetupMirrorFailsWhenWhenInDefaultMode() {
        assertFailInDefaultMode(() -> controller.setupMirror(new MirrorForm(), null, request));
    }

    @Test
    public void testSetupMirrorNoErrors() {
        String title = "title";
        String baseUrl = "https://example.com";
        String parameterName = "mirror";
        String parameterValue = "true";
        MirrorForm form = new MirrorForm(title, baseUrl);

        inMirrorMode().withRequestParameters(parameterName, parameterValue);

        ModelAndView modelAndView = controller.setupMirror(form, errors, request);

        assertThat(modelAndView, redirectToMirror());

        verify(propertiesService).setDisplayName(title);
        verify(propertiesService).setBaseURL(URI.create(baseUrl));
        verify(propertiesService).setSetup(true);
        verify(fragments).validate(parameters(parameterName, parameterValue), any());
        verify(fragments).execute(parameters(parameterName, parameterValue));
    }

    @Test
    public void testSetupMirrorReportsValidationErrors() {
        String title = "title";
        String baseUrl = "https://example.com";
        String field = KEY_BASE_URL;
        String fieldError = "error";
        String someError = "someError";
        MirrorForm form = new MirrorForm(title, baseUrl);

        inMirrorMode().withFieldError(field, fieldError).withError(someError);

        ModelAndView modelAndView = controller.setupMirror(form, errors, request);

        assertThat(modelAndView, mirrorView().withFieldError(field, fieldError).withFormError(someError));
        assertMirrorNotSetup();
    }

    @Test
    public void testSetupMirrorReportsSslErrors() {
        String title = "title";
        String baseUrl = "http://example.com";
        String field = "field";
        String fieldError = "otherField";
        String someError = "someError";
        MirrorForm form = new MirrorForm(title, baseUrl);

        inMirrorMode().withFieldError(field, fieldError).withError(someError);

        ModelAndView modelAndView = controller.setupMirror(form, errors, request);

        assertThat(modelAndView, mirrorView().withFieldError(field, fieldError)
                .withFormError(someError)
                .withBaseUrlSslError()
                .withSslGlobalError());
        assertMirrorNotSetup();
    }

    @Test
    public void testSetupMirrorReportsErrorsFromPlugin() {
        String title = "title";
        String baseUrl = "http://example.com";
        String field = "field";
        String fieldError = "error";
        String errorHtml = "errorHtml";
        String parameterName = "mirror";
        String parameterValue = "true";
        MirrorForm form = new MirrorForm(title, baseUrl);

        inMirrorMode()
                .withMirrorFragmentFieldError(field, fieldError)
                .withMirrorFragmentErrorHtml(errorHtml)
                .withRequestParameters(parameterName, parameterValue);

        ModelAndView modelAndView = controller.setupMirror(form, errors, request);

        assertThat(modelAndView, mirrorView().withFragments(errorHtml).withBaseUrlSslError());
        assertMirrorNotSetup();

        verify(fragments).validate(parameters(parameterName, parameterValue), any());
        verify(fragments).getErrorHtml(parameters(parameterName, parameterValue),
                eq(ImmutableListMultimap.of(field, fieldError).asMap()));
    }

    private void assertMirrorNotSetup() {
        verify(propertiesService, never()).setDisplayName(anyString());
        verify(propertiesService, never()).setBaseURL(any());
        verify(propertiesService, never()).setSetup(anyBoolean());
        verify(fragments, never()).execute(any());
    }

    private void submitAndVerifyJiraFormDataAttributes() throws URISyntaxException {
        URI jiraUrl = new URI("http://mycompany.com/jira");
        URI stashUrl = new URI("http://mycompany.com/jira");

        JiraIntegrationForm form = new JiraIntegrationForm();
        form.setSkip(false);
        form.setStashBaseUrl(stashUrl.toASCIIString());
        form.setJiraBaseUrl(jiraUrl.toASCIIString());
        form.setJiraAdminUsername("admin-jira");
        form.setJiraAdminPassword("secret");
        form.setAddUserDirectory(true);

        ModelAndView modelAndView = controller.setupJiraIntegration(form, errors);
        assertEquals(JIRA_INTEGRATION_VIEW, modelAndView.getViewName());
        assertSetupTrackingDataAttributes(modelAndView);
    }

    private void assertSetupTrackingDataAttributes(ModelAndView mv) {
        String key = "dataAttributes";
        assertThat(mv.getModel(), hasKey(key));

        @SuppressWarnings("unchecked")
        Map<String, String> dataAttributes = (Map) mv.getModel().get(key);
        assertThat(dataAttributes, hasKey("server-id"));
        assertThat(dataAttributes, hasKey("dev-mode-enabled"));
    }

    private SetupControllerTest withFieldError(String field, String error) {
        when(errors.hasErrors()).thenReturn(true);
        when(errors.getFieldErrors()).thenReturn(ImmutableList.of(new FieldError("form", field, error)));
        return this;
    }

    private SetupControllerTest withError(String error) {
        when(errors.hasErrors()).thenReturn(true);
        when(errors.getGlobalErrors()).thenReturn(ImmutableList.of(new ObjectError("form", error)));
        return this;
    }

    private SetupControllerTest mirrorConfig() {
        return this;
    }

    private SetupControllerTest withTitle(String title) {
        when(propertiesService.getDisplayName()).thenReturn(title);

        return this;
    }

    private SetupControllerTest withBaseUrl(String baseUrl) {
        when(propertiesService.getBaseUrl()).thenReturn(URI.create(baseUrl));

        return this;
    }

    private SetupControllerTest inMirrorMode() {
        when(propertiesService.getMode()).thenReturn(ApplicationMode.MIRROR);
        return this;
    }

    private void inDefaultMode() {
        when(propertiesService.getMode()).thenReturn(ApplicationMode.DEFAULT);
    }

    private void alreadySetup() {
        when(propertiesService.isSetup()).thenReturn(true);
    }

    private static MirrorViewMatcher mirrorView() {
        return new MirrorViewMatcher();
    }

    private SetupControllerTest withMirrorFragments(String mirrorFragment) {
        when(fragments.getViewHtml()).thenReturn(mirrorFragment);
        return this;
    }

    private SetupControllerTest withMirrorFragmentFieldError(String fieldName, String fieldError) {
        doAnswer(invocation -> {
            ValidationErrors errors = invocation.getArgumentAt(1, ValidationErrors.class);
            errors.addFieldError(fieldName, fieldError);
            return null;
        }).when(fragments).validate(any(), any());
        return this;
    }

    private SetupControllerTest withMirrorFragmentErrorHtml(String errorHtml) {
        when(fragments.getErrorHtml(any(), any())).thenReturn(errorHtml);
        return this;
    }

    private SetupControllerTest withRequestParameters(String name, String...args) {
        Map<String, String[]> parameters = ImmutableMap.of(name, args);
        when(request.getParameterMap()).thenReturn(parameters);
        return this;
    }

    private SetupControllerTest withRequestUrl(String url) {
        when(navBuilder.buildAbsolute()).thenReturn(url);
        return this;
    }

    private static Map<String, String[]> parameters(String name, String... args) {

        Map<String, List<String>> expectedParameters = Maps.transformValues(ImmutableMap.of(name, args), Lists::newArrayList);

        return Mockito.argThat(new TypeSafeDiagnosingMatcher<Map<String, String[]>>() {
            @Override
            protected boolean matchesSafely(Map<String, String[]> item, Description mismatchDescription) {
                Map<String, List<String>> actualParameters = Maps.transformValues(item, Lists::newArrayList);
                if (actualParameters.equals(expectedParameters)) {
                    return true;
                } else {
                    mismatchDescription.appendValue(actualParameters);
                    return false;
                }
            }

            @Override
            public void describeTo(Description description) {
                description.appendValue(expectedParameters);
            }
        });
    }

    private static Matcher<ModelAndView> redirectToMirror() {
        return new RedirectMatcher("/mirror?setup=true");
    }

    private void assertFailInMirrorMode(Runnable call) {
        inMirrorMode();
        try {
            call.run();
            fail("Excepted exception.");
        } catch (IllegalStateException e) {
            //good
        }
    }

    private void assertFailInDefaultMode(Runnable call) {
        inDefaultMode();
        try {
            call.run();
            fail("Excepted exception.");
        } catch (IllegalStateException e) {
            //good
        }
    }

    private void assertFailWhenAlreadySetup(Runnable call) {
        alreadySetup();
        try {
            call.run();
            fail("Excepted exception.");
        } catch (IllegalStateException e) {
            //good
        }
    }

    private static class MirrorViewMatcher extends AbstractModelAndViewMatcher<MirrorViewMatcher> {
        protected MirrorViewMatcher() {
            super(SetupController.MIRROR_VIEW);
        }

        @Override
        protected MirrorViewMatcher self() {
            return this;
        }

        public MirrorViewMatcher withSettings(String applicationTitle, String baseUrl) {
            return withEntry("form", new MirrorFormMatcher(applicationTitle, baseUrl));
        }

        public MirrorViewMatcher withFragments(String mirrorFragment) {
            return withEntry("pluginHtml", mirrorFragment);
        }

        public MirrorViewMatcher withSslGlobalError() {
            return withEntry("sslError", true);
        }

        public MirrorViewMatcher withoutSslGloablError() {
            return withEntry("sslError", false);
        }

        public MirrorViewMatcher withBaseUrlSslError() {
            return withFieldError(KEY_BASE_URL, "bitbucket.web.setup.mirror.baseUrl.https.error");
        }
    }

    private static class MirrorFormMatcher extends TypeSafeDiagnosingMatcher<MirrorForm> {
        private final String applicationTitle;
        private final String baseUrl;

        public MirrorFormMatcher(String applicationTitle, String baseUrl) {
            this.applicationTitle = applicationTitle;
            this.baseUrl = baseUrl;
        }

        @Override
        protected boolean matchesSafely(MirrorForm item, Description mismatchDescription) {
            if (Objects.equals(applicationTitle, item.getApplicationTitle())
                && Objects.equals(baseUrl, item.getBaseUrl())) {
                return true;
            } else {
                mismatchDescription.appendText(toString(item.getApplicationTitle(), item.getBaseUrl()));
                return false;
            }
        }

        @Override
        public void describeTo(Description description) {
            description.appendText(toString(applicationTitle, baseUrl));
        }

        private static String toString(String applicationTitle, String baseUrl) {
            return String.format("[name: %s, baseUrl: %s]", applicationTitle, baseUrl);
        }
    }

    private static class RedirectMatcher extends TypeSafeDiagnosingMatcher<ModelAndView> {

        private static final String REDIRECT = "redirect:";

        private final String redirect;

        private RedirectMatcher(String redirect) {
            this.redirect = REDIRECT + redirect;
        }

        @Override
        protected boolean matchesSafely(ModelAndView item, Description mismatchDescription) {

            if (redirect.equals(item.getViewName()) && item.getModel().isEmpty()) {
                return true;
            } else {
                mismatchDescription.appendText(String.format("%s [%s]", item.getViewName(), item.getModel()));
                return false;
            }
        }

        @Override
        public void describeTo(Description description) {
            description.appendText(String.format("%s []", redirect));
        }
    }
}
