using System.IO;
using System.IO.Compression;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Steps;
using ClaudeDo.Releases;

namespace ClaudeDo.Installer.Tests;

public sealed class DownloadAndExtractStepTests : IDisposable
{
    private readonly string _tempDir;
    private readonly string _installDir;
    private readonly string _cacheDir;

    public DownloadAndExtractStepTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDoDownloadStep-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(_tempDir);
        _installDir = Path.Combine(_tempDir, "install");
        Directory.CreateDirectory(_installDir);
        _cacheDir = Path.Combine(_tempDir, "cache");
    }

    public void Dispose()
    {
        try { Directory.Delete(_tempDir, recursive: true); } catch { }
    }

    private sealed class FileCopyReleaseClient : IReleaseClient
    {
        private readonly Dictionary<string, string> _urlToSourceFile;
        public GiteaRelease? Release { get; set; }
        public List<string> DownloadedUrls { get; } = new();

        public FileCopyReleaseClient(Dictionary<string, string> urlToSourceFile)
            => _urlToSourceFile = urlToSourceFile;

        public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct) => Task.FromResult(Release);

        public Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
        {
            DownloadedUrls.Add(url);
            File.Copy(_urlToSourceFile[url], destPath, overwrite: true);
            progress.Report(new FileInfo(destPath).Length);
            return Task.CompletedTask;
        }
    }

    // Builds a valid release zip + checksums.txt and a client that serves both.
    private (FileCopyReleaseClient Client, string ZipName) SetUpRelease()
    {
        var zipPath = Path.Combine(_tempDir, "release.zip");
        using (var fs = File.Create(zipPath))
        using (var zip = new ZipArchive(fs, ZipArchiveMode.Create))
        {
            var a = zip.CreateEntry("app/a.txt");
            using (var w = new StreamWriter(a.Open())) w.Write("hello-app");
            var b = zip.CreateEntry("worker/b.txt");
            using (var w = new StreamWriter(b.Open())) w.Write("hello-worker");
        }

        const string zipName = "ClaudeDo-0.1.0-win-x64.zip";
        var checksumsPath = Path.Combine(_tempDir, "checksums.txt");
        File.WriteAllText(checksumsPath, $"{ChecksumVerifier.ComputeSha256(zipPath)}  {zipName}\n");

        var release = new GiteaRelease("v0.1.0", "v0.1.0", new[]
        {
            new ReleaseAsset(zipName, "fake://zip", new FileInfo(zipPath).Length),
            new ReleaseAsset("checksums.txt", "fake://checksums", new FileInfo(checksumsPath).Length),
        });

        var client = new FileCopyReleaseClient(new()
        {
            ["fake://zip"] = zipPath,
            ["fake://checksums"] = checksumsPath,
        }) { Release = release };

        return (client, zipName);
    }

    [Fact]
    public async Task Extracts_Zip_Into_InstallDir_App_And_Worker()
    {
        var zipPath = Path.Combine(_tempDir, "release.zip");
        using (var fs = File.Create(zipPath))
        using (var zip = new ZipArchive(fs, ZipArchiveMode.Create))
        {
            var a = zip.CreateEntry("app/a.txt");
            using (var w = new StreamWriter(a.Open())) w.Write("hello-app");
            var b = zip.CreateEntry("worker/b.txt");
            using (var w = new StreamWriter(b.Open())) w.Write("hello-worker");
        }

        var zipHash = ChecksumVerifier.ComputeSha256(zipPath);
        var checksumsPath = Path.Combine(_tempDir, "checksums.txt");
        File.WriteAllText(checksumsPath, $"{zipHash}  ClaudeDo-0.1.0-win-x64.zip\n");

        var release = new GiteaRelease("v0.1.0", "v0.1.0", new[]
        {
            new ReleaseAsset("ClaudeDo-0.1.0-win-x64.zip", "fake://zip", new FileInfo(zipPath).Length),
            new ReleaseAsset("checksums.txt", "fake://checksums", new FileInfo(checksumsPath).Length),
        });

        var client = new FileCopyReleaseClient(new()
        {
            ["fake://zip"] = zipPath,
            ["fake://checksums"] = checksumsPath,
        }) { Release = release };

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.True(result.Success, result.ErrorMessage);
        Assert.Equal("hello-app", File.ReadAllText(Path.Combine(_installDir, "app", "a.txt")));
        Assert.Equal("hello-worker", File.ReadAllText(Path.Combine(_installDir, "worker", "b.txt")));
        Assert.Equal("0.1.0", ctx.InstalledVersion);
    }

    [Fact]
    public async Task Fails_On_ChecksumMismatch_Without_Overwriting_InstallDir()
    {
        var zipPath = Path.Combine(_tempDir, "release.zip");
        using (var fs = File.Create(zipPath))
        using (var zip = new ZipArchive(fs, ZipArchiveMode.Create))
        {
            var a = zip.CreateEntry("app/a.txt");
            using (var w = new StreamWriter(a.Open())) w.Write("x");
        }

        var checksumsPath = Path.Combine(_tempDir, "checksums.txt");
        File.WriteAllText(checksumsPath, $"{new string('0', 64)}  ClaudeDo-0.1.0-win-x64.zip\n");

        File.WriteAllText(Path.Combine(_installDir, "marker.txt"), "untouched");

        var release = new GiteaRelease("v0.1.0", "v0.1.0", new[]
        {
            new ReleaseAsset("ClaudeDo-0.1.0-win-x64.zip", "fake://zip", 0),
            new ReleaseAsset("checksums.txt", "fake://checksums", 0),
        });

        var client = new FileCopyReleaseClient(new()
        {
            ["fake://zip"] = zipPath,
            ["fake://checksums"] = checksumsPath,
        }) { Release = release };

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.False(result.Success);
        Assert.Contains("checksum", result.ErrorMessage!, StringComparison.OrdinalIgnoreCase);
        Assert.True(File.Exists(Path.Combine(_installDir, "marker.txt")));
        Assert.False(Directory.Exists(Path.Combine(_installDir, "app")));
    }

    [Fact]
    public async Task Fails_When_Release_Has_No_Zip_Asset()
    {
        var release = new GiteaRelease("v0.1.0", "v0.1.0", Array.Empty<ReleaseAsset>());
        var client = new FileCopyReleaseClient(new()) { Release = release };
        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.False(result.Success);
        Assert.Contains("not found", result.ErrorMessage!, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Reuses_Cached_Zip_Instead_Of_Downloading_It_Again()
    {
        var (client, zipName) = SetUpRelease();
        Directory.CreateDirectory(_cacheDir);
        // A previous run left a fully downloaded, still-valid zip behind.
        File.Copy(Path.Combine(_tempDir, "release.zip"), Path.Combine(_cacheDir, zipName));

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.True(result.Success, result.ErrorMessage);
        Assert.DoesNotContain("fake://zip", client.DownloadedUrls);
        Assert.Equal("hello-app", File.ReadAllText(Path.Combine(_installDir, "app", "a.txt")));
    }

    [Fact]
    public async Task Redownloads_When_Cached_Zip_Is_Corrupt()
    {
        var (client, zipName) = SetUpRelease();
        Directory.CreateDirectory(_cacheDir);
        // A cancelled/aborted download left a truncated file behind.
        File.WriteAllText(Path.Combine(_cacheDir, zipName), "not-a-zip");

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.True(result.Success, result.ErrorMessage);
        Assert.Contains("fake://zip", client.DownloadedUrls);
        Assert.Equal("hello-app", File.ReadAllText(Path.Combine(_installDir, "app", "a.txt")));
    }

    [Fact]
    public async Task Keeps_Zip_Cached_When_The_Install_Fails_And_Drops_It_On_Success()
    {
        var (client, zipName) = SetUpRelease();
        var cachedZip = Path.Combine(_cacheDir, zipName);

        // Make extraction fail: a *file* named "app" blocks the app/ entry's directory.
        var blocker = Path.Combine(_installDir, "app");
        File.WriteAllText(blocker, "blocked");

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var failed = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);
        Assert.False(failed.Success);
        Assert.True(File.Exists(cachedZip), "a failed install must keep the download cached");

        // Retry after clearing the blocker: no second download, and the cache is dropped.
        File.Delete(blocker);
        client.DownloadedUrls.Clear();

        var ok = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.True(ok.Success, ok.ErrorMessage);
        Assert.DoesNotContain("fake://zip", client.DownloadedUrls);
        Assert.False(File.Exists(cachedZip), "a successful install must not leave the zip behind");
    }

    [Fact]
    public async Task Prunes_Cached_Zips_From_Other_Versions()
    {
        var (client, _) = SetUpRelease();
        Directory.CreateDirectory(_cacheDir);
        var stale = Path.Combine(_cacheDir, "ClaudeDo-0.0.9-win-x64.zip");
        File.WriteAllText(stale, "old release");

        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.True(result.Success, result.ErrorMessage);
        Assert.False(File.Exists(stale));
    }

    [Fact]
    public async Task Fails_When_ReleaseClient_Returns_Null()
    {
        var client = new FileCopyReleaseClient(new()) { Release = null };
        var step = new DownloadAndExtractStep(client, _cacheDir);
        var ctx = new InstallContext { InstallDirectory = _installDir };

        var result = await step.ExecuteAsync(ctx, new Progress<string>(_ => { }), CancellationToken.None);

        Assert.False(result.Success);
    }
}
