using System.Net.Http;

namespace ClaudeDo.Releases.Tests;

public class SelfUpdaterAssetMatchingTests
{
    [Fact]
    public void FindInstallerAsset_PicksInstallerExeByPattern()
    {
        var assets = new[]
        {
            new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "https://x/app.zip", 10),
            new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst.exe", 20),
            new ReleaseAsset("checksums.txt", "https://x/checks", 1),
        };

        var result = SelfUpdater.FindInstallerAsset(assets);

        Assert.NotNull(result);
        Assert.Equal("ClaudeDo.Installer-0.3.0.exe", result!.Asset.Name);
        Assert.Equal("0.3.0", result.Version);
    }

    [Fact]
    public void FindInstallerAsset_ReturnsNullWhenAbsent()
    {
        var assets = new[]
        {
            new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "https://x/app.zip", 10),
        };

        Assert.Null(SelfUpdater.FindInstallerAsset(assets));
    }

    [Fact]
    public void FindInstallerAsset_IgnoresAppZipThatContainsInstaller()
    {
        var assets = new[]
        {
            new ReleaseAsset("ClaudeDo.Installer.Portable-0.3.0.zip", "https://x/1", 1),
            new ReleaseAsset("not-the-installer.exe", "https://x/2", 1),
        };

        Assert.Null(SelfUpdater.FindInstallerAsset(assets));
    }
}

public class SelfUpdaterDecisionTests
{
    private sealed class FakeReleaseClient : IReleaseClient
    {
        public GiteaRelease? Release { get; set; }
        public bool Throw { get; set; }

        public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct)
        {
            if (Throw) throw new HttpRequestException("boom");
            return Task.FromResult(Release);
        }

        public Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
            => throw new NotSupportedException("not used in decision tests");
    }

    [Fact]
    public async Task Decide_NoRelease_NoUpdate()
    {
        var client = new FakeReleaseClient { Release = null };
        var d = await SelfUpdater.DecideUpdateAsync(client, currentVersion: "0.1.0", CancellationToken.None);
        Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
    }

    [Fact]
    public async Task Decide_NetworkError_NoUpdate()
    {
        var client = new FakeReleaseClient { Throw = true };
        var d = await SelfUpdater.DecideUpdateAsync(client, "0.1.0", CancellationToken.None);
        Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
    }

    [Fact]
    public async Task Decide_OlderLatest_NoUpdate()
    {
        var client = new FakeReleaseClient
        {
            Release = new GiteaRelease("v0.1.0", "rel", new[]
            {
                new ReleaseAsset("ClaudeDo.Installer-0.1.0.exe", "u", 1),
            }),
        };
        var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
        Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
    }

    [Fact]
    public async Task Decide_NewerLatestWithAsset_UpdateAvailable()
    {
        var client = new FakeReleaseClient
        {
            Release = new GiteaRelease("v0.3.0", "rel", new[]
            {
                new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x", 20),
                new ReleaseAsset("checksums.txt", "https://checks", 1),
            }),
        };
        var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
        Assert.Equal(SelfUpdateDecisionKind.UpdateAvailable, d.Kind);
        Assert.Equal("0.3.0", d.LatestVersion);
        Assert.NotNull(d.InstallerAsset);
        Assert.NotNull(d.ChecksumsAsset);
    }

    [Fact]
    public async Task Decide_NewerLatestButNoInstallerAsset_NoUpdate()
    {
        var client = new FakeReleaseClient
        {
            Release = new GiteaRelease("v0.3.0", "rel", new[]
            {
                new ReleaseAsset("ClaudeDo-0.3.0-win-x64.zip", "u", 20),
            }),
        };
        var d = await SelfUpdater.DecideUpdateAsync(client, "0.2.0", CancellationToken.None);
        Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
    }
}

public class SelfUpdaterReplaceSelfTests : IDisposable
{
    private readonly string _tempDir;

    public SelfUpdaterReplaceSelfTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(_tempDir);
    }

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

    [Fact]
    public async Task Replace_DeletesOldAndCopiesCurrent()
    {
        var oldPath = Path.Combine(_tempDir, "old.exe");
        var currentPath = Path.Combine(_tempDir, "current.exe");
        await File.WriteAllTextAsync(oldPath, "OLD");
        await File.WriteAllTextAsync(currentPath, "NEW");

        var relaunchedWith = "";
        var result = await SelfUpdater.HandleReplaceSelfAsync(
            oldPath: oldPath,
            currentExePath: currentPath,
            launchProcess: path => { relaunchedWith = path; return true; },
            maxWaitMs: 500);

        Assert.True(result);
        Assert.Equal(oldPath, relaunchedWith);
        Assert.Equal("NEW", await File.ReadAllTextAsync(oldPath));
    }

    [Fact]
    public async Task Replace_TimesOutWhenFileStaysLocked_ReturnsFalse()
    {
        var oldPath = Path.Combine(_tempDir, "locked.exe");
        var currentPath = Path.Combine(_tempDir, "current.exe");
        await File.WriteAllTextAsync(oldPath, "OLD");
        await File.WriteAllTextAsync(currentPath, "NEW");

        // Hold an exclusive lock across the wait window.
        using var lockStream = new FileStream(oldPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);

        var result = await SelfUpdater.HandleReplaceSelfAsync(
            oldPath: oldPath,
            currentExePath: currentPath,
            launchProcess: _ => true,
            maxWaitMs: 200);

        Assert.False(result);
    }
}

public class SelfUpdaterDownloadTests : IDisposable
{
    private readonly string _tempDir;

    public SelfUpdaterDownloadTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
        Directory.CreateDirectory(_tempDir);
    }

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

    private sealed class StubReleaseClient : IReleaseClient
    {
        public string FileContent { get; set; } = "";
        public string ChecksumsBody { get; set; } = "";

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

        public async Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
        {
            if (url.EndsWith("checksums.txt", StringComparison.OrdinalIgnoreCase))
            {
                await File.WriteAllTextAsync(destPath, ChecksumsBody, ct);
            }
            else
            {
                await File.WriteAllTextAsync(destPath, FileContent, ct);
            }
            progress.Report(FileContent.Length);
        }
    }

    [Fact]
    public async Task Download_MatchingChecksum_ReturnsPath()
    {
        var content = "FAKE-INSTALLER-BINARY";
        var hash = Sha256Hex(content);
        var client = new StubReleaseClient
        {
            FileContent = content,
            ChecksumsBody = $"{hash}  ClaudeDo.Installer-0.3.0.exe\n",
        };
        var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", content.Length);
        var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);

        var path = await SelfUpdater.DownloadAndVerifyAsync(
            client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);

        Assert.NotNull(path);
        Assert.Equal(content, await File.ReadAllTextAsync(path!));
    }

    [Fact]
    public async Task Download_ChecksumMismatch_ReturnsNull()
    {
        var client = new StubReleaseClient
        {
            FileContent = "real",
            ChecksumsBody = "deadbeef" + new string('0', 56) + "  ClaudeDo.Installer-0.3.0.exe\n",
        };
        var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", 4);
        var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);

        var path = await SelfUpdater.DownloadAndVerifyAsync(
            client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);

        Assert.Null(path);
    }

    private static string Sha256Hex(string s)
    {
        using var sha = System.Security.Cryptography.SHA256.Create();
        return Convert.ToHexString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s))).ToLowerInvariant();
    }
}
