package org.jenkinsci.plugins.gitclient;

import static org.jenkinsci.plugins.gitclient.GitAPITest.getConfigNoSystemEnvVars;
import static org.junit.jupiter.api.Assertions.*;

import hudson.model.TaskListener;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.util.SystemReader;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;

/**
 * Test that command line git preserves execute permission across clone. Git
 * does not preserve all file permission bits, only the execute bit for the
 * owner.
 */
@ParameterizedClass
@MethodSource("permissionBits")
class FilePermissionsTest {

    @Parameter(0)
    private int permission;

    private static final TaskListener listener = StreamTaskListener.fromStdout();

    private static File repo;

    @BeforeAll
    static void createTestRepo() throws Exception {
        if (isWindows()) {
            return;
        }
        SystemReader.getInstance().getUserConfig().clear();
        repo = Files.createTempDirectory(null).toFile();
        Git.with(listener, new hudson.EnvVars()).in(repo).getClient().init();
    }

    /**
     * Tests that need the default branch name can use this variable.
     */
    private static String defaultBranchName = "mast" + "er"; // Intentionally separated string

    /**
     * Determine the global default branch name.
     * Command line git is moving towards more inclusive naming.
     * Git 2.32.0 honors the configuration variable `init.defaultBranch` and uses it for the name of the initial branch.
     * This method reads the global configuration and uses it to set the value of `defaultBranchName`.
     */
    @BeforeAll
    static void computeDefaultBranchName() throws Exception {
        File configDir = Files.createTempDirectory("readGitConfig").toFile();
        CliGitCommand getDefaultBranchNameCmd =
                new CliGitCommand(Git.with(TaskListener.NULL, getConfigNoSystemEnvVars())
                        .in(configDir)
                        .using("git")
                        .getClient());
        String[] output = getDefaultBranchNameCmd.runWithoutAssert("config", "--get", "init.defaultBranch");
        for (String s : output) {
            String result = s.trim();
            if (result != null && !result.isEmpty()) {
                defaultBranchName = result;
            }
        }
        assertTrue(configDir.delete(), "Failed to delete temporary readGitConfig directory");
    }

    @AfterAll
    static void verifyTestRepo() throws Exception {
        if (isWindows()) {
            return;
        }
        File newRepo = null;
        try {
            newRepo = cloneTestRepo(repo);
            List<Integer[]> permissions = permissionBits();
            for (Integer[] permArray : permissions) {
                int filePermission = permArray[0];
                verifyFile(repo, filePermission);
                verifyFile(newRepo, filePermission);
            }
        } finally {
            FileUtils.deleteDirectory(repo); // Remove the original test repo
            if (newRepo != null) {
                FileUtils.deleteDirectory(newRepo); // Remove the verification repo
            }
        }
    }

    private static File cloneTestRepo(File repo) throws Exception {
        File newRepo = Files.createTempDirectory(null).toFile();
        GitClient git = Git.with(listener, new hudson.EnvVars())
                .in(newRepo)
                .using("git")
                .getClient();
        String repoURL = repo.toURI().toURL().toString();
        git.clone_().repositoryName("origin").url(repoURL).execute();
        git.checkoutBranch(defaultBranchName, "origin/" + defaultBranchName);
        return newRepo;
    }

    private static void verifyFile(File repo, int staticPerm) throws Exception {
        String fileName = "git-%03o.txt".formatted(staticPerm);
        File file = new File(repo, fileName);
        assertTrue(file.exists(), "Missing " + file.getAbsolutePath());
        String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
        assertTrue(content.contains(fileName), fileName + " wrong content: '" + content + "'");
        String rwx = permString(staticPerm);
        Set<PosixFilePermission> expected = PosixFilePermissions.fromString(rwx);
        Path path = FileSystems.getDefault().getPath(file.getPath());
        PosixFileAttributes attrs =
                Files.getFileAttributeView(path, PosixFileAttributeView.class).readAttributes();
        assertEquals(
                expected.contains(PosixFilePermission.OWNER_EXECUTE),
                attrs.permissions().contains(PosixFilePermission.OWNER_EXECUTE),
                fileName + " OWNER_EXECUTE (execute) perm mismatch, expected: " + expected + ", was actually: "
                        + attrs.permissions());
    }

    @AfterEach
    void checkListenerLoggedNoErrors() {
        assertFalse(listener.getLogger().checkError(), "Error logged by listener");
    }

    static List<Integer[]> permissionBits() {
        List<Integer[]> permissions = new ArrayList<>();
        /* 0640 and 0740 are the only permissions to be tested */
        Integer[] permissionArray0640 = {0640};
        permissions.add(permissionArray0640);
        /* When running as root, execute permission is NOT preserved, don't test it */
        if (!(new File("/").canWrite())) {
            Integer[] permissionArray0740 = {0740};
            permissions.add(permissionArray0740);
        }
        return permissions;
    }

    @Test
    void posixPermissionTest() throws Exception {
        if (isWindows()) {
            return;
        }
        addFile();
        modifyFile();
    }

    private String getFileName() {
        return "git-%03o.txt".formatted(permission);
    }

    private void addFile() throws Exception {
        String fileName = getFileName();
        String content = fileName + " and UUID " + UUID.randomUUID();
        File added = new File(repo, fileName);
        assertFalse(added.exists(), fileName + " already exists");
        Files.writeString(added.toPath(), content, StandardCharsets.UTF_8);
        assertTrue(added.exists(), fileName + " doesn't exist");

        GitClient git = Git.with(listener, getConfigNoSystemEnvVars()).in(repo).getClient();
        git.add(fileName);
        git.commit("Added " + fileName);
        Path path = FileSystems.getDefault().getPath(added.getPath());
        assertEquals(path, Files.setPosixFilePermissions(path, filePerms(permission)));
        git.add(fileName);
        git.commit("Perms %03o %s".formatted(permission, fileName));
    }

    private void modifyFile() throws Exception {
        String fileName = getFileName();
        String content = fileName + " chg UUID " + UUID.randomUUID();
        File modified = new File(repo, fileName);
        assertTrue(modified.exists(), fileName + " doesn't exist");
        Files.writeString(modified.toPath(), content, StandardCharsets.UTF_8);

        GitClient git = Git.with(listener, getConfigNoSystemEnvVars()).in(repo).getClient();
        git.add(fileName);
        git.commit("Modified " + fileName);
        assertTrue(modified.exists(), fileName + " doesn't exist");
    }

    private static String permString(int filePermission) {
        return String.valueOf((filePermission & 0400) != 0 ? 'r' : '-')
                + ((filePermission & 0200) != 0 ? 'w' : '-')
                + ((filePermission & 0100) != 0 ? 'x' : '-')
                + ((filePermission & 0040) != 0 ? 'r' : '-')
                + ((filePermission & 0020) != 0 ? 'w' : '-')
                + ((filePermission & 0010) != 0 ? 'x' : '-')
                + ((filePermission & 0004) != 0 ? 'r' : '-')
                + ((filePermission & 0002) != 0 ? 'w' : '-')
                + ((filePermission & 0001) != 0 ? 'x' : '-');
    }

    private Set<PosixFilePermission> filePerms(int filePermission) {
        return PosixFilePermissions.fromString(permString(filePermission));
    }

    private static boolean isWindows() {
        return File.pathSeparatorChar == ';';
    }
}
