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

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.hook.repository.RepositoryHookService;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.project.Project;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryCreateRequest;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.scm.AvailableScm;
import com.atlassian.bitbucket.scm.FeatureUnsupportedScmException;
import com.atlassian.bitbucket.scm.PluginMergeStrategies;
import com.atlassian.bitbucket.scm.ScmFeature;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.stash.internal.attach.AttachmentService;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.scm.InternalScmService;
import com.atlassian.stash.internal.scm.SimpleAvailableScm;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.*;

import static com.atlassian.stash.internal.web.admin.RepositoryAdminController.*;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.*;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class RepositoryAdminControllerTest {

    private static final String PROJECT_KEY = "PROJECT_KEY";
    private static final String REPO_SLUG = "repo";
    private static final String HELP_TOPIC = "help.topic";
    private static final String SCM = "git";

    @Mock
    private AttachmentService attachmentService;
    @Mock
    private AuthenticationContext authenticationContext;
    @InjectMocks
    private RepositoryAdminController controller;
    @Mock
    private Errors errors;
    @Mock
    private PluginFormFragmentsFactory fragmentsFactory;
    @Mock
    private PluginFormFragments fragments;
    @Mock
    private HelpPathService helpPathService;
    @Mock
    private RepositoryHookService hookService;
    @Spy
    @SuppressWarnings("unused")
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private NavBuilder navBuilder;
    @Mock
    private PermissionService permissionService;
    @Mock
    private PluginMergeStrategies pluginMergeStrategies;
    @Mock
    private NavBuilder.Project projectNavBuilder;
    @Mock
    private ApplicationPropertiesService propertiesService;
    @Mock
    private InternalProjectService projectService;
    @Mock
    private Project project;
    @Mock
    private NavBuilder.PullRequestRepoSettings prRepoSettingsNavBuilder;
    @Mock
    private PullRequestService pullRequestService;
    @Mock
    private RefService refService;
    @Mock
    private Repository repository;
    @Mock
    private RepositoryForm repositoryForm;
    @Mock
    private RepositoryService repositoryService;
    @Mock
    private NavBuilder.Repo repoNavBuilder;

    @Mock
    private NavBuilder.RepoSettings repoSettingsNavBuilder;
    @Mock
    private InternalScmService scmService;

    @Before
    public void setUp() throws Exception {
        when(helpPathService.getPageUrl(eq(HELP_TOPIC))).thenReturn("help.url");
        when(navBuilder.project(anyString())).thenReturn(projectNavBuilder);
        when(navBuilder.repo(eq(repository))).thenReturn(repoNavBuilder);

        when(permissionService.hasProjectPermission(project, Permission.PROJECT_ADMIN)).thenReturn(true);
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)).thenReturn(true);

        when(pluginMergeStrategies.getHelpTopic()).thenReturn(HELP_TOPIC);

        when(projectNavBuilder.repo(anyString())).thenReturn(repoNavBuilder);

        when(projectService.getByKey(PROJECT_KEY)).thenReturn(project);

        when(repoNavBuilder.settings()).thenReturn(repoSettingsNavBuilder);
        when(repoSettingsNavBuilder.pullRequests()).thenReturn(prRepoSettingsNavBuilder);
        when(prRepoSettingsNavBuilder.buildRelNoContext()).thenReturn("URL");

        when(repository.getId()).thenReturn(1);
        when(repository.getSlug()).thenReturn(REPO_SLUG);
        when(repository.getName()).thenReturn(REPO_SLUG);
        when(repository.getProject()).thenReturn(project);
        when(repository.getScmId()).thenReturn(SCM);

        when(repositoryService.create(isA(RepositoryCreateRequest.class))).thenReturn(repository);
        when(repositoryService.getBySlug(PROJECT_KEY, REPO_SLUG)).thenReturn(repository);

        when(scmService.getAvailable())
                .thenReturn(ImmutableSet.<AvailableScm>of(new SimpleAvailableScm.Builder(SCM).name("Git").build()));
        when(scmService.isSupported(eq(repository), eq(ScmFeature.MERGE_STRATEGIES))).thenReturn(true);

    }

    @Test
    public void testRepositoryList() throws Exception {
        RedirectView view = controller.repositoryList(PROJECT_KEY);
        assertNotNull(view);
        verify(navBuilder).project(PROJECT_KEY);
        verifyNoMoreInteractions(navBuilder);
        verify(projectNavBuilder).buildRelNoContext();
        verifyNoMoreInteractions(projectNavBuilder);
    }

    @Test
    public void testCreateRepositoryViewWithPermission() throws Exception {
        ModelAndView modelAndView = controller.createRepositoryView(PROJECT_KEY);
        assertNotNull(modelAndView);
        verify(permissionService).hasProjectPermission(project, Permission.PROJECT_ADMIN);
        verifyNoMoreInteractions(permissionService);
        verify(projectService).getByKey(PROJECT_KEY);
        verifyNoMoreInteractions(projectService);
    }

    @Test(expected = AccessDeniedException.class)
    public void testCreateRepositoryViewWithoutPermission() throws Exception {
        reset(permissionService);
        when(permissionService.hasProjectPermission(project, Permission.PROJECT_ADMIN)).thenReturn(false);
        try {
            controller.createRepositoryView(PROJECT_KEY);
        } finally {
            verify(permissionService).hasProjectPermission(project, Permission.PROJECT_ADMIN);
            verifyNoMoreInteractions(permissionService);
            verify(projectService).getByKey(PROJECT_KEY);
            verifyNoMoreInteractions(projectService);
        }
    }

    @Test(expected = NoSuchEntityException.class)
    public void testCreateRepositoryViewWithProjectNotFound() throws Exception {
        try {
            controller.createRepositoryView("NOT_FOUND");
        } finally {
            verify(projectService).getByKey("NOT_FOUND");
            verifyNoMoreInteractions(projectService);
        }
    }

    @Test
    public void testCreateRepositoryWithPermission() throws Exception {
        when(repositoryForm.getName()).thenReturn("repo");
        when(repositoryForm.getScmId()).thenReturn("hg");

        ModelAndView modelAndView = controller.createRepository(PROJECT_KEY, repositoryForm, errors);
        assertNotNull(modelAndView);

        ArgumentCaptor<RepositoryCreateRequest> requestCaptor = ArgumentCaptor.forClass(RepositoryCreateRequest.class);

        verify(projectService).getByKey(PROJECT_KEY);
        verify(repositoryService).create(requestCaptor.capture());

        RepositoryCreateRequest request = requestCaptor.getValue();
        assertEquals(repositoryForm.getName(), request.getName());
        assertEquals("hg", request.getScmId());

        assertThat(modelAndView.getView(), new IsInstanceOf(RedirectView.class));
    }

    @Test
    public void testUpdateMergeStrategiesNoDefault() throws Exception {
        Set<Map<String, String>> errorMap = controller.updateMergeStrategySettings(repository, ImmutableMap.of());

        assertThat(errorMap, hasItem(new MergeErrorMatcher(MERGE_CONFIG_DEFAULT_STRATEGY_UNSET)));
        assertThat(errorMap, hasItem(new MergeErrorMatcher(MERGE_CONFIG_NO_STRATEGY_ENABLED)));
    }

    @Test
    public void testUpdateMergeStrategiesBadStrategy() throws Exception {
        doThrow(new ArgumentValidationException(mock(KeyedMessage.class))).when(pullRequestService)
                .setMergeConfig(any(SetPullRequestMergeConfigRequest.class));
        Set<Map<String, String>> errorMap = controller.updateMergeStrategySettings(repository, ImmutableMap.of(
                MERGE_CONFIG_DEFAULT_STRATEGY_ID, new String[]{"bad-ff"}, MERGE_CONFIG_ENABLED_STRATEGY_IDS, new String[]{"ff"}));

        assertThat(errorMap, hasItem(new MergeErrorMatcher(MERGE_CONFIG_ERROR)));
    }

    @Test
    public void testUpdateMergeStrategiesSCMUnsupported() throws Exception {
        when(fragmentsFactory.forKey(eq(RepositoryAdminController.FRAGS_PULL_REQUEST_SETTINGS), notNull(Map.class)))
                .thenReturn(fragments);
        when(scmService.isSupported(eq(repository), eq(ScmFeature.MERGE_STRATEGIES))).thenReturn(false);
        doThrow(new FeatureUnsupportedScmException(mock(KeyedMessage.class), "git", ScmFeature.MERGE_STRATEGIES))
                .when(pullRequestService)
                .setMergeConfig(any(SetPullRequestMergeConfigRequest.class));
        ModelAndView modelAndView = controller.updatePullRequestSettings(PROJECT_KEY, REPO_SLUG, mock(HttpServletRequest.class), mock(RedirectAttributes.class));
        verify(pullRequestService, times(0)).setMergeConfig(any(SetPullRequestMergeConfigRequest.class));
        assertFalse("No merge strategy validations occur", modelAndView.getModel().containsKey("mergeStrategyErrors"));
        assertTrue("Frontend told to omit merge strategy", modelAndView.getViewName().startsWith("redirect:"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testRepositoryLocationIsNotRetrievedWhenNotSysAdmin() throws Exception {
        File repositoryDir = new File("/tmp/path/to/repo");

        when(fragments.getViewHtml()).thenReturn("fragmentsView");
        when(fragmentsFactory.forKey(eq(RepositoryAdminController.FRAGS_GENERAL_SETTINGS), notNull(Map.class)))
                .thenReturn(fragments);
        when(propertiesService.getRepositoryDir(repository)).thenReturn(repositoryDir);

        assertNull(controller.getSettings(PROJECT_KEY, "repo").getModel().get("repositoryLocation"));
        verify(propertiesService, never()).getRepositoryDir(isA(Repository.class));

        when(permissionService.hasGlobalPermission(Permission.SYS_ADMIN)).thenReturn(true);

        assertEquals(controller.getSettings(PROJECT_KEY, "repo").getModel().get("repositoryLocation"),
                repositoryDir.getAbsolutePath());
        verify(propertiesService, times(1)).getRepositoryDir(isA(Repository.class));
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetPullRequestSettingsWhenNotAuthorized() throws Exception {
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)).thenReturn(false);
        controller.getPullRequestSettings(PROJECT_KEY, "repo");
    }

    @Test(expected = AccessDeniedException.class)
    public void testApplyPullRequestSettingsWhenNotAuthorized() throws Exception {
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)).thenReturn(false);
        controller.updatePullRequestSettings(PROJECT_KEY, "repo", mock(HttpServletRequest.class), mock(RedirectAttributes.class));
    }

    @Test
    public void testMergeConfigToMap() {
        mockMergeConfig();

        Map<String, ?> mergeConfigMap = controller.mergeConfigToMap(repository);

        Map<String, Object> strategyMap = ImmutableMap.of(
                "flag", "flag",
                "description", "description",
                "id", "id",
                "isEnabled", true,
                "name", "name");

        Map<String, Object> defaultEntry = (Map<String, Object>) mergeConfigMap.get(MERGE_CONFIG_DEFAULT_STRATEGY);
        assertNotNull(defaultEntry);
        assertEquals(strategyMap, defaultEntry);

        List<Object> enabledEntries = (List<Object>) mergeConfigMap.get(MERGE_CONFIG_STRATEGIES);
        assertNotNull(enabledEntries);
        assertEquals(Collections.singletonList(strategyMap), enabledEntries);

        assertEquals(PullRequestMergeConfigType.REPOSITORY.name(), mergeConfigMap.get(MERGE_CONFIG_TYPE));
    }

    @Test
    public void testGetPullRequestSettings() {
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)).thenReturn(true);
        PluginFormFragments fragments = mock(PluginFormFragments.class);
        when(fragmentsFactory.forKey(anyString(), anyMapOf(String.class, Object.class))).thenReturn(fragments);
        when(scmService.getMergeStrategies(eq(SCM))).thenReturn(pluginMergeStrategies);

        mockMergeConfig();

        controller.getPullRequestSettings(PROJECT_KEY, "repo");

        verify(fragmentsFactory).forKey(eq(FRAGS_PULL_REQUEST_SETTINGS), (Map) argThat(hasEntry("repository", repository)));
        verify(fragments).getViewHtml();
        verify(pullRequestService).getMergeConfig(any(GetPullRequestMergeConfigRequest.class));
    }

    private void mockMergeConfig() {
        PullRequestMergeConfig config = mock(PullRequestMergeConfig.class);
        PullRequestMergeStrategy defaultStrategy = mock(PullRequestMergeStrategy.class);
        when(defaultStrategy.getName()).thenReturn("name");
        when(defaultStrategy.getId()).thenReturn("id");
        when(defaultStrategy.getDescription()).thenReturn("description");
        when(defaultStrategy.getFlag()).thenReturn(Optional.of("flag"));
        when(defaultStrategy.isEnabled()).thenReturn(true);

        when(config.getDefaultStrategy()).thenReturn(defaultStrategy);
        when(config.getStrategies()).thenReturn(Collections.singleton(defaultStrategy));
        when(config.getType()).thenReturn(PullRequestMergeConfigType.REPOSITORY);

        when(pullRequestService.getMergeConfig(any(GetPullRequestMergeConfigRequest.class))).thenReturn(config);
    }

    private static class MergeErrorMatcher extends BaseMatcher<Collection<Map<String, Object>>> {

        private final String key;

        private MergeErrorMatcher(String key) {
            this.key = key;
        }

        @SuppressWarnings("unchecked")
        @Override
        public boolean matches(Object item) {
            if (item instanceof Map) {
                Object idKey = ((Map<String, Object>) item).get("id");
                if (idKey != null && idKey.equals(key)) {
                        return true;
                }
            }
            return false;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("Map should contain key " + key);
        }
    }
}
