package com.atlassian.stash.internal.pull.comment.drift;

import com.atlassian.bitbucket.scm.annotation.Repo;
import com.atlassian.bitbucket.content.DiffFileType;
import com.atlassian.bitbucket.content.DiffSegmentType;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestDiffCommentAnchor;
import com.atlassian.stash.internal.pull.InternalPullRequestRef;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.scm.git.AbstractGitTest;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.pull.PullRequestEffectiveDiff;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;

import static com.atlassian.bitbucket.util.Chainable.chain;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.filter;
import static org.mockito.Mockito.when;
import static org.mockito.internal.util.Checks.checkNotNull;

public abstract class AbstractCommentDriftStrategyTest<T extends CommentDriftStrategy> extends AbstractGitTest {

    /**
     * Prefix for the {@link com.atlassian.stash.internal.pull.InternalPullRequest#getFromRef() from ref}.
     */
    protected static final String QUALIFIER_FROM = "from_";
    /**
     * Prefix for the merge ref, which is {@link PullRequestEffectiveDiff#getUntilId() effective diff "untilId"}.
     */
    protected static final String QUALIFIER_MERGE = "merge_";
    /**
     * Prefix for the {@link com.atlassian.stash.internal.pull.InternalPullRequest#getToRef() to ref}, which is also
     * the {@link PullRequestEffectiveDiff#getSinceId() effective diff "sinceId"}
     */
    protected static final String QUALIFIER_TO = "to_";
    protected static final String REPOSITORY = "classpath:git/comment-drift.zip";

    @Mock
    protected InternalPullRequestRef fromRef;
    @Mock
    protected InternalPullRequest pullRequest;
    @Repo(REPOSITORY)
    protected InternalRepository repository;
    @Autowired
    protected ScmService scmService;
    protected T strategy;
    @Mock
    protected InternalPullRequestRef toRef;

    @Autowired
    private GitAgent agent;
    private long nextAnchorId;

    @Before
    public void setup() {
        when(pullRequest.getFromRef()).thenReturn(fromRef);
        when(pullRequest.getGlobalId()).thenReturn(1L);
        when(pullRequest.getToRef()).thenReturn(toRef);
    }

    protected InternalPullRequestDiffCommentAnchor createAnchor(String toHash, String fromHash, String path,
                                                                int line, DiffSegmentType lineType) {
        return createAnchor(toHash, fromHash, path, null, line, lineType);
    }

    protected InternalPullRequestDiffCommentAnchor createAnchor(String toHash, String fromHash, String path,
                                                                String srcPath, int line, DiffSegmentType lineType) {
        return new InternalPullRequestDiffCommentAnchor.Builder(pullRequest)
                .fromHash(fromHash)
                .id(nextAnchorId++)
                .line(line, lineType, DiffFileType.forSegmentType(lineType))
                .path(path)
                .srcPath(srcPath)
                .toHash(toHash)
                .build();
    }

    @Nonnull
    protected InternalPullRequestDiffCommentAnchor createFileAnchor(@Nonnull String toHash, @Nonnull String fromHash,
                                                                    @Nonnull String path) {
        return createFileAnchor(toHash, fromHash, path, null);
    }

    @Nonnull
    protected InternalPullRequestDiffCommentAnchor createFileAnchor(@Nonnull String toHash, @Nonnull String fromHash,
                                                                    @Nonnull String path, @Nullable String srcPath) {
        return new InternalPullRequestDiffCommentAnchor.Builder(pullRequest)
                .fromHash(fromHash)
                .id(nextAnchorId++)
                .path(path)
                .srcPath(srcPath)
                .toHash(toHash)
                .build();
    }

    /**
     * Asserts that the provided {@code initial} anchor was "drifted", meaning its hashes were updated and its line
     * number was shifted by the specified {@code offset}. The specified {@code offset} should never be {@code 0};
     * if the line number should not have drifted, use {@link #assertRetained(InternalPullRequestDiffCommentAnchor,
     * PullRequestEffectiveDiff, java.util.List) assertRetained} instead.
     *
     * @param initial       the initial state of the anchor
     * @param offset        the offset the anchor's line number is expected to shift by; may be negative but should
     *                      never be {@code 0}
     * @param effectiveDiff the effective diff whose hashes should have been applied to the anchor
     * @param updates       the processed/updated anchors
     * @throws IllegalArgumentException if the specified {@code offset} is {@code 0}
     */
    protected static void assertDrifted(InternalPullRequestDiffCommentAnchor initial, int offset,
                                        PullRequestEffectiveDiff effectiveDiff,
                                        List<InternalPullRequestDiffCommentAnchor> updates) {
        InternalPullRequestDiffCommentAnchor update = find(initial, updates);
        assertFalse("Anchor [" + initial.getId() + "] was orphaned", update.isOrphaned());
        assertEquals(effectiveDiff.getSinceId(), update.getFromHash());
        assertEquals(effectiveDiff.getUntilId(), update.getToHash());
        assertEquals(initial.getLine() + offset, update.getLine());
        assertSame(initial.getLineType(), update.getLineType());
    }

    /**
     * Assert that none of the anchors in {@code initialAnchors} has been processed, i.e. it cannot be present in
     * the list of {@code updates}.
     *
     * @param initialAnchors the list of initial anchors that are not supposed to be processed
     * @param updates       the processed/updated anchors
     */
    protected static void assertNotProcessed(@Nonnull Iterable<InternalPullRequestDiffCommentAnchor> initialAnchors,
                                             @Nonnull List<InternalPullRequestDiffCommentAnchor> updates) {
        for (InternalPullRequestDiffCommentAnchor anchor : initialAnchors) {
            assertFalse(String.format("The updated anchors %s contained anchor with ID %d", updates, anchor.getId()),
                    any(updates, matchesId(anchor)::test));
        }
    }

    /**
     * Asserts that the provided {@code initial} anchor was "orphaned", meaning its hashes <i>were not updated</i>
     * and that its {@code orphaned} flag was set to {@code true}. As an additional sanity test, this method also
     * asserts that the {@code initial} anchor's {@code orphaned} flag was {@code false}.
     *
     * @param initial the initial state of the anchor
     * @param updates the processed/updated anchors
     */
    protected static void assertOrphaned(InternalPullRequestDiffCommentAnchor initial,
                                         List<InternalPullRequestDiffCommentAnchor> updates) {
        assertFalse("Invalid test data: Anchor [" + initial.getId() + "] was orphaned before being processed",
                initial.isOrphaned());

        InternalPullRequestDiffCommentAnchor update = find(initial, updates);
        assertTrue("Anchor [" + initial.getId() + "] was not orphaned", update.isOrphaned());
        assertEquals(initial.getFromHash(), update.getFromHash());
        assertEquals(initial.getLine(), update.getLine());
        assertSame(initial.getLineType(), update.getLineType());
        assertEquals(initial.getToHash(), update.getToHash());
    }

    /**
     * Asserts that the provided {@code initial} anchor was "retained", meaning its hashes were updated without any
     * change to its line number.
     *
     * @param initial       the initial state of the anchor
     * @param effectiveDiff the effective diff whose hashes should have been applied to the anchor
     * @param updates       the processed/updated anchors
     */
    protected static void assertRetained(InternalPullRequestDiffCommentAnchor initial,
                                         PullRequestEffectiveDiff effectiveDiff,
                                         List<InternalPullRequestDiffCommentAnchor> updates) {
        InternalPullRequestDiffCommentAnchor update = find(initial, updates);
        assertFalse("Anchor [" + initial.getId() + "] was orphaned", update.isOrphaned());
        assertEquals(effectiveDiff.getSinceId(), update.getFromHash());
        assertEquals(effectiveDiff.getUntilId(), update.getToHash());
        assertEquals(initial.getLine(), update.getLine());
        assertSame(initial.getLineType(), update.getLineType());
    }

    /**
     * Finds the updated version of the provided {@code anchor} in the {@code updates} list. Anchors are matched by
     * their ID, which is assumed to have been set during {@link #createAnchor(String, String, String, String, int,
     * DiffSegmentType) creation}.
     *
     * @param anchor  the anchor to find in the {@code updates} list
     * @param updates the processed/updated anchors
     * @return the updated anchor for the provided initial anchor
     * @throws IllegalStateException if no updated anchor is found with the same ID as the provided {@code anchor}
     */
    protected static InternalPullRequestDiffCommentAnchor find(InternalPullRequestDiffCommentAnchor anchor,
                                                               List<InternalPullRequestDiffCommentAnchor> updates) {
        for (InternalPullRequestDiffCommentAnchor update : updates) {
            if (anchor.getId() == update.getId()) {
                return update;
            }
        }
        throw new IllegalStateException("Anchor [" + anchor.getId() + "] was not found in the updates list");
    }

    /**
     * Finds all anchors in the {@code anchors} list that match the provided path.
     *
     * @param path path to find anchors by
     * @param anchors the list of anchors anchors
     * @return list of anchors matching the path, may be empty
     */
    @Nonnull
    protected static List<InternalPullRequestDiffCommentAnchor> findByPath(@Nonnull final String path,
                                                                           @Nonnull List<InternalPullRequestDiffCommentAnchor> anchors) {
        checkNotNull(path, "path");
        checkNotNull(anchors, "updates");

        return ImmutableList.copyOf(filter(anchors, anchor -> anchor.getPath().equals(path)));
    }

    @Nonnull
    protected static Predicate<InternalPullRequestDiffCommentAnchor> matchesId(@Nonnull InternalPullRequestDiffCommentAnchor anchor,
                                                                               @Nonnull InternalPullRequestDiffCommentAnchor... moreAnchors) {
        checkNotNull(anchor, "anchor");
        checkNotNull(moreAnchors, "moreAnchors");
        final Iterable<InternalPullRequestDiffCommentAnchor> allAnchors = ImmutableList.<InternalPullRequestDiffCommentAnchor>builder()
                .add(anchor)
                .addAll(ImmutableList.copyOf(moreAnchors))
                .build();
        return matchesId(allAnchors);
    }

    @Nonnull
    protected static Predicate<InternalPullRequestDiffCommentAnchor> matchesId(@Nonnull final Iterable<InternalPullRequestDiffCommentAnchor> anchors) {
        checkNotNull(anchors, "anchor");
        return other -> {
            for (InternalPullRequestDiffCommentAnchor anchor : anchors) {
                if (anchor.getId() == other.getId()) {
                    return true;
                }
            }
            return false;
        };
    }

    protected String resolve(String branchName) {
        Branch branch = agent.resolveBranch(repository, agent.qualifyBranch(branchName), true);
        assertNotNull("Required branch [" + branchName + "] could not be resolved", branch);

        return branch.getLatestCommit();
    }

    protected void runTest(String prefix, String currentSuffix, String previousSuffix, StrategyTest test) {
        String currentFrom = resolve(prefix + QUALIFIER_FROM + currentSuffix);
        String currentMerge = resolve(prefix + QUALIFIER_MERGE + currentSuffix);
        String currentTo = resolve(prefix + QUALIFIER_TO + currentSuffix);

        String previousFrom = resolve(prefix + QUALIFIER_FROM + previousSuffix);
        String previousMerge = resolve(prefix + QUALIFIER_MERGE + previousSuffix);
        String previousTo = resolve(prefix + QUALIFIER_TO + previousSuffix);

        when(fromRef.getLatestCommit()).thenReturn(currentFrom);
        when(toRef.getLatestCommit()).thenReturn(currentTo);

        //untilId -> merge_$(currentSuffix)
        //sinceId -> to_$(currentSuffix)
        PullRequestEffectiveDiff currentDiff = new PullRequestEffectiveDiff(currentMerge, currentTo);

        //toHash -> merge_$(previousSuffix)
        //fromHash -> to_$(previousSuffix)
        Iterable<InternalPullRequestDiffCommentAnchor> anchors = test.createAnchors(previousMerge, previousTo);
        InternalPullRequestDiffCommentAnchor first = Iterables.getFirst(anchors, null);

        @SuppressWarnings("ConstantConditions")
        DriftContext context = new DriftContext(repository, pullRequest, previousFrom, previousTo, currentDiff,
                new PullRequestEffectiveDiff(first.getToHash(), first.getFromHash()), Lists.newArrayList(anchors));
        strategy.apply(context);

        test.validateContext(currentDiff, context);
    }

    protected abstract static class AnchorTest implements StrategyTest {

        @Override
        public void validateContext(PullRequestEffectiveDiff currentDiff, DriftContext context) {
            //Calling done() will throw if some anchors have not been processed, whether that processing is to drift,
            //retain or orphan. AnchorTest.validateAnchors is expected to ascertain that the correct processing has
            //been applied; all the done() call verifies is that _some_ processing has been applied
            validateAnchors(currentDiff, context.done());
        }

        protected abstract void validateAnchors(PullRequestEffectiveDiff currentDiff,
                                                List<InternalPullRequestDiffCommentAnchor> updates);
    }

    /**
     * A {@link StrategyTest} that assumes that some anchors have not been processed by the tested strategy. The
     * unprocessed anchors are collected from context and then orphaned, so {@link DriftContext#done()} can be called
     * and retrieve all updates. As a result, extending tests may implement
     * {@link #validateNotProcessedAnchors(java.util.List)} and
     * {@link #validateProcessedAnchors(PullRequestEffectiveDiff, java.util.List)} to validate anchors that have been
     * processed by the tested strategy, as well as those that should have been left untouched.
     */
    protected abstract static class PartialAnchorTest implements StrategyTest {

        @Override
        public void validateContext(PullRequestEffectiveDiff currentDiff, DriftContext context) {

            ImmutableList.Builder<InternalPullRequestDiffCommentAnchor> unprocessedBuilder = ImmutableList.builder();
            for (Iterator<InternalPullRequestDiffCommentAnchor> unprocessedIterator = context.iterator(); unprocessedIterator.hasNext();) {
                InternalPullRequestDiffCommentAnchor next = unprocessedIterator.next();
                unprocessedBuilder.add(next);
                context.orphan(next);
                unprocessedIterator.remove();
            }
            List<InternalPullRequestDiffCommentAnchor> unprocessed = unprocessedBuilder.build();
            validateProcessedAnchors(currentDiff, chain(context.done()).filter(matchesId(unprocessed).negate()).toList());
            validateNotProcessedAnchors(unprocessed);
        }

        protected abstract void validateProcessedAnchors(PullRequestEffectiveDiff currentDiff,
                                                         List<InternalPullRequestDiffCommentAnchor> updates);

        protected abstract void validateNotProcessedAnchors(List<InternalPullRequestDiffCommentAnchor> unprocessed);
    }

    protected interface StrategyTest {

        Iterable<InternalPullRequestDiffCommentAnchor> createAnchors(String untilId, String sinceId);

        void validateContext(PullRequestEffectiveDiff currentDiff, DriftContext context);
    }
}
