package org.jenkinsci.plugins.gitclient;

import static org.junit.jupiter.api.Assertions.*;

import hudson.EnvVars;
import hudson.Util;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.lib.Repository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
 * Git API Test which are solely for CLI git,
 * but doesn't need an initialized working repo.
 * These tests are not implemented for JGit.
 */
@ParameterizedClass(name = "{0}")
@MethodSource("gitObjects")
class GitAPICliGitNotInitializedTest {

    @RegisterExtension
    private final GitClientSampleRepoRule repo = new GitClientSampleRepoRule();

    private int logCount = 0;
    private static final String LOGGING_STARTED = "Logging started";
    private LogHandler handler = null;
    private TaskListener listener;

    @Parameter(0)
    private String gitImplName;

    private WorkspaceWithRepo workspace;

    private GitClient testGitClient;
    private File testGitDir;

    static List<Arguments> gitObjects() {
        List<Arguments> arguments = new ArrayList<>();
        String[] gitImplNames = {"git"};
        for (String gitImplName : gitImplNames) {
            Arguments item = Arguments.of(gitImplName);
            arguments.add(item);
        }
        return arguments;
    }

    @BeforeAll
    static void loadLocalMirror() throws Exception {
        /* Prime the local mirror cache before other tests run */
        /* Allow 2-6 second delay before priming the cache */
        /* Allow other tests a better chance to prime the cache */
        /* 2-6 second delay is small compared to execution time of this test */
        Random random = new Random();
        Thread.sleep(2000L + random.nextInt(4000)); // Wait 2-6 seconds before priming the cache
        TaskListener mirrorListener = StreamTaskListener.fromStdout();
        File tempDir =
                Files.createTempDirectory("PrimeGitAPITestCliGitNotInitialized").toFile();
        WorkspaceWithRepo cache = new WorkspaceWithRepo(tempDir, "git", mirrorListener);
        cache.localMirror();
        Util.deleteRecursive(tempDir);
    }

    @BeforeEach
    void setUpRepositories() throws Exception {
        Logger logger = Logger.getLogger(this.getClass().getPackage().getName() + "-" + logCount++);
        handler = new LogHandler();
        handler.setLevel(Level.ALL);
        logger.setUseParentHandlers(false);
        logger.addHandler(handler);
        logger.setLevel(Level.ALL);
        listener = new hudson.util.LogTaskListener(logger, Level.ALL);
        listener.getLogger().println(LOGGING_STARTED);

        workspace = new WorkspaceWithRepo(repo.getRoot(), gitImplName, listener);

        testGitClient = workspace.getGitClient();
        testGitDir = workspace.getGitFileDir();
    }

    @AfterEach
    void afterTearDown() {
        try {
            String messages = StringUtils.join(handler.getMessages(), ";");
            assertTrue(handler.containsMessageSubstring(LOGGING_STARTED), "Logging not started: " + messages);
        } finally {
            handler.close();
        }
    }

    /* Submodule checkout in JGit does not support renamed submodules.
     * The test branch intentionally includes a renamed submodule, so this test
     * is not run with JGit.
     */
    @Test
    void testSubmoduleCheckoutSimple() throws Exception {
        workspace.cloneRepo(workspace, workspace.localMirror());
        assertSubmoduleDirs(testGitDir, false, false);

        /* Checkout a branch which includes submodules (in modules directory) */
        String subBranch = "tests/getSubmodules";
        String subRefName = "origin/" + subBranch;
        testGitClient.checkout().ref(subRefName).branch(subBranch).execute();
        assertSubmoduleDirs(testGitDir, true, false);

        testGitClient.submoduleUpdate().recursive(true).execute();
        assertSubmoduleDirs(testGitDir, true, true);
        assertSubmoduleContents(testGitDir);
        assertSubmoduleRepository(new File(testGitDir, "modules/ntp"));
        assertSubmoduleRepository(new File(testGitDir, "modules/firewall"));
        assertSubmoduleRepository(new File(testGitDir, "modules/sshkeys"));
    }

    private void assertSubmoduleRepository(File submoduleDir) throws Exception {
        /* Get a client directly on the submoduleDir */
        GitClient submoduleClient = setupGitAPI(submoduleDir);

        /* Assert that when we invoke the repository callback it gets a
         * functioning repository object
         */
        submoduleClient.withRepository((final Repository repo, VirtualChannel channel) -> {
            assertTrue(repo.getObjectDatabase().exists(), repo.getDirectory() + " is not a valid repository");
            return null;
        });
    }

    protected GitClient setupGitAPI(File ws) throws Exception {
        return Git.with(listener, new EnvVars()).in(ws).using(gitImplName).getClient();
    }

    private void assertSubmoduleContents(File repo) throws Exception {
        final File modulesDir = new File(repo, "modules");

        final File sshkeysDir = new File(modulesDir, "sshkeys");
        final File sshkeysModuleFile = new File(sshkeysDir, "Modulefile");
        assertFileExists(sshkeysModuleFile);

        final File keeperFile = new File(modulesDir, "keeper");
        final String keeperContent = "";
        assertFileExists(keeperFile);
        assertFileContents(keeperFile, keeperContent);

        final File ntpDir = new File(modulesDir, "ntp");
        final File ntpContributingFile = new File(ntpDir, "CONTRIBUTING.md");
        final String ntpContributingContent = "Puppet Labs modules on the Puppet Forge are open projects";
        assertFileExists(ntpContributingFile);
        assertFileContains(ntpContributingFile, ntpContributingContent); /* Check substring in file */
    }

    private void assertFileContains(File file, String expectedContent) throws Exception {
        assertFileExists(file);
        final String fileContent = Files.readString(file.toPath(), StandardCharsets.UTF_8);
        final String message = file + " does not contain '" + expectedContent + "', contains '" + fileContent + "'";
        assertTrue(fileContent.contains(expectedContent), message);
    }

    private void assertFileContents(File file, String expectedContent) throws Exception {
        assertFileExists(file);
        final String fileContent = Files.readString(file.toPath(), StandardCharsets.UTF_8);
        assertEquals(expectedContent, fileContent, file + " wrong content");
    }

    private void assertSubmoduleDirs(File repo, boolean dirsShouldExist, boolean filesShouldExist) {
        final File modulesDir = new File(repo, "modules");
        final File ntpDir = new File(modulesDir, "ntp");
        final File firewallDir = new File(modulesDir, "firewall");
        final File keeperFile = new File(modulesDir, "keeper");
        final File ntpContributingFile = new File(ntpDir, "CONTRIBUTING.md");
        final File sshkeysDir = new File(modulesDir, "sshkeys");
        final File sshkeysModuleFile = new File(sshkeysDir, "Modulefile");
        if (dirsShouldExist) {
            assertDirExists(modulesDir);
            assertDirExists(ntpDir);
            assertDirExists(firewallDir);
            assertDirExists(sshkeysDir);
            /* keeperFile is in the submodules branch, but is a plain file */
            assertFileExists(keeperFile);
        } else {
            assertDirNotFound(modulesDir);
            assertDirNotFound(ntpDir);
            assertDirNotFound(firewallDir);
            assertDirNotFound(sshkeysDir);
            /* keeperFile is in the submodules branch, but is a plain file */
            assertFileNotFound(keeperFile);
        }
        if (filesShouldExist) {
            assertFileExists(ntpContributingFile);
            assertFileExists(sshkeysModuleFile);
        } else {
            assertFileNotFound(ntpContributingFile);
            assertFileNotFound(sshkeysModuleFile);
        }
    }

    private void assertDirNotFound(File dir) {
        assertFileNotFound(dir);
    }

    private void assertFileNotFound(File file) {
        assertFalse(file.exists(), file + " found, peer files: " + listDir(file.getParentFile()));
    }

    private void assertDirExists(File dir) {
        assertFileExists(dir);
        assertTrue(dir.isDirectory(), dir + " is not a directory");
    }

    private void assertFileExists(File file) {
        assertTrue(file.exists(), file + " not found, peer files: " + listDir(file.getParentFile()));
    }

    private String listDir(File dir) {
        if (dir == null || !dir.exists()) {
            return "";
        }
        File[] files = dir.listFiles();
        if (files == null) {
            return "";
        }
        StringBuilder fileList = new StringBuilder();
        for (File file : files) {
            fileList.append(file.getName());
            fileList.append(',');
        }
        if (!fileList.isEmpty()) {
            fileList.deleteCharAt(fileList.length() - 1);
        }
        return fileList.toString();
    }
}
