package org.jenkinsci.plugins.gitclient;

import static java.util.Collections.unmodifiableList;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.jenkinsci.plugins.gitclient.GitAPITest.getConfigNoSystemEnvVars;
import static org.junit.jupiter.api.Assertions.*;

import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import hudson.Launcher;
import hudson.ProxyConfiguration;
import hudson.Util;
import hudson.model.TaskListener;
import hudson.plugins.git.*;
import hudson.remoting.VirtualChannel;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.TemporaryDirectoryAllocator;
import org.objenesis.ObjenesisStd;

abstract class GitAPITestUpdate {

    private final TemporaryDirectoryAllocator temporaryDirectoryAllocator = new TemporaryDirectoryAllocator();

    private LogHandler handler = null;
    private int logCount = 0;
    protected static String defaultBranchName = "mast" + "er"; // Intentionally split string
    protected static String defaultRemoteBranchName = "origin/" + defaultBranchName;
    private static final String ZIP_FILE_DEFAULT_BRANCH_NAME = "mast" + "er";

    private final String remoteMirrorURL = "https://github.com/jenkinsci/git-client-plugin.git";

    protected TaskListener listener;

    private static final String DEFAULT_JGIT_BRANCH_NAME = Constants.MASTER;
    protected static final String DEFAULT_MIRROR_BRANCH_NAME = "mast" + "er"; // Intentionally split string

    private static final String LOGGING_STARTED = "Logging started";
    protected int checkoutTimeout = -1;
    protected int submoduleUpdateTimeout = -1;
    protected final Random random = new Random();

    protected final hudson.EnvVars env = getConfigNoSystemEnvVars();

    private static boolean firstRun = true;

    private void assertCheckoutTimeout() {
        if (checkoutTimeout > 0) {
            assertSubstringTimeout("git checkout", checkoutTimeout);
        }
    }

    private void assertSubmoduleUpdateTimeout() {
        if (submoduleUpdateTimeout > 0) {
            assertSubstringTimeout("git -c protocol.file.allow=always submodule update", submoduleUpdateTimeout);
        }
    }

    private void assertSubstringTimeout(final String substring, int expectedTimeout) {
        if (!(w.git instanceof CliGitAPIImpl)) { // Timeout only implemented in CliGitAPIImpl
            return;
        }
        List<String> messages = handler.getMessages();
        List<String> substringMessages = new ArrayList<>();
        List<String> substringTimeoutMessages = new ArrayList<>();
        final String messageRegEx = ".*\\b" + substring + "\\b.*"; // the expected substring
        final String timeoutRegEx = messageRegEx + " [#] timeout=" + expectedTimeout + "\\b.*"; // # timeout=<value>
        for (String message : messages) {
            if (message.matches(messageRegEx)) {
                substringMessages.add(message);
            }
            if (message.matches(timeoutRegEx)) {
                substringTimeoutMessages.add(message);
            }
        }
        assertThat(messages, is(not(empty())));
        assertThat(substringMessages, is(not(empty())));
        assertThat(substringTimeoutMessages, is(not(empty())));
        assertEquals(substringMessages, substringTimeoutMessages);
    }

    protected abstract GitClient setupGitAPI(File ws) throws Exception;

    class WorkingArea {

        final File repo;
        final GitClient git;
        final boolean bare = false;

        WorkingArea() throws Exception {
            this(temporaryDirectoryAllocator.allocate());
        }

        WorkingArea(File repo) throws Exception {
            this.repo = repo;
            git = setupGitAPI(repo);
            setupProxy(git);
        }

        private void setupProxy(GitClient gitClient) throws Exception {
            final String proxyHost = getSystemProperty("proxyHost", "http.proxyHost", "https.proxyHost");
            final String proxyPort = getSystemProperty("proxyPort", "http.proxyPort", "https.proxyPort");
            final String proxyUser = getSystemProperty("proxyUser", "http.proxyUser", "https.proxyUser");
            // final String proxyPassword = getSystemProperty("proxyPassword", "http.proxyPassword",
            // "https.proxyPassword");
            final String noProxyHosts = getSystemProperty("noProxyHosts", "http.noProxyHosts", "https.noProxyHosts");
            if (isBlank(proxyHost) || isBlank(proxyPort)) {
                return;
            }
            ProxyConfiguration proxyConfig = new ObjenesisStd().newInstance(ProxyConfiguration.class);
            setField(ProxyConfiguration.class, "name", proxyConfig, proxyHost);
            setField(ProxyConfiguration.class, "port", proxyConfig, Integer.parseInt(proxyPort));
            setField(ProxyConfiguration.class, "userName", proxyConfig, proxyUser);
            setField(ProxyConfiguration.class, "noProxyHost", proxyConfig, noProxyHosts);
            // Password does not work since a set password results in a "Secret" call which expects a running Jenkins
            setField(ProxyConfiguration.class, "password", proxyConfig, null);
            setField(ProxyConfiguration.class, "secretPassword", proxyConfig, null);
            gitClient.setProxy(proxyConfig);
        }

        private void setField(Class<?> clazz, String fieldName, Object object, Object value) throws Exception {
            Field declaredField = clazz.getDeclaredField(fieldName);
            declaredField.setAccessible(true);
            declaredField.set(object, value);
        }

        private String getSystemProperty(String... keyVariants) {
            for (String key : keyVariants) {
                String value = System.getProperty(key);
                if (value != null) {
                    return value;
                }
            }
            return null;
        }

        String launchCommand(String... args) throws Exception {
            return launchCommand(false, args);
        }

        String launchCommand(boolean ignoreError, String... args) throws Exception {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int st = new Launcher.LocalLauncher(listener)
                    .launch()
                    .pwd(repo)
                    .cmds(args)
                    .envs(env)
                    .stdout(out)
                    .join();
            String s = out.toString();
            if (!ignoreError) {
                if (s == null || s.isEmpty()) {
                    s = StringUtils.join(args, ' ');
                }
                assertEquals(0, st, s);
                /* Reports full output of failing commands */
            }
            return s;
        }

        String repoPath() {
            return repo.getAbsolutePath();
        }

        GitAPITestUpdate.WorkingArea init() throws Exception {
            git.init();
            String userName = "root";
            String emailAddress = "root@mydomain.com";
            CliGitCommand gitCmd = new CliGitCommand(git);
            gitCmd.initializeRepository(userName, emailAddress);
            git.setAuthor(userName, emailAddress);
            git.setCommitter(userName, emailAddress);
            return this;
        }

        GitAPITestUpdate.WorkingArea init(boolean bare) throws Exception {
            git.init_().workspace(repoPath()).bare(bare).execute();
            return this;
        }

        void tag(String tag) throws Exception {
            tag(tag, false);
        }

        void tag(String tag, boolean force) throws Exception {
            if (force) {
                launchCommand("git", "tag", "--force", tag);
            } else {
                launchCommand("git", "tag", tag);
            }
        }

        void commitEmpty(String msg) throws Exception {
            launchCommand("git", "commit", "--allow-empty", "-m", msg);
        }

        /**
         * Refers to a file in this workspace
         */
        File file(String path) {
            return new File(repo, path);
        }

        boolean exists(String path) {
            return file(path).exists();
        }

        /**
         * Creates a file in the workspace.
         */
        void touch(String path) throws Exception {
            file(path).createNewFile();
        }

        /**
         * Creates a file in the workspace.
         */
        File touch(String path, String content) throws Exception {
            File f = file(path);
            Files.writeString(f.toPath(), content, StandardCharsets.UTF_8);
            return f;
        }

        void rm(String path) {
            file(path).delete();
        }

        String contentOf(String path) throws Exception {
            return Files.readString(file(path).toPath(), StandardCharsets.UTF_8);
        }

        /* Remember the CliGitAPIImpl for this WorkingArea so that the
         * allowFileProtocol() changes are "sticky" for the life of
         * the WorkingArea.
         *
         * Some of the submodule checkout tests use command line git
         * to create a submodule test repository.  When running the
         * test with JGit, the command line git configuration of the
         * WorkingArea needs to be preserved for the life of the
         * WorkingArea object so that command line git uses the
         * correct arguments when creating the submodule test
         * repository.
         */
        private CliGitAPIImpl cachedCliGitAPIImpl = null;

        /**
         * Returns a CGit implementation. Sometimes we need this for testing
         * JGit impl.
         */
        CliGitAPIImpl cgit() throws Exception {
            if (git instanceof CliGitAPIImpl impl) {
                return impl;
            }
            if (cachedCliGitAPIImpl != null) {
                return cachedCliGitAPIImpl;
            }
            cachedCliGitAPIImpl = (CliGitAPIImpl)
                    Git.with(listener, env).in(repo).using("git").getClient();
            return cachedCliGitAPIImpl;
        }

        /**
         * Creates a JGit implementation. Sometimes we need this for testing
         * CliGit impl.
         */
        JGitAPIImpl jgit() throws Exception {
            return (JGitAPIImpl) Git.with(listener, env).in(repo).using("jgit").getClient();
        }

        /**
         * Creates a {@link Repository} object out of it.
         */
        FileRepository repo() throws Exception {
            return bare ? new FileRepository(repo) : new FileRepository(new File(repo, ".git"));
        }

        /**
         * Obtain the current HEAD revision
         */
        ObjectId head() throws Exception {
            return git.revParse("HEAD");
        }

        /**
         * Casts the {@link #git} to {@link IGitAPI}
         */
        IGitAPI igit() {
            return (IGitAPI) git;
        }

        void initializeWorkingArea(String userName, String userEmail) throws Exception {
            CliGitCommand gitCmd = new CliGitCommand(git);
            gitCmd.initializeRepository(userName, userEmail);
        }
    }

    protected WorkingArea w;

    protected WorkingArea clone(String src) throws Exception {
        WorkingArea x = new WorkingArea();
        FileUtils.cleanDirectory(new File(x.repoPath()));
        x.launchCommand("git", "clone", src, x.repoPath());
        WorkingArea clonedArea = new WorkingArea(x.repo);
        clonedArea.initializeWorkingArea(
                "Vojtěch Zweibrücken-Šafařík", "email.address.from.git.client.plugin.test@example.com");
        return clonedArea;
    }

    @BeforeEach
    public void setUp() throws Exception {
        if (firstRun) {
            firstRun = false;
            defaultBranchName = getDefaultBranchName();
            defaultRemoteBranchName = "origin/" + defaultBranchName;
        }
        checkoutTimeout = -1;
        submoduleUpdateTimeout = -1;
        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);
        w = new WorkingArea();
    }

    private boolean isShallow() {
        File shallowMarker = new File(".git", "shallow");
        return shallowMarker.isFile();
    }

    /**
     * Populate the local mirror of the git client plugin repository. Returns
     * path to the local mirror directory.
     *
     * @return path to the local mirror directory
     * @throws IOException on I/O error
     * @throws InterruptedException when exception is interrupted
     */
    protected String localMirror() throws Exception {
        File base = new File(".").getAbsoluteFile();
        for (File f = base; f != null; f = f.getParentFile()) {
            File targetDir = new File(f, "target");
            if (targetDir.exists()) {
                String cloneDirName = "clone.git";
                File clone = new File(targetDir, cloneDirName);
                if (!clone.exists()) {
                    /* Clone to a temporary directory then move the
                     * temporary directory to the final destination
                     * directory. The temporary directory prevents
                     * collision with other tests running in parallel.
                     * The atomic move after clone completion assures
                     * that only one of the parallel processes creates
                     * the final destination directory.
                     */
                    Path tempClonePath = Files.createTempDirectory(targetDir.toPath(), "clone-");
                    String repoUrl = "https://github.com/jenkinsci/git-client-plugin.git";
                    String destination = tempClonePath.toFile().getAbsolutePath();
                    if (isShallow()) {
                        w.launchCommand("git", "clone", "--mirror", repoUrl, destination);
                    } else {
                        w.launchCommand(
                                "git", "clone", "--reference", f.getCanonicalPath(), "--mirror", repoUrl, destination);
                    }
                    if (!clone.exists()) { // Still a race condition, but a narrow race handled by Files.move()
                        renameAndDeleteDir(tempClonePath, cloneDirName);
                    } else {
                        /*
                         * If many unit tests run at the same time and
                         * are using the localMirror, multiple clones
                         * will happen.  All but one of the clones
                         * will be discarded.  The tests reduce the
                         * likelihood of multiple concurrent clones by
                         * adding a random delay to the start of
                         * longer running tests that use the local
                         * mirror.  The delay was enough in my tests
                         * to prevent the duplicate clones and the
                         * resulting discard of the results of the
                         * clone.
                         *
                         * Different processor configurations with
                         * different performance characteristics may
                         * still have parallel tests which attempt to
                         * clone the local mirror concurrently. If
                         * parallel clones happen, only one of the
                         * parallel clones will 'win the race'.  The
                         * deleteRecursive() will discard a clone that
                         * 'lost the race'.
                         */
                        Util.deleteRecursive(tempClonePath.toFile());
                    }
                }
                return clone.getPath();
            }
        }
        throw new IllegalStateException();
    }

    private void renameAndDeleteDir(Path srcDir, String destDirName) {
        try {
            // Try an atomic move first
            Files.move(srcDir, srcDir.resolveSibling(destDirName), StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException e) {
            // If Atomic move is not supported, try a move, display exception on failure
            try {
                Files.move(srcDir, srcDir.resolveSibling(destDirName));
            } catch (IOException ioe) {
                Util.displayIOException(ioe, listener);
            }
        } catch (FileAlreadyExistsException ignored) {
            // Intentionally ignore FileAlreadyExists, another thread or process won the race
        } catch (IOException ioe) {
            Util.displayIOException(ioe, listener);
        } finally {
            try {
                Util.deleteRecursive(srcDir.toFile());
            } catch (IOException ioe) {
                Util.displayIOException(ioe, listener);
            }
        }
    }

    private List<File> tempDirsToDelete = new ArrayList<>();

    @AfterEach
    public void tearDown() throws Exception {
        try {
            temporaryDirectoryAllocator.dispose();
        } catch (IOException e) {
            e.printStackTrace(System.err);
        }
        try {
            String messages = StringUtils.join(handler.getMessages(), ";");
            assertTrue(handler.containsMessageSubstring(LOGGING_STARTED), "Logging not started: " + messages);
            assertCheckoutTimeout();
            assertSubmoduleUpdateTimeout();
        } finally {
            handler.close();
        }
        try {
            for (File tempdir : tempDirsToDelete) {
                Util.deleteRecursive(tempdir);
            }
        } catch (IOException e) {
            e.printStackTrace(System.err);
        } finally {
            tempDirsToDelete = new ArrayList<>();
        }
    }

    private void checkGetHeadRev(String remote, String branchSpec, ObjectId expectedObjectId) throws Exception {
        ObjectId actualObjectId = w.git.getHeadRev(remote, branchSpec);
        assertNotNull(
                expectedObjectId,
                "Expected ObjectId is null expectedObjectId '%s', remote '%s', branchSpec '%s'."
                        .formatted(expectedObjectId, remote, branchSpec));
        assertNotNull(
                actualObjectId,
                "Actual ObjectId is null. expectedObjectId '%s', remote '%s', branchSpec '%s'."
                        .formatted(expectedObjectId, remote, branchSpec));
        assertEquals(
                expectedObjectId,
                actualObjectId,
                ("""
                        Actual ObjectId differs from expected one for branchSpec '%s', remote '%s':
                        Actual %s,
                        Expected %s
                        """)
                        .formatted(
                                branchSpec,
                                remote,
                                StringUtils.join(getBranches(actualObjectId), ", "),
                                StringUtils.join(getBranches(expectedObjectId), ", ")));
    }

    private List<Branch> getBranches(ObjectId objectId) throws Exception {
        List<Branch> matches = new ArrayList<>();
        Set<Branch> branches = w.git.getBranches();
        for (Branch branch : branches) {
            if (branch.getSHA1().equals(objectId)) {
                matches.add(branch);
            }
        }
        return unmodifiableList(matches);
    }

    /**
     * Test getHeadRev with namespaces in the branch name and branch specs
     * starting with "refs/heads/".
     */
    @Test
    public void testGetHeadRevNamespacesWithRefsHeads() throws Exception {
        File tempRemoteDir = temporaryDirectoryAllocator.allocate();
        extract(new ZipFile("src/test/resources/namespaceBranchRepo.zip"), tempRemoteDir);
        Properties commits = parseLsRemote(new File("src/test/resources/namespaceBranchRepo.ls-remote"));
        w = clone(tempRemoteDir.getAbsolutePath());
        final String remote = tempRemoteDir.getAbsolutePath();

        final String[][] checkBranchSpecs = {
            {
                "refs/heads/" + ZIP_FILE_DEFAULT_BRANCH_NAME,
                commits.getProperty("refs/heads/" + ZIP_FILE_DEFAULT_BRANCH_NAME)
            },
            {
                "refs/heads/a_tests/b_namespace1/" + ZIP_FILE_DEFAULT_BRANCH_NAME,
                commits.getProperty("refs/heads/a_tests/b_namespace1/" + ZIP_FILE_DEFAULT_BRANCH_NAME)
            },
            {
                "refs/heads/a_tests/b_namespace2/" + ZIP_FILE_DEFAULT_BRANCH_NAME,
                commits.getProperty("refs/heads/a_tests/b_namespace2/" + ZIP_FILE_DEFAULT_BRANCH_NAME)
            },
            {
                "refs/heads/a_tests/b_namespace3/" + ZIP_FILE_DEFAULT_BRANCH_NAME,
                commits.getProperty("refs/heads/a_tests/b_namespace3/" + ZIP_FILE_DEFAULT_BRANCH_NAME)
            },
            {
                "refs/heads/b_namespace3/" + ZIP_FILE_DEFAULT_BRANCH_NAME,
                commits.getProperty("refs/heads/b_namespace3/" + ZIP_FILE_DEFAULT_BRANCH_NAME)
            }
        };

        for (String[] branch : checkBranchSpecs) {
            final ObjectId objectId = ObjectId.fromString(branch[1]);
            final String branchName = branch[0];
            checkGetHeadRev(remote, branchName, objectId);
        }
    }

    /**
     * Command line git clean as implemented in CliGitAPIImpl does not remove
     * untracked submodules or files contained in untracked submodule dirs. JGit
     * clean as implemented in JGitAPIImpl removes untracked submodules. This
     * test captures that surprising difference between the implementations.
     *
     * Command line git as implemented in CliGitAPIImpl supports renamed
     * submodules. JGit as implemented in JGitAPIImpl does not support renamed
     * submodules. This test captures that surprising difference between the
     * implementations.
     *
     * This test really should be split into multiple tests. Current transitions
     * in the test include: with submodules -> without submodules, with
     * files/dirs of same name with submodules -> without submodules, no
     * files/dirs of same name
     *
     * See bug reports such as: JENKINS-22510 - Clean After Checkout Results in
     * Failed to Checkout Revision JENKINS-8053 - Git submodules are cloned too
     * early and not removed once the revToBuild has been checked out
     * JENKINS-14083 - Build can't recover from broken submodule path
     * JENKINS-15399 - Changing remote URL doesn't update submodules
     *
     * @throws Exception on test failure
     */
    @Test
    public void testSubmoduleCheckoutAndCleanTransitions() throws Exception {
        if (isWindows() || random.nextBoolean()) {
            /* Skip slow, low value test on Windows, run 50% of time on non-Windows */
            return;
        }
        w = clone(localMirror());
        assertSubmoduleDirs(false, false);

        String subBranch = "tests/getSubmodules";
        String subRefName = "origin/" + subBranch;

        File modulesDir = new File(w.repo, "modules");
        assertDirNotFound(modulesDir);

        File keeperFile = new File(modulesDir, "keeper");
        assertFileNotFound(keeperFile);

        File ntpDir = new File(modulesDir, "ntp");
        File ntpContributingFile = new File(ntpDir, "CONTRIBUTING.md");
        assertDirNotFound(ntpDir);
        assertFileNotFound(ntpContributingFile);

        File firewallDir = new File(modulesDir, "firewall");
        assertDirNotFound(firewallDir);

        File sshkeysDir = new File(modulesDir, "sshkeys");
        File sshkeysModuleFile = new File(sshkeysDir, "Modulefile");
        assertDirNotFound(sshkeysDir);
        assertFileNotFound(sshkeysModuleFile);

        /* Checkout a branch which includes submodules (in modules directory) */
        w.git.checkout().ref(subRefName).branch(subBranch).execute();
        assertDirExists(modulesDir);
        assertFileExists(keeperFile);
        assertFileContents(keeperFile, "");
        assertSubmoduleDirs(true, false);

        /* Call submodule update without recursion */
        w.git.submoduleUpdate().recursive(false).execute();
        /* Command line git supports renamed submodule dirs, JGit does not */
        /* JGit silently fails submodule updates on renamed submodule dirs */
        if (w.git instanceof CliGitAPIImpl) {
            assertSubmoduleDirs(true, true);
            assertSubmoduleContents();
            assertSubmoduleRepository(new File(w.repo, "modules/ntp"));
            assertSubmoduleRepository(new File(w.repo, "modules/firewall"));
            assertSubmoduleRepository(new File(w.repo, "modules/sshkeys"));
        } else {
            /* JGit does not fully support renamed submodules - creates directories but not content */
            assertSubmoduleDirs(true, false);
        }

        /* Call submodule update with recursion */
        w.git.submoduleUpdate().recursive(true).execute();
        /* Command line git supports renamed submodule dirs, JGit does not */
        /* JGit silently fails submodule updates on renamed submodule dirs */
        if (w.git instanceof CliGitAPIImpl) {
            assertSubmoduleDirs(true, true);
            assertSubmoduleContents();
            assertSubmoduleRepository(new File(w.repo, "modules/ntp"));
            assertSubmoduleRepository(new File(w.repo, "modules/firewall"));
            assertSubmoduleRepository(new File(w.repo, "modules/sshkeys"));
        } else {
            /* JGit does not fully support renamed submodules - creates directories but not content */
            assertSubmoduleDirs(true, false);
        }

        String notSubBranchName = "tests/notSubmodules";
        String notSubRefName = "origin/" + notSubBranchName;
        String contributingFileContentFromNonsubmoduleBranch = "This is not a useful contribution";

        /* Checkout a detached head which does not include submodules,
         * since checkout of a branch does not currently use the "-f"
         * option (though it probably should).  The checkout includes a file
         * modules/ntp/CONTRIBUTING.md which collides with a file from the
         * submodule but is provided from the repository rather than from a
         * submodule.
         */
        // w.git.checkout().ref(notSubRefName).execute();
        w.git.checkout()
                .ref(notSubRefName)
                .branch(notSubBranchName)
                .deleteBranchIfExist(true)
                .execute();
        assertDirExists(ntpDir);
        assertFileExists(ntpContributingFile);
        assertFileContains(ntpContributingFile, contributingFileContentFromNonsubmoduleBranch);
        if (w.git instanceof CliGitAPIImpl) {
            /*
             * Transition from "with submodule" to "without submodule"
             * where the "without submodule" case includes the file
             * ntpContributingFile and the directory ntpDir.
             */
            /* submodule dirs exist because git.clean() won't remove untracked submodules */
            assertDirExists(firewallDir);
            assertDirExists(sshkeysDir);
            assertFileExists(sshkeysModuleFile);
        } else {
            /*
             * Transition from "with submodule" to "without submodule"
             * where the "without submodule" case includes the file
             * ntpContributingFile and the directory ntpDir.
             *
             * Prior to JGit 5.3.1 ntpDir was not available at this point.
             *
             * Prior to JGit 5.2.0 and the CheckoutCommand bug fix,
             * the ntpDir would remain along with ntpContributingFile.
             */
            /* firewallDir and sshKeysDir don't exist because JGit submodule update never created them */
            assertDirNotFound(firewallDir);
            assertDirNotFound(sshkeysDir);
        }

        /* CLI git clean does not remove submodule remnants, JGit does */
        w.git.clean();
        assertDirExists(ntpDir);
        assertFileExists(ntpContributingFile);
        /* exists in nonSubmodule branch */
        if (w.git instanceof CliGitAPIImpl) {
            /* untracked - CLI clean doesn't remove submodule dirs or their contents */
            assertDirExists(firewallDir);
            assertDirExists(sshkeysDir);
            assertFileExists(sshkeysModuleFile);
        } else {
            /* JGit clean removes submodule dirs*/
            assertDirNotFound(firewallDir);
            assertDirNotFound(sshkeysDir);
        }

        /* Checkout default remote branch - will leave submodule files untracked */
        w.git.checkout().ref(DEFAULT_MIRROR_BRANCH_NAME).execute();
        // w.git.checkout().ref(DEFAULT_MIRROR_BRANCH_NAME).branch(defaultBranchName).execute();
        if (w.git instanceof CliGitAPIImpl) {
            /* CLI git clean will not remove untracked submodules */
            assertDirExists(ntpDir);
            assertDirExists(firewallDir);
            assertDirExists(sshkeysDir);
            assertFileNotFound(ntpContributingFile);
            /* cleaned because it is in tests/notSubmodules branch */
            assertFileExists(sshkeysModuleFile);
        } else {
            /* JGit git clean removes them */
            assertDirNotFound(ntpDir);
            assertDirNotFound(firewallDir);
            assertDirNotFound(sshkeysDir);
        }

        /* git.clean() does not remove submodule remnants in CliGitAPIImpl, does in JGitAPIImpl */
        w.git.clean();
        if (w.git instanceof CliGitAPIImpl) {
            assertDirExists(ntpDir);
            assertDirExists(firewallDir);
            assertDirExists(sshkeysDir);
        } else {
            assertDirNotFound(ntpDir);
            assertDirNotFound(firewallDir);
            assertDirNotFound(sshkeysDir);
        }

        /* Really remove submodule remnant, use git command line double force */
        if (w.git instanceof CliGitAPIImpl) {
            if (!isWindows()) {
                w.launchCommand("git", "clean", "-xffd");
            } else {
                try {
                    w.launchCommand("git", "clean", "-xffd");
                } catch (IOException | InterruptedException e) {
                    /* Retry once (and only once) in case of Windows busy file behavior */
                    Thread.sleep(503);
                    /* Wait 0.5 seconds for Windows */
                    w.launchCommand("git", "clean", "-xffd");
                }
            }
        }
        assertSubmoduleDirs(false, false);

        /* Checkout a branch which *includes submodules* after a prior
         * checkout with a file which has the same name as a file
         * provided by a submodule checkout.  Use a detached head,
         * since checkout of a branch does not currently use the "-f"
         * option.
         */
        assertEquals(ObjectId.fromString("a6dd186704985fdb0c60e60f5c6ea7ea35e082e5"), w.git.revParse(subRefName));
        // w.git.checkout().ref(subRefName).branch(subBranch).execute();
        w.git.checkout().ref(subRefName).execute();
        assertDirExists(modulesDir);
