using LibGit2Sharp;
using RackPeek.Domain.Git;
using RackPeek.Domain.Git.UseCases;

namespace Tests.Git;

public sealed class GitRepositoryAvailabilityTests : IDisposable {
    private readonly string _tempDir;
    private readonly IGitCredentialsProvider _creds =
        new TokenCredentialsProvider("test-user", "test-token");

    public GitRepositoryAvailabilityTests() {
        _tempDir = Path.Combine(
            Path.GetTempPath(),
            "rackpeek-git-tests",
            Guid.NewGuid().ToString());

        Directory.CreateDirectory(_tempDir);
        File.WriteAllText(Path.Combine(_tempDir, "config.yaml"), "");
    }

    public void Dispose() {
        try {
            if (Directory.Exists(_tempDir))
                ForceDelete(_tempDir);
        }
        catch {
            // ignore cleanup issues
        }
    }

    private static void ForceDelete(string path) {
        foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) {
            File.SetAttributes(file, FileAttributes.Normal);
        }

        Directory.Delete(path, true);
    }

    [Fact]
    public void Fresh_Config_Directory_Becomes_Available_After_Construction() {
        // Reproduces the user's bug: GIT_TOKEN is set, container starts on a
        // fresh volume with only config.yaml. Without auto-init, IsAvailable
        // stays false and every git action returns "Git is not available."
        Assert.False(Repository.IsValid(_tempDir),
            "precondition: fresh dir should not be a git repo yet");

        var repo = new LibGit2GitRepository(_tempDir, _creds);

        Assert.True(repo.IsAvailable,
            "after construction with GIT_TOKEN configured, the repo should be initialised " +
            "so the user can immediately add a remote without an explicit Enable Git click");
    }

    [Fact]
    public async Task Add_Remote_Succeeds_On_Fresh_Config_Directory() {
        // End-to-end: simulates the user clicking Add Remote on a brand-new
        // install. Before the fix this returns "Git is not available." even
        // though GIT_TOKEN was set on the container.
        var repo = new LibGit2GitRepository(_tempDir, _creds);
        var useCase = new AddRemoteUseCase(repo);

        var error = await useCase.ExecuteAsync("https://example.com/user/repo.git");

        // The AddRemote fetch will fail (no real remote) but the failure
        // should be a network error, not "Git is not available."
        Assert.DoesNotContain("Git is not available", error ?? string.Empty);
    }

    [Fact]
    public void Existing_Repo_Is_Not_Reinitialised() {
        // Auto-init must be idempotent: pre-existing repos must keep their
        // history. Initialising twice would discard the user's commits.
        Repository.Init(_tempDir);

        using (var seed = new Repository(_tempDir)) {
            File.WriteAllText(Path.Combine(_tempDir, "seed.txt"), "seed");
            Commands.Stage(seed, "seed.txt");
            var sig = new Signature("seed", "seed@test", DateTimeOffset.UtcNow);
            seed.Commit("seed commit", sig, sig);
        }

        var repo = new LibGit2GitRepository(_tempDir, _creds);

        Assert.True(repo.IsAvailable);

        using var verify = new Repository(_tempDir);
        Assert.NotNull(verify.Head.Tip);
        Assert.Equal("seed commit", verify.Head.Tip.MessageShort);
    }

    [Fact]
    public void Missing_Directory_Stays_Unavailable() {
        // Edge case: directory doesn't exist (e.g. misconfigured volume).
        // We must not throw out of the constructor and we must not pretend
        // git is available.
        var missing = Path.Combine(_tempDir, "definitely-not-there");

        var repo = new LibGit2GitRepository(missing, _creds);

        Assert.False(repo.IsAvailable);
    }

    [Fact]
    public void Readonly_Config_Directory_Does_Not_Throw_From_Constructor() {
        // Doc promises the UI will surface "Git configured but config directory
        // is not writable" when init can't run — that requires the constructor
        // to swallow the init failure and set IsAvailable=false rather than
        // crashing the Blazor render that resolves the singleton.
        if (OperatingSystem.IsWindows())
            return; // chmod-style read-only doesn't translate cleanly

        var readOnly = Path.Combine(_tempDir, "readonly");
        Directory.CreateDirectory(readOnly);
        File.WriteAllText(Path.Combine(readOnly, "config.yaml"), "");

        // Strip write permission for owner + group + other (0555).
        File.SetUnixFileMode(readOnly,
            UnixFileMode.UserRead | UnixFileMode.UserExecute |
            UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
            UnixFileMode.OtherRead | UnixFileMode.OtherExecute);

        try {
            var repo = new LibGit2GitRepository(readOnly, _creds);

            Assert.False(repo.IsAvailable,
                "constructor must report unavailable, not throw, so the UI can " +
                "show the documented writability warning");
        }
        finally {
            // Restore write perm so cleanup works.
            File.SetUnixFileMode(readOnly,
                UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        }
    }

    [Fact]
    public async Task Add_Remote_Persists_The_Configured_Origin() {
        // The full happy path that was broken before the fix: construct a repo
        // against a fresh dir, add a remote, confirm it lives on disk so a
        // subsequent push/pull would find it.
        var repo = new LibGit2GitRepository(_tempDir, _creds);
        var useCase = new AddRemoteUseCase(repo);

        var url = "https://example.com/user/repo.git";
        await useCase.ExecuteAsync(url);

        using var verify = new Repository(_tempDir);
        Remote? origin = verify.Network.Remotes["origin"];

        Assert.NotNull(origin);
        Assert.Equal(url, origin!.Url);
    }

    [Fact]
    public void Existing_Repo_With_Remote_Is_Detected_As_Available() {
        // After a container restart we should pick up the previously
        // initialised repo (including its remote) without re-initialising
        // and without losing state.
        Repository.Init(_tempDir);
        using (var seed = new Repository(_tempDir)) {
            seed.Network.Remotes.Add("origin", "https://example.com/user/repo.git");
        }

        var repo = new LibGit2GitRepository(_tempDir, _creds);

        Assert.True(repo.IsAvailable);
        Assert.True(repo.HasRemote());
    }
}
