package com.atlassian.stash.internal.commit;

import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.bitbucket.comment.DiffCommentAnchor;
import com.atlassian.bitbucket.comment.commit.AddCommitFileCommentRequest;
import com.atlassian.bitbucket.comment.commit.AddCommitLineCommentRequest;
import com.atlassian.bitbucket.commit.*;
import com.atlassian.bitbucket.commit.graph.TraversalCallback;
import com.atlassian.bitbucket.commit.graph.TraversalRequest;
import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.hamcrest.SimpleTypeSafeMatcher;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.stash.internal.comment.CommentCountChangeCallback;
import com.atlassian.stash.internal.comment.CommentCounts;
import com.atlassian.stash.internal.commit.graph.CommitGraphSource;
import com.atlassian.stash.internal.content.InternalChangeLocation;
import com.atlassian.stash.internal.content.comment.CommitDiscussionCommentAnchorSearchRequest;
import com.atlassian.stash.internal.content.comment.InternalCommitDiscussionCommentHelper;
import com.atlassian.stash.internal.watcher.InternalWatcherService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
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.runners.MockitoJUnitRunner;
import org.springframework.beans.DirectFieldAccessor;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import static java.util.Optional.of;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("unchecked")
public class DefaultCommitServiceTest extends AbstractServiceTest {

    private static final String COMMIT_ID = "abcdef";
    private static final int REPOSITORY_ID = 99;

    @Mock
    private ScmCommandFactory commandFactory;
    @Mock
    private InternalCommitDiscussionCommentHelper commentService;
    @Mock
    private CommitDiscussionSource discussionSource;
    @Mock
    private InternalCommitEnricher enricher;
    @Mock
    private ScmExtendedCommandFactory extendedCommandFactory;
    @Mock
    private CommitGraphSource graphSource;
    @Mock
    private PageRequest pageRequest;
    @Mock
    private PermissionService permissionService;
    @Mock
    private Repository repository;
    @Mock
    private ScmService scmService;
    @InjectMocks
    private DefaultCommitService service;
    @Mock
    private InternalWatcherService watcherService;

    public DefaultCommitServiceTest() {
        super(DefaultCommitService.class, CommitService.class);
    }

    @Before
    public void setup() {
        when(scmService.getCommandFactory(same(repository))).thenReturn(commandFactory);
        when(scmService.getExtendedCommandFactory(same(repository))).thenReturn(extendedCommandFactory);

        DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(service);
        fieldAccessor.setPropertyValue("lastModifiedTimeout", 60L);
        fieldAccessor.setPropertyValue("maxCommits", 100);
        fieldAccessor.setPropertyValue("maxDiffLines", 100);
        fieldAccessor.setPropertyValue("maxLineLength", 1000);
        fieldAccessor.setPropertyValue("maxMessageLength", 1000);
    }

    @Test
    public void testAddDiffComment() {
        when(repository.getId()).thenReturn(REPOSITORY_ID);
        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(argThat(new CommitDiscussionRequestMatcher()))).thenReturn(discussion);

        AddCommitLineCommentRequest request = new AddCommitLineCommentRequest.Builder(repository, COMMIT_ID)
                .fileType(DiffFileType.FROM)
                .fromHash("since")
                .lineType(DiffSegmentType.CONTEXT)
                .line(20)
                .path("pom.xml")
                .text("A comment")
                .build();

        service.addLineComment(request);

        verify(commentService).createLineComment(same(discussion), argThat(new SimpleTypeSafeMatcher<AddCommitLineCommentRequest>() {
            @Override
            protected boolean matchesSafely(AddCommitLineCommentRequest actual) {
                return repository == actual.getRepository() && COMMIT_ID.equals(actual.getToHash())
                        && DiffFileType.FROM.equals(actual.getFileType())
                        && "since".equals(actual.getFromHash().get())
                        && DiffSegmentType.CONTEXT.equals(actual.getLineType())
                        && 20 == actual.getLine() && "pom.xml".equals(actual.getPath())
                        && "A comment".equals(actual.getText());
            }
        }));
    }

    @Test
    public void testAddFileComment() {
        when(repository.getId()).thenReturn(REPOSITORY_ID);
        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(argThat(new CommitDiscussionRequestMatcher()))).thenReturn(discussion);

        AddCommitFileCommentRequest request = new AddCommitFileCommentRequest.Builder(repository, COMMIT_ID)
                .fromHash("since")
                .path("pom.xml")
                .text("A comment")
                .build();

        service.addFileComment(request);

        verify(commentService).createFileComment(same(discussion), argThat(new SimpleTypeSafeMatcher<AddCommitFileCommentRequest>() {
            @Override
            protected boolean matchesSafely(AddCommitFileCommentRequest actual) {
                return request.getRepository() == actual.getRepository() && COMMIT_ID.equals(actual.getToHash())
                        && "since".equals(actual.getFromHash().get()) && "pom.xml".equals(actual.getPath())
                        && "A comment".equals(actual.getText());
            }
        }));
    }

    @Test
    public void testAddFileCommentWithoutFromHash() {
        when(repository.getId()).thenReturn(REPOSITORY_ID);
        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(argThat(new CommitDiscussionRequestMatcher()))).thenReturn(discussion);

        when(discussion.getParentId()).thenReturn("since");

        AddCommitFileCommentRequest request = new AddCommitFileCommentRequest.Builder(repository, COMMIT_ID)
                .path("pom.xml")
                .text("Comment")
                .build();
        service.addFileComment(request);

        verify(commentService).createFileComment(same(discussion), argThat(new SimpleTypeSafeMatcher<AddCommitFileCommentRequest>() {
            @Override
            protected boolean matchesSafely(AddCommitFileCommentRequest request) {
                return request.getRepository() == repository && COMMIT_ID.equals(request.getToHash())
                        && "since".equals(request.getFromHash().get())
                        && "pom.xml".equals(request.getPath())
                        && "Comment".equals(request.getText());
            }
        }));
    }

    @Test
    public void testAddLineCommentWithoutFromHash() {
        when(repository.getId()).thenReturn(REPOSITORY_ID);
        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(argThat(new CommitDiscussionRequestMatcher()))).thenReturn(discussion);

        when(discussion.getParentId()).thenReturn("since");

        AddCommitLineCommentRequest request = new AddCommitLineCommentRequest.Builder(repository, COMMIT_ID)
                .fileType(DiffFileType.FROM)
                .lineType(DiffSegmentType.ADDED)
                .line(20)
                .path("pom.xml")
                .text("Comment")
                .build();
        service.addLineComment(request);

        verify(commentService).createLineComment(same(discussion), argThat(new SimpleTypeSafeMatcher<AddCommitLineCommentRequest>() {
            @Override
            protected boolean matchesSafely(AddCommitLineCommentRequest request) {
                return request.getRepository() == repository && COMMIT_ID.equals(request.getToHash())
                        && DiffFileType.FROM.equals(request.getFileType())
                        && DiffSegmentType.ADDED.equals(request.getLineType())
                        && 20 == request.getLine()
                        && "since".equals(request.getFromHash().get())
                        && "pom.xml".equals(request.getPath())
                        && "Comment".equals(request.getText());
            }
        }));
    }

    @Test
    public void testGetChanges() throws Exception {
        Page<Change> page = mock(Page.class);

        Command<Page<Change>> command = mock(Command.class);
        when(command.call()).thenReturn(page);

        when(commandFactory.changes(isA(ChangesCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(25))).thenReturn(pageRequest);

        setMaxChanges(25);
        ChangesRequest request = new ChangesRequest.Builder(repository, "commitId")
                .sinceId("parentId")
                .build();
        assertSame(page, service.getChanges(request, pageRequest));
    }

    @Test
    public void testGetChangesWithInvalidScmClientImplementation() throws Exception {
        Command<Page<Change>> command = mock(Command.class);

        when(commandFactory.changes(isA(ChangesCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(25))).thenReturn(pageRequest);

        setMaxChanges(25);

        ChangesRequest request = new ChangesRequest.Builder(repository, "commitId")
                .sinceId("parentId")
                .build();
        Page<Change> page = service.getChanges(request, pageRequest);
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());
    }

    @Test
    public void testGetChangesets() throws Exception {
        List<String> commitIds = Collections.singletonList("commitId");

        Page<Changeset> page = mock(Page.class);

        Command<Page<Changeset>> command = mock(Command.class);
        when(command.call()).thenReturn(page);

        when(commandFactory.changesets(isA(ChangesetsCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(100))).thenReturn(pageRequest);

        setMaxChanges(25);
        ChangesetsRequest request = new ChangesetsRequest.Builder(repository)
                .commitIds(commitIds)
                .maxChangesPerCommit(50)
                .build();
        assertSame(page, service.getChangesets(request, pageRequest));
    }

    @Test
    public void testGetChangesetsWithInvalidScmClientImplementation() throws Exception {
        List<String> commitIds = Collections.singletonList("commitId");

        Command<Page<Changeset>> command = mock(Command.class);

        when(commandFactory.changesets(isA(ChangesetsCommandParameters.class), same(pageRequest)))
                .thenReturn(command);
        when(pageRequest.buildRestrictedPageRequest(eq(100))).thenReturn(pageRequest);

        setMaxChanges(25);

        ChangesetsRequest request = new ChangesetsRequest.Builder(repository)
                .commitIds(commitIds)
                .maxChangesPerCommit(50)
                .build();
        Page<Changeset> page = service.getChangesets(request, pageRequest);
        assertNotNull(page);
        assertEquals(0, page.getSize());
        assertTrue(page.getIsLastPage());
    }

    @Test
    public void testStreamChangesWithComments() throws Exception {
        ChangesRequest request = new ChangesRequest.Builder(repository, "until")
                .pullRequestId(99L)
                .withComments(true)
                .build();
        ChangeCallback callback = mock(ChangeCallback.class);
        SimpleChange change = mock(SimpleChange.class);
        Command<Void> changesCommand = mock(Command.class);

        when(change.getPath()).thenReturn(new SimplePath("path"));
        when(change.getSrcPath()).thenReturn(null);
        InternalChangeLocation changeLocation = new InternalChangeLocation.Builder(change).build();

        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(any(CommitDiscussionRequest.class))).thenReturn(discussion);
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_READ)).thenReturn(true);
        when(commentService.countsByLocation(discussion, 99L))
                .thenReturn(ImmutableMap.of(changeLocation, new CommentCounts(1, 1)));
        when(commandFactory.changes(notNull(ChangesCommandParameters.class), notNull(ChangeCallback.class)))
                .thenReturn(changesCommand);

        setMaxChanges(500);

        service.streamChanges(request, callback);

        ArgumentCaptor<ChangesCommandParameters> commandParametersCaptor = ArgumentCaptor.forClass(ChangesCommandParameters.class);
        verify(commandFactory).changes(commandParametersCaptor.capture(), isA(CommentCountChangeCallback.class));
        verify(changesCommand).call();

        assertEquals("until", commandParametersCaptor.getValue().getUntilId());
        assertEquals(500, commandParametersCaptor.getValue().getMaxChanges());
    }

    @Test
    public void testStreamChangesNoPermission() throws Exception {
        ChangesRequest request = new ChangesRequest.Builder(repository, "until").withComments(true).build();
        ChangeCallback callback = mock(ChangeCallback.class);
        SimpleChange change = mock(SimpleChange.class);
        Command<Void> changesCommand = mock(Command.class);

        when(change.getPath()).thenReturn(new SimplePath("path"));
        when(change.getSrcPath()).thenReturn(null);
        InternalChangeLocation changeLocation = new InternalChangeLocation.Builder(change).build();

        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(any(CommitDiscussionRequest.class))).thenReturn(discussion);
        // no permission, no comment counts
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_READ)).thenReturn(false);
        when(commentService.countsByLocation(discussion)).thenReturn(ImmutableMap.of(
                changeLocation, new CommentCounts(1, 1)
        ));
        when(commandFactory.changes(notNull(ChangesCommandParameters.class), notNull(ChangeCallback.class)))
                .thenReturn(changesCommand);

        setMaxChanges(500);

        service.streamChanges(request, callback);

        verify(commandFactory).changes(isA(ChangesCommandParameters.class), same(callback));
        verify(changesCommand).call();
    }

    @Test
    public void testStreamChangesNoCountsAvailable() throws Exception {
        ChangesRequest request = new ChangesRequest.Builder(repository, "until").withComments(true).build();
        ChangeCallback callback = mock(ChangeCallback.class);
        SimpleChange change = mock(SimpleChange.class);
        Command<Void> changesCommand = mock(Command.class);

        when(change.getPath()).thenReturn(new SimplePath("path"));
        when(change.getSrcPath()).thenReturn(null);

        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(any(CommitDiscussionRequest.class))).thenReturn(discussion);
        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_READ)).thenReturn(true);
        // no counts for any change
        when(commentService.countsByLocation(discussion)).thenReturn(Collections.emptyMap());
        when(commandFactory.changes(notNull(ChangesCommandParameters.class), notNull(ChangeCallback.class)))
                .thenReturn(changesCommand);

        setMaxChanges(500);

        service.streamChanges(request, callback);

        verify(commandFactory).changes(isA(ChangesCommandParameters.class), same(callback));
        verify(changesCommand).call();
    }

    @Test
    public void testStreamDiff() throws Exception {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        Command<Void> command = mock(Command.class);

        when(commandFactory.diff(argThat(new SimpleTypeSafeMatcher<DiffCommandParameters>() {

            @Override
            protected boolean matchesSafely(DiffCommandParameters parameters) {
                return parameters.getMaxLineLength() == 1000 &&
                        parameters.getMaxLines() == 100 &&
                        "since".equals(parameters.getSinceId()) &&
                        "until".endsWith(parameters.getUntilId()) &&
                        parameters.getWhitespace() == DiffWhitespace.IGNORE_ALL &&
                        "pom.xml".equals(Iterables.getOnlyElement(parameters.getPaths()));
            }
        }), same(callback))).thenReturn(command);

        service.streamDiff(new DiffRequest.Builder(repository, "until")
                .path("pom.xml")
                .sinceId("since")
                .whitespace(DiffWhitespace.IGNORE_ALL)
                .build(), callback);

        verify(command).call();
        verifyNoMoreInteractions(command);
    }

    @Test
    public void testStreamDiffWithComments() throws IOException {
        DiffContentCallback callback = mock(DiffContentCallback.class);
        Command<Void> command = mock(Command.class);

        when(repository.getId()).thenReturn(REPOSITORY_ID);

        InternalCommitDiscussion discussion = mock(InternalCommitDiscussion.class);
        when(discussionSource.get(argThat(new CommitDiscussionRequestMatcher(false, false)))).thenReturn(discussion);

        when(commandFactory.diff(argThat(new SimpleTypeSafeMatcher<DiffCommandParameters>() {

            @Override
            protected boolean matchesSafely(DiffCommandParameters parameters) {
                return parameters.getMaxLineLength() == 1000 &&
                        parameters.getMaxLines() == 100 &&
                        "since".equals(parameters.getSinceId()) &&
                        COMMIT_ID.endsWith(parameters.getUntilId()) &&
                        parameters.getWhitespace() == DiffWhitespace.IGNORE_ALL &&
                        "pom.xml".equals(Iterables.getOnlyElement(parameters.getPaths()));
            }
        }), same(callback))).thenReturn(command);

        when(permissionService.hasRepositoryPermission(repository, Permission.REPO_READ)).thenReturn(true);
        List<DiffCommentAnchor> anchors = ImmutableList.of(mock(DiffCommentAnchor.class));
        when(commentService.findDiffAnchors(
                argThat(new CommitDiscussionCommentAnchorSearchRequestMatcher(discussion, "since", "pom.xml", 99L)),
                eq(true))).thenReturn(anchors);

        service.streamDiff(new DiffRequest.Builder(repository, COMMIT_ID)
                .path("pom.xml")
                .pullRequest(99L)
                .sinceId("since")
                .whitespace(DiffWhitespace.IGNORE_ALL)
                .withComments(true)
                .build(), callback);

        verify(command).call();
        verifyNoMoreInteractions(command);
        verify(callback).offerAnchors(anchors);
    }

    @Test
    public void testStreamLastModified() throws Exception {
        LastModifiedCallback callback = mock(LastModifiedCallback.class);
        Command<Void> command = mock(Command.class);

        ArgumentCaptor<LastModifiedCallback> callbackCaptor = ArgumentCaptor.forClass(LastModifiedCallback.class);

        when(extendedCommandFactory.lastModified(argThat(new SimpleTypeSafeMatcher<LastModifiedCommandParameters>() {

            @Override
            protected boolean matchesSafely(LastModifiedCommandParameters parameters) {
                return "commit".equals(parameters.getCommitId()) &&
                        "path".equals(parameters.getPath()) &&
                        1000 == parameters.getMaxMessageLength(); //-1 from the request should be restricted
            }
        }), callbackCaptor.capture())).thenReturn(command);

        service.streamLastModified(new LastModifiedRequest.Builder(repository, "commit")
                .path("path")
                .build(), callback);

        verify(command).call();
        verify(command).setExecutionTimeout(eq(60L));
        verify(command).setIdleTimeout(eq(60L));

        assertEnrichingCallback(callback, callbackCaptor.getValue());
    }

    @Test
    public void testTraverse() throws Exception {
        TraversalRequest request = mock(TraversalRequest.class);
        TraversalCallback callback = mock(TraversalCallback.class);
        service.traverse(request, callback);

        verify(graphSource).traverse(same(request), same(callback));
    }

    private void assertEnrichingCallback(LastModifiedCallback callback, LastModifiedCallback actualCallback)
            throws IOException {
        assertNotNull(actualCallback);
        assertNotSame(callback, actualCallback); //The callback should be wrapped
        verifyZeroInteractions(callback);        //No interactions against the real callback yet

        //Verify that onStart passes through to the real callback unchanged
        LastModifiedContext context = mock(LastModifiedContext.class);
        actualCallback.onStart(context);
        verify(callback).onStart(same(context));

        //Verify that onFile passes through to the real callback unchanged
        Commit commit = mock(Commit.class, "commit");
        actualCallback.onFile("file.txt", commit);
        verify(callback).onFile(eq("file.txt"), same(commit));
        verifyZeroInteractions(enricher); //Should not be enriching file commits

        //Verify that onEnd enriches the summary's latestCommit
        Commit enriched = mock(Commit.class, "enriched");
        when(enricher.enrich(same(repository), same(commit), isNull(Collection.class))).thenReturn(enriched);

        LastModifiedSummary summary = new LastModifiedSummary.Builder(CommandResult.SUCCEEDED)
                .latestCommit(commit)
                .build();
        actualCallback.onEnd(summary);

        ArgumentCaptor<LastModifiedSummary> summaryCaptor = ArgumentCaptor.forClass(LastModifiedSummary.class);
        verify(callback).onEnd(summaryCaptor.capture());
        verify(enricher).enrich(same(repository), same(commit), isNull(Collection.class));

        LastModifiedSummary actualSummary = summaryCaptor.getValue();
        assertNotNull(actualSummary);
        assertNotSame(summary, actualSummary); //The commit should have been enriched
        assertSame(summary.getResult(), actualSummary.getResult());
        assertEquals(of(enriched), actualSummary.getLatestCommit());
    }

    private void setMaxChanges(int changes) throws Exception {
        setPageLimit(service, "maxChanges", changes);
    }

    private class CommitDiscussionRequestMatcher extends SimpleTypeSafeMatcher<CommitDiscussionRequest> {

        private final boolean create;
        private final boolean participate;

        private CommitDiscussionRequestMatcher() {
            this(true, true);
        }

        private CommitDiscussionRequestMatcher(boolean create, boolean participate) {
            this.create = create;
            this.participate = participate;
        }

        @Override
        protected boolean matchesSafely(CommitDiscussionRequest discussionRequest) {
            return discussionRequest.getRepository().getId() == REPOSITORY_ID
                    && COMMIT_ID.equals(discussionRequest.getCommitId())
                    && discussionRequest.isCreate() == create && discussionRequest.isParticipate() == participate;
        }
    }

    private class CommitDiscussionCommentAnchorSearchRequestMatcher
            extends SimpleTypeSafeMatcher<CommitDiscussionCommentAnchorSearchRequest> {

        private final InternalCommitDiscussion discussion;
        private final String parentId;
        private final String path;
        private final Long pullRequestId;

        private CommitDiscussionCommentAnchorSearchRequestMatcher(InternalCommitDiscussion discussion,
                                                                  String parentId, String path, Long pullRequestId) {
            this.discussion = discussion;
            this.parentId = parentId;
            this.path = path;
            this.pullRequestId = pullRequestId;
        }

        @Override
        protected boolean matchesSafely(CommitDiscussionCommentAnchorSearchRequest request) {
            return  Objects.equals(request.getDiscussion(), discussion)
                    && Objects.equals(request.getParentId().orElse(null), parentId)
                    && Objects.equals(request.getPath().orElse(null), path)
                    && Objects.equals(request.getPullRequestScopeId().orElse(null), pullRequestId);
        }
    }
}
