package com.indeed.proctor.store;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.indeed.proctor.common.model.Allocation;
import com.indeed.proctor.common.model.Range;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestDefinition;
import com.indeed.proctor.common.model.TestMatrixVersion;
import com.indeed.proctor.common.model.TestType;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.groups.Tuple;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RmCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class GitProctorTest {
    @Rule
    public TemporaryFolder testFolder = new TemporaryFolder();

    private GitProctor gitProctor;
    private Git git;
    private String initialCommitHash;

    @Before
    public void setUp() throws IOException, GitAPIException {
        git = Git.init().setDirectory(testFolder.getRoot()).call();

        // initial commit is required to initialize GitProctor
        testFolder.newFile(".gitkeep");
        git.add().addFilepattern(".gitkeep").call();
        initialCommitHash = git.commit().setMessage("initial commit").call().getId().getName();

        gitProctor = new GitProctor(testFolder.getRoot().getPath(), "", "");
    }

    @Test
    public void testGetDefinition() throws StoreException, IOException, GitAPIException {
        final String revision1 =
                addTestDefinition("proc_tst", "author", "add a new test", DEFINITION_A);
        final String revision2 =
                addTestDefinition("proc_another_tst", "author", "add a another", DEFINITION_B);
        final String revision3 =
                updateTestDefinition("proc_tst", "author", "edit a test", DEFINITION_B);
        final String revision4 =
                deleteAllTestDefinitions("delete tests");
        final String revision5 =
                addTestDefinition("proc_new_tst", "author", "add a new test", DEFINITION_A);

        assertThat(gitProctor.getTestDefinition("proc_tst", revision1))
                .isEqualTo(DEFINITION_A);
        assertThat(gitProctor.getTestDefinition("proc_tst", revision2))
                .isEqualTo(DEFINITION_A);
        assertThat(gitProctor.getTestDefinition("proc_tst", revision3))
                .isEqualTo(DEFINITION_B);
        assertThat(gitProctor.getTestDefinition("proc_tst", revision4))
                .isNull();
        assertThat(gitProctor.getTestDefinition("proc_tst", revision5))
                .isNull();

        assertThat(gitProctor.getTestDefinition("proc_another_tst", revision1))
                .isNull();
        assertThat(gitProctor.getTestDefinition("proc_another_tst", revision2))
                .isEqualTo(DEFINITION_B);
        assertThat(gitProctor.getTestDefinition("proc_another_tst", revision3))
                .isEqualTo(DEFINITION_B);
        assertThat(gitProctor.getTestDefinition("proc_another_tst", revision4))
                .isNull();
        assertThat(gitProctor.getTestDefinition("proc_another_tst", revision5))
                .isNull();

        assertThat(gitProctor.getCurrentTestDefinition("proc_tst"))
                .isNull();
        assertThat(gitProctor.getCurrentTestDefinition("proc_another_tst"))
                .isNull();
        assertThat(gitProctor.getCurrentTestDefinition("proc_new_tst"))
                .isEqualTo(DEFINITION_A);

    }

    @Test
    public void testGetMatrix() throws StoreException, IOException, GitAPIException {
        final String revision1 =
                addTestDefinition("proc_tst", "author", "add a new test", DEFINITION_A);
        final String revision2 =
                addTestDefinition("proc_another_tst", "author", "add a another", DEFINITION_B);
        final String revision3 =
                deleteAllTestDefinitions("delete tests");

        assertThat(gitProctor.getTestMatrix(revision1).getTestMatrixDefinition().getTests())
                .hasSize(1)
                .containsEntry("proc_tst", DEFINITION_A);
        assertThat(gitProctor.getTestMatrix(revision1))
                .extracting(TestMatrixVersion::getAuthor, TestMatrixVersion::getDescription, TestMatrixVersion::getVersion)
                .containsExactly("author", "add a new test", revision1);

        assertThat(gitProctor.getTestMatrix(revision2).getTestMatrixDefinition().getTests())
                .hasSize(2)
                .containsEntry("proc_tst", DEFINITION_A)
                .containsEntry("proc_another_tst", DEFINITION_B);
        assertThat(gitProctor.getTestMatrix(revision2))
                .extracting(TestMatrixVersion::getAuthor, TestMatrixVersion::getDescription, TestMatrixVersion::getVersion)
                .containsExactly("author", "add a another", revision2);

        assertThat(gitProctor.getTestMatrix(revision3).getTestMatrixDefinition().getTests())
                .isEmpty();
        assertThat(gitProctor.getCurrentTestMatrix().getTestMatrixDefinition().getTests())
                .isEmpty();
        assertThat(gitProctor.getCurrentTestMatrix())
                .extracting(TestMatrixVersion::getDescription, TestMatrixVersion::getVersion)
                .containsExactly("delete tests", revision3);
    }

    @Test
    public void testHistories() throws StoreException {
        final String revision1 =
                addTestDefinition("proc_a_tst", "author1", "add a new test a", DEFINITION_A);
        final String revision2 =
                addTestDefinition("proc_b_tst", "author2", "add a new test b", DEFINITION_B);
        final String revision3 =
                updateTestDefinition("proc_a_tst", "author3", "edit a test a", DEFINITION_B);

        assertThat(gitProctor.getLatestVersion()).isEqualTo(revision3);

        assertThat(gitProctor.getHistory("proc_a_tst", 0, 10))
                .extracting(Revision::getRevision, Revision::getAuthor, Revision::getMessage)
                .containsExactly(
                        Tuple.tuple(revision3, "author3", "edit a test a"),
                        Tuple.tuple(revision1, "author1", "add a new test a")
                );

        assertThat(gitProctor.getHistory("proc_a_tst", revision1, 0, 10))
                .extracting(Revision::getRevision, Revision::getAuthor, Revision::getMessage)
                .containsExactly(
                        Tuple.tuple(revision1, "author1", "add a new test a")
                );

        assertThat(gitProctor.getHistory("proc_b_tst", 0, 10))
                .extracting(Revision::getRevision, Revision::getAuthor, Revision::getMessage)
                .containsExactly(
                        Tuple.tuple(revision2, "author2", "add a new test b")
                );

        assertThatThrownBy(() -> gitProctor.getHistory("proc_a_tst", UNKNOWN_GIT_REVISION, 0, 10))
                .isInstanceOf(StoreException.class);

        assertThat(gitProctor.getMatrixHistory(0, 10))
                .extracting(Revision::getRevision, Revision::getMessage)
                .containsExactly(
                        Tuple.tuple(revision3, "edit a test a"),
                        Tuple.tuple(revision2, "add a new test b"),
                        Tuple.tuple(revision1, "add a new test a"),
                        Tuple.tuple(initialCommitHash, "initial commit")
                );

        assertThat(gitProctor.getMatrixHistory(1, 1))
                .extracting(Revision::getRevision, Revision::getAuthor, Revision::getMessage)
                .containsExactly(
                        Tuple.tuple(revision2, "author2", "add a new test b")
                );

        assertThat(gitProctor.getAllHistories())
                .hasSize(2)
                .hasEntrySatisfying("proc_a_tst", l ->
                        assertThat(l)
                                .extracting(Revision::getRevision)
                                .containsExactly(revision3, revision1)
                )
                .hasEntrySatisfying("proc_b_tst", l ->
                        assertThat(l)
                                .extracting(Revision::getRevision)
                                .containsExactly(revision2)
                );
    }

    @Test
    public void testRevisionDetails() throws StoreException, IOException, GitAPIException {
        final String revision1 =
                addTestDefinition("proc_a_tst", "author1", "add a new test a", DEFINITION_A);
        addTestDefinition("proc_b_tst", "author2", "add a new test b", DEFINITION_B);
        final String revision3 = deleteAllTestDefinitions("delete all");

        assertThat(gitProctor.getRevisionDetails(revision1))
                .extracting(
                        r -> r.getRevision().getRevision(),
                        r -> r.getRevision().getAuthor(),
                        RevisionDetails::getModifiedTests
                )
                .containsExactly(
                        revision1,
                        "author1",
                        Collections.singleton("proc_a_tst")
                );

        assertThat(gitProctor.getRevisionDetails(revision3))
                .extracting(
                        r -> r.getRevision().getRevision(),
                        r -> r.getRevision().getMessage(),
                        RevisionDetails::getModifiedTests
                )
                .containsExactly(
                        revision3,
                        "delete all",
                        ImmutableSet.of("proc_a_tst", "proc_b_tst")
                );

        assertThat(gitProctor.getRevisionDetails("invalidrevisionid"))
                .isNull();

        final String unknownRevision = "5f508f66bbacc1df5b9644b621763bcf26321f61";
        assertThat(gitProctor.getRevisionDetails(unknownRevision))
                .isNull();
    }

    @Test
    public void testRevisionDetailsWithRename() throws StoreException, IOException, GitAPIException {
        addTestDefinition("proc_a_tst", "author1", "add a new test a", DEFINITION_A);
        final String revision = renameTestDefinition("proc_a_tst", "proc_b_tst", "rename");

        assertThat(gitProctor.getRevisionDetails(revision).getModifiedTests())
                .containsExactlyInAnyOrder("proc_a_tst", "proc_b_tst");
    }

    @Test
    public void testAddTestDefinition() throws StoreException {
        final String testname = "proc_sample1_tst";
        final String username = "proctorwebapp";
        final String author = "me";
        final String comment = "comment";
        final TestDefinition testDefinition = createRandomTestDefinition();
        final Instant timestamp = OffsetDateTime.of(
                2019, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC
        ).toInstant();

        gitProctor.addTestDefinition(
                ChangeMetadata.builder()
                        .setUsername(username)
                        .setPassword("")
                        .setAuthor(author)
                        .setComment(comment)
                        .setTimestamp(timestamp)
                        .build(),
                testname,
                testDefinition,
                Collections.emptyMap()
        );

        final Revision latestRevision = gitProctor.getMatrixHistory(0, 1).get(0);

        assertThat(gitProctor.getHistory(testname, 0, 2)).hasSize(1)
                .first()
                .isEqualTo(
                        new Revision(
                                latestRevision.getRevision(),
                                author,
                                new Date(timestamp.toEpochMilli()),
                                comment
                        )
                );
    }

    private String addTestDefinition(
            final String testName,
            final String author,
            final String message,
            final TestDefinition definition
    ) throws StoreException {
        gitProctor.addTestDefinition(
                "",
                "",
                author,
                testName,
                definition,
                Collections.emptyMap(),
                message
        );
        return gitProctor.getHistory(testName, 0, 1).get(0)
                .getRevision();
    }

    private String updateTestDefinition(
            final String testName,
            final String author,
            final String message,
            final TestDefinition definition
    ) throws StoreException {
        gitProctor.updateTestDefinition(
                "",
                "",
                author,
                "",
                testName,
                definition,
                Collections.emptyMap(),
                message
        );
        return gitProctor.getHistory(testName, 0, 1).get(0)
                .getRevision();
    }

    private String deleteAllTestDefinitions(
            final String message
    ) throws IOException, GitAPIException, StoreException {
        resetStageAndWorkingTreeToHEAD();
        FileUtils.deleteDirectory(getPathToDefinitionDirectory().toFile());
        final String revision = commitAllWorkingTreeChanges(message);
        gitProctor.refresh();
        return revision;
    }

    private String renameTestDefinition(
            final String srcTestName,
            final String dstTestName,
            final String message
    ) throws IOException, GitAPIException, StoreException {
        resetStageAndWorkingTreeToHEAD();
        final File srcDir = getPathToDefinitionDirectory().resolve(srcTestName).toFile();
        final File dstDir = getPathToDefinitionDirectory().resolve(dstTestName).toFile();
        FileUtils.moveDirectory(srcDir, dstDir);
        final String revision = commitAllWorkingTreeChanges(message);
        gitProctor.refresh();
        return revision;
    }

    private Path getPathToDefinitionDirectory() {
        return testFolder.getRoot()
                .toPath()
                .resolve(FileBasedProctorStore.DEFAULT_TEST_DEFINITIONS_DIRECTORY);
    }

    /**
     * Reset stage and working tree to HEAD.
     * <p>
     * This is equivalent to
     * $ git reset HEAD
     * $ git checkout -- .
     */
    private void resetStageAndWorkingTreeToHEAD() throws GitAPIException {
        git.reset().setRef(Constants.HEAD).call();
        git.checkout().setAllPaths(true).call();
    }

    /**
     * commit all changes in working tree.
     * <p>
     * This is equivalent to the following in git (written in C)
     * $ git add .
     * $ git commit -m $message
     */
    private String commitAllWorkingTreeChanges(final String message) throws GitAPIException {
        final RmCommand rmCommand = git.rm();
        git.status().call().getMissing().forEach(rmCommand::addFilepattern);
        rmCommand.call();
        git.add().addFilepattern(".").call();
        return git.commit().setMessage(message).call().getId().getName();
    }

    private static final String UNKNOWN_GIT_REVISION = StringUtils.repeat('0', 40);

    private static final TestDefinition DEFINITION_A = createRandomTestDefinition();
    private static final TestDefinition DEFINITION_B = createRandomTestDefinition();

    private static TestDefinition createRandomTestDefinition() {
        return new TestDefinition(
                "-1",
                null,
                TestType.ANONYMOUS_USER,
                "&" + RandomStringUtils.randomAlphabetic(8).toLowerCase(Locale.ENGLISH),
                ImmutableList.of(
                        new TestBucket("inactive", -1, "")
                ),
                ImmutableList.of(
                        new Allocation(
                                null,
                                ImmutableList.of(
                                        new Range(-1, 1.0)
                                ),
                                "#A1"
                        )
                ),
                false,
                Collections.emptyMap(),
                Collections.emptyMap(),
                RandomStringUtils.randomAlphabetic(8),
                ImmutableList.of(RandomStringUtils.randomAlphabetic(8).toLowerCase())
        );
    }

}