package it.com.atlassian.bitbucket.scm.cache;

import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.scm.cache.ScmRequestProtocol;
import com.atlassian.bitbucket.scm.cache.ScmRequestType;
import com.atlassian.stash.hosting.ScmTransport;
import com.atlassian.bitbucket.test.DefaultFuncTestData;
import com.atlassian.bitbucket.test.ProcessFailedException;
import com.atlassian.bitbucket.test.ProcessTestHelper;
import com.atlassian.bitbucket.test.TestContext;
import com.atlassian.util.contentcache.CacheEntryStatistics;
import com.atlassian.util.contentcache.ContentCacheStatistics;
import org.apache.commons.io.FileUtils;
import org.junit.*;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

import static org.junit.Assert.*;

/**
 * Func test for SCM Cache correctness
 */
public abstract class ScmCacheTestBase {

    static final String TEST_PROJECT = DefaultFuncTestData.getProject1();
    static final String TEST_REPO = DefaultFuncTestData.getProject1Repository1();
    static final String TEST_USER = "scm-cache-tester";
    static final String TEST_USER_PASSWORD = "scm-cache-tester";

    protected static ScmTransport transport;
    protected static TestContext testContext;

    @Rule
    public TemporaryFolder tempDirectory = new TemporaryFolder();
    protected File repoDir;

    @BeforeClass
    public static void setUpOnce() throws Exception {
        testContext = new TestContext();
        testContext
                .user(TEST_USER, TEST_USER_PASSWORD)
                .projectPermission(TEST_PROJECT, TEST_USER, Permission.PROJECT_WRITE);
    }

    @AfterClass
    public static void tearDownOnce() throws Throwable {
        try {
            testContext.cleanup();
        } finally {
            testContext = null;
        }
    }

    @Before
    public void setUp() throws Exception {
        ScmCacheTestSupport.clearCache();
        ScmCacheTestSupport.enableCaching();

        repoDir = new File(tempDirectory.getRoot(), "repo");
    }

    @After
    public void tearDown() throws Throwable {
        ScmCacheTestSupport.disableCaching();
    }

    @Test
    public void testCloneEmptyCache() throws Exception {
        cloneRepository();
        assertCacheHits(0, -1, 0);

        // clone again - should result in cache hits
        cloneRepository();
        assertCacheHits(1, -1, 1);
        assertRepositoryValid(false);
    }

    @Test
    public void testCloneNoCaching() throws Exception {
        ScmCacheTestSupport.disableCaching();
        cloneRepository();
        assertCacheHits(-1, -1, -1);
        assertRepositoryValid(false);
    }

    @Test
    public void testCloneNoRefCaching() throws Exception {
        ScmCacheTestSupport.disableCaching(ScmRequestType.REFS);

        // fill the cache
        cloneRepository();
        assertCacheHits(-1, -1, 0);

        // clone again - should result in cache hits
        cloneRepository();
        assertCacheHits(-1, -1, 1);
        assertRepositoryValid(false);
    }

    @Test
    public void testCloneNoPackCaching() throws Exception {
        ScmCacheTestSupport.disableCaching(ScmRequestType.UPLOAD_PACK);

        // fill the cache
        cloneRepository();
        assertCacheHits(0, -1, -1);

        // clone again - should result in cache hits
        cloneRepository();
        assertCacheHits(1, -1, -1);
        assertRepositoryValid(false);
    }

    @Test
    public void testClonePackCached() throws Exception {
        // first fill the cache
        cloneRepository();

        // verify that only the pack is cached
        clearCacheEntries("refs");
        assertCacheHits(-1, -1, 0);

        cloneRepository();
        assertCacheHits(0, -1, 1);
        assertRepositoryValid(false);
    }

    @Test
    public void testCloneRefsCached() throws Throwable {
        // first fill the cache with refs only
        cloneRepository();
        clearCacheEntries("clone");
        assertCacheHits(0, -1, -1);

        cloneRepository();
        assertCacheHits(1, -1, 0);
        assertRepositoryValid(false);
    }

    @Test
    public void testFetchBypassesCache() throws Exception {
        // initialize a repo that has part of the refs
        ProcessTestHelper.execute(tempDirectory.getRoot(), "git", "init", repoDir.getAbsolutePath());
        transport.fetchRefSpec(repoDir, TEST_PROJECT, TEST_REPO, "refs/heads/basic_branching:refs/heads/a");
        assertCacheHits(0, -1, 0);

        // fetch another branch - should hit the refs-cache, bypass the pack-cache
        transport.fetchRefSpec(repoDir, TEST_PROJECT, TEST_REPO, "refs/heads/master:refs/heads/b");
        assertCacheHits(1, -1, 0);
    }

    @Test
    public void testFetchBypassesCacheNoRefsCache() throws Exception {
        ScmCacheTestSupport.disableCaching(ScmRequestType.REFS);

        // initialize a repo that has part of the refs
        ProcessTestHelper.execute(tempDirectory.getRoot(), "git", "init", repoDir.getAbsolutePath());
        transport.fetchRefSpec(repoDir, TEST_PROJECT, TEST_REPO, "refs/heads/basic_branching:refs/heads/a");
        assertCacheHits(-1, -1, 0);

        // fetch another branch - should bypass the pack-cache
        transport.fetchRefSpec(repoDir, TEST_PROJECT, TEST_REPO, "refs/heads/master:refs/heads/b");
        assertCacheHits(-1, -1, 0);
    }

    @Test
    public void testProtocolCacheDisabled() throws Exception {
        ScmCacheTestSupport.disableCaching(getProtocol());

        shallowCloneRepository();
        assertCacheHits(-1, -1, -1);

        // clone again - there should be no cache hits
        shallowCloneRepository();
        assertCacheHits(-1, -1, -1);
    }

    @Test
    public void testPushInvalidatesCache() throws Throwable {
        String repoName = "push-test-repo";
        TestContext context = new TestContext();
        try {
            // set up a new repo; we're going to muck with the repository
            context.repository(TEST_PROJECT, repoName)
                    .repositoryPermission(TEST_PROJECT, repoName, TEST_USER, Permission.REPO_WRITE);

            // clone the default repo
            cloneRepository();

            // and push it to the new repo
            transport.pushAllRefs(repoDir, TEST_PROJECT, repoName);

            // clone twice from the new repository
            maybeDeleteRepositoryDirectory();
            transport.clone(repoDir, TEST_PROJECT, repoName);
            maybeDeleteRepositoryDirectory();
            transport.clone(repoDir, TEST_PROJECT, repoName);

            assertCacheHits(repoName, 1, -1, 1);

            ProcessTestHelper.execute(repoDir, "git", "checkout", "-b", "branch-" + UUID.randomUUID());
            transport.pushAllRefs(repoDir, TEST_PROJECT, TEST_REPO);

            assertCacheHits(-1, -1, -1);
        } finally {
            context.cleanup();
        }
    }

    @Test
    public void testShallowCloneEmptyCache() throws Exception {
        shallowCloneRepository();
        assertCacheHits(0, 0, 0);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(1, 1, 1);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneRefsCached() throws Exception {
        // fill the cache
        shallowCloneRepository();
        clearCacheEntries("negotiation", "clone");
        assertCacheHits(0, -1, -1);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(1, 0, 0);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneNegotiationCached() throws Exception {
        // fill the cache
        shallowCloneRepository();

        clearCacheEntries("refs", "clone");
        assertCacheHits(-1, 0, -1);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(0, 1, 0);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneNegotiationAndPackCached() throws Exception {
        // fill the cache
        shallowCloneRepository();

        clearCacheEntries("refs");
        assertCacheHits(-1, 0, 0);

        // clone again
        shallowCloneRepository();
        assertCacheHits(0, 1, 1);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneNoRefCaching() throws Exception {
        ScmCacheTestSupport.disableCaching(ScmRequestType.REFS);

        // fill the cache
        shallowCloneRepository();
        assertCacheHits(-1, 0, 0);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(-1, 1, 1);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneNoPackCaching() throws Exception {
        ScmCacheTestSupport.disableCaching(ScmRequestType.UPLOAD_PACK);

        // fill the cache
        shallowCloneRepository();
        assertCacheHits(0, -1, -1);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(1, -1, -1);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowClonePackCached() throws Exception {
        // fill the cache
        shallowCloneRepository();

        clearCacheEntries("refs", "negotiation");
        assertCacheHits(-1, -1, 0);

        // clone again
        shallowCloneRepository();
        assertCacheHits(0, 0, 1);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneRefsAndNegotiationCached() throws Exception {
        //  fill the cache
        shallowCloneRepository();

        clearCacheEntries("clone");
        assertCacheHits(0, 0, -1);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(1, 1, 0);
        assertRepositoryValid(true);
    }

    @Test
    public void testShallowCloneRefsAndPackCached() throws Exception {
        // fill the cache
        shallowCloneRepository();

        clearCacheEntries("negotiation");
        assertCacheHits(0, -1, 0);

        // clone again - should result in cache hits
        shallowCloneRepository();
        assertCacheHits(1, 0, 1);
        assertRepositoryValid(true);
    }

    protected abstract ScmRequestProtocol getProtocol();

    private void assertContainsEntry(ContentCacheStatistics stats, String keySubstring, int hits) {
        for (CacheEntryStatistics entryStatus : stats.getEntries()) {
            if (entryStatus.getKey().contains(keySubstring)) {
                assertEquals(hits, entryStatus.getHits());
                return;
            }
        }

        fail("No CacheEntry found matching key '" + keySubstring + "'");
    }

    /**
     * @param expectedRefHits -1 means no 'refs' entry is expected
     * @param expectedPackHits -1 means no 'clone' entry is expected
     */
    private void assertCacheHits(int expectedRefHits, int expectedShallowInfoHits, int expectedPackHits) {
        assertCacheHits(TEST_REPO, expectedRefHits, expectedShallowInfoHits, expectedPackHits);
    }

    /**
     * @param expectedRefHits -1 means no 'refs' entry is expected
     * @param expectedPackHits -1 means no 'clone' entry is expected
     */
    private void assertCacheHits(String repoSlug, int expectedRefHits, int expectedShallowInfoHits, int expectedPackHits) {
        ContentCacheStatistics stats = ScmCacheTestSupport.getStatistics(TEST_PROJECT, repoSlug);
        assertNotNull(stats);
        int expectedSize = (expectedRefHits >= 0 ? 1 : 0) + (expectedShallowInfoHits >= 0 ? 1 : 0) + (expectedPackHits >= 0 ? 1 : 0);
        assertEquals(expectedSize, stats.getEntries().size());

        if (expectedRefHits >= 0) {
            assertContainsEntry(stats, "refs", expectedRefHits);
        }
        if (expectedShallowInfoHits >= 0) {
            assertContainsEntry(stats, "negotiation", expectedShallowInfoHits);
        }
        if (expectedPackHits >= 0) {
            assertContainsEntry(stats, "clone", expectedPackHits);
        }
    }

    private void assertRepositoryValid(boolean shallow) throws Exception {
        // verify that the clone succeeded
        assertTrue(repoDir.exists());
        assertTrue(new File(repoDir, ".git").exists());

        // 0a943a29376f2336b78312d99e65da17048951db - tip commit; should be present in both a shallow and full clone
        assertEquals("Commit 0a943a29376f2336b78312d99e65da17048951db not found", 0,
                ProcessTestHelper.execute(repoDir, "git", "show", "0a943a29376f2336b78312d99e65da17048951db")
                    .getExitCode());

        // 409a2180fd95b744df8d817e770b7f8c853f78dc - initial commit; should not be available in a shallow clone
        if (shallow) {
            try {
                int exitCode = ProcessTestHelper.execute(repoDir, "git", "show", "409a2180fd95b744df8d817e770b7f8c853f78dc")
                        .getExitCode();

                fail("Commit 409a2180fd95b744df8d817e770b7f8c853f78dc should not be present in a shallow clone. Exit code " + exitCode);
            } catch (ProcessFailedException e) {
                // expected
            }
        } else {
            assertEquals("Commit 409a2180fd95b744df8d817e770b7f8c853f78dc should be present in a full clone", 0,
                    ProcessTestHelper.execute(repoDir, "git", "show", "409a2180fd95b744df8d817e770b7f8c853f78dc")
                        .getExitCode());
        }
    }

    private void clearCacheEntries(String... keyPatterns) {
        ContentCacheStatistics stats = ScmCacheTestSupport.getStatistics(TEST_PROJECT, TEST_REPO);
        for (String keyPattern : keyPatterns) {
            ScmCacheTestSupport.clearCacheEntry(TEST_PROJECT, TEST_REPO, getCacheKey(stats, keyPattern));
        }
    }

    private void cloneRepository() throws IOException {
        maybeDeleteRepositoryDirectory();
        transport.clone(repoDir, TEST_PROJECT, TEST_REPO);
    }

    private void maybeDeleteRepositoryDirectory() throws IOException {
        if (repoDir.exists()) {
            FileUtils.deleteDirectory(repoDir);
        }
    }

    private String getCacheKey(ContentCacheStatistics stats, String keySubstring) {
        for (CacheEntryStatistics entryStats : stats.getEntries()) {
            if (entryStats.getKey().contains(keySubstring)) {
                return entryStats.getKey();
            }
        }
        return null;
    }

    private void shallowCloneRepository() throws IOException {
        maybeDeleteRepositoryDirectory();
        transport.clone(repoDir, TEST_PROJECT, TEST_REPO, "--depth", "1");
    }

}
