using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using ServerMonitorManager_Desktop;
using Xunit;

namespace ServerMonitorManager.Desktop.Security.Tests;

[Trait("Category", "LiveRelease")]
public sealed class LiveReleaseUpdateVerificationTests : IAsyncDisposable
{
    private const string DefaultReleaseTag = "v0.1.0-alpha.14";
    private const string Repository = "ochenstarik-ui/server-monitor-manager";
    private readonly string _tempDir;
    private readonly HttpClient _http;
    private readonly string _tag;

    public LiveReleaseUpdateVerificationTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), $"smm-live-release-tests-{Guid.NewGuid():N}");
        Directory.CreateDirectory(_tempDir);
        _http = new HttpClient();
        _http.DefaultRequestHeaders.Add("User-Agent", "ServerMonitorManager.Desktop.Tests");
        _tag = Environment.GetEnvironmentVariable("SMM_TEST_RELEASE_TAG") ?? DefaultReleaseTag;
    }

    private async Task<(string ManifestPath, string SigPath, string PemPath)> DownloadReleaseArtifactsAsync()
    {
        var manifestUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.json";
        var sigUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.sig";
        var pemUrl = $"https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.pem";

        var manifestPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.json");
        var sigPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.sig");
        var pemPath = Path.Combine(_tempDir, "server-monitor-manager-manifest.pem");

        var manifestBytes = await _http.GetByteArrayAsync(manifestUrl, TestContext.Current.CancellationToken);
        var sigBytes = await _http.GetByteArrayAsync(sigUrl, TestContext.Current.CancellationToken);
        var pemBytes = await _http.GetByteArrayAsync(pemUrl, TestContext.Current.CancellationToken);

        await File.WriteAllBytesAsync(manifestPath, manifestBytes, TestContext.Current.CancellationToken);
        await File.WriteAllBytesAsync(sigPath, sigBytes, TestContext.Current.CancellationToken);
        await File.WriteAllBytesAsync(pemPath, pemBytes, TestContext.Current.CancellationToken);

        return (manifestPath, sigPath, pemPath);
    }

    [Fact]
    public async Task AcceptanceRealReleaseManifestAndSignatureAreAccepted()
    {
        var (manifestPath, sigPath, pemPath) = await DownloadReleaseArtifactsAsync();

        var fileStorage = new TestDirectoryFileStorage(_tempDir);
        var httpTransport = new DefaultHttpTransport();
        var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);

        // 1. ProcessSignatureVerifier directly verifies real release material
        await verifier.VerifySignatureAsync(sigPath, manifestPath, pemPath, TestContext.Current.CancellationToken);

        // 2. Parse and verify manifest content
        var manifestJson = await File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
        var node = JsonNode.Parse(manifestJson);
        Assert.NotNull(node);
        Assert.Equal(_tag, node["version"]?.GetValue<string>());

        var msixHash = node["hashes"]?["ServerMonitorManager-win-x64.msix"]?.GetValue<string>();
        Assert.False(string.IsNullOrWhiteSpace(msixHash));
        Assert.Equal(64, msixHash.Length);

        // 3. UpdateService end-to-end against real release material
        var mockHttp = new MockHttpTransport
        {
            GetStringAsyncFunc = url =>
            {
                if (url.EndsWith("releases/latest", StringComparison.OrdinalIgnoreCase))
                {
                    return Task.FromResult($@"{{
                        ""tag_name"": ""{_tag}"",
                        ""assets"": [
                            {{ ""name"": ""server-monitor-manager-manifest.json"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.json"" }},
                            {{ ""name"": ""server-monitor-manager-manifest.sig"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.sig"" }},
                            {{ ""name"": ""server-monitor-manager-manifest.pem"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/server-monitor-manager-manifest.pem"" }},
                            {{ ""name"": ""ServerMonitorManager-win-x64.msix"", ""browser_download_url"": ""https://github.com/{Repository}/releases/download/{_tag}/ServerMonitorManager-win-x64.msix"" }}
                        ]
                    }}");
                }
                if (url.EndsWith("server-monitor-manager-manifest.json", StringComparison.OrdinalIgnoreCase))
                {
                    return File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
                }
                if (url.EndsWith("server-monitor-manager-manifest.sig", StringComparison.OrdinalIgnoreCase))
                {
                    return File.ReadAllTextAsync(sigPath, TestContext.Current.CancellationToken);
                }
                if (url.EndsWith("server-monitor-manager-manifest.pem", StringComparison.OrdinalIgnoreCase))
                {
                    return File.ReadAllTextAsync(pemPath, TestContext.Current.CancellationToken);
                }
                throw new InvalidOperationException($"Unexpected URL: {url}");
            }
        };

        var service = new UpdateService(mockHttp, verifier, fileStorage);
        var updateInfo = await service.CheckForUpdatesAsync(cancellationToken: TestContext.Current.CancellationToken);

        Assert.NotNull(updateInfo);
        Assert.Equal(_tag, updateInfo.Version);
        Assert.Equal(msixHash, updateInfo.ExpectedHash);
        Assert.Contains(_tag, updateInfo.DownloadUrl, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Negative1TamperedHashInRealManifestIsRejected()
    {
        var (manifestPath, sigPath, pemPath) = await DownloadReleaseArtifactsAsync();

        // Alter manifest content by tampering with the hash
        var originalManifest = await File.ReadAllTextAsync(manifestPath, TestContext.Current.CancellationToken);
        var node = JsonNode.Parse(originalManifest);
        Assert.NotNull(node);
        if (node["hashes"]?["ServerMonitorManager-win-x64.msix"] is not null)
        {
            node["hashes"]!["ServerMonitorManager-win-x64.msix"] = "0000000000000000000000000000000000000000000000000000000000000000";
        }
        else
        {
            node["hashes"] = new JsonObject
            {
                ["ServerMonitorManager-win-x64.msix"] = "0000000000000000000000000000000000000000000000000000000000000000"
            };
        }
        var tamperedManifest = node.ToJsonString();

        var tamperedManifestPath = Path.Combine(_tempDir, "tampered-manifest.json");
        await File.WriteAllTextAsync(tamperedManifestPath, tamperedManifest, TestContext.Current.CancellationToken);

        var fileStorage = new TestDirectoryFileStorage(_tempDir);
        var httpTransport = new DefaultHttpTransport();
        var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);

        // Verification of tampered manifest with real signature must fail
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            verifier.VerifySignatureAsync(sigPath, tamperedManifestPath, pemPath, TestContext.Current.CancellationToken));
        Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Negative2RealManifestWithSignatureFromDifferentIdentityIsRejected()
    {
        var (manifestPath, _, pemPath) = await DownloadReleaseArtifactsAsync();

        // Create a fake signature signed by a different key
        using var rsa = RSA.Create();
        var fakeSigBytes = rsa.SignData(
            await File.ReadAllBytesAsync(manifestPath, TestContext.Current.CancellationToken),
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        var fakeSigPath = Path.Combine(_tempDir, "fake-identity.sig");
        await File.WriteAllTextAsync(fakeSigPath, Convert.ToBase64String(fakeSigBytes), TestContext.Current.CancellationToken);

        var fileStorage = new TestDirectoryFileStorage(_tempDir);
        var httpTransport = new DefaultHttpTransport();
        var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            verifier.VerifySignatureAsync(fakeSigPath, manifestPath, pemPath, TestContext.Current.CancellationToken));
        Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Negative3MissingCertificateIsRejected()
    {
        var (manifestPath, sigPath, _) = await DownloadReleaseArtifactsAsync();
        var nonExistentPemPath = Path.Combine(_tempDir, "non-existent-certificate.pem");

        var fileStorage = new TestDirectoryFileStorage(_tempDir);
        var httpTransport = new DefaultHttpTransport();
        var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            verifier.VerifySignatureAsync(sigPath, manifestPath, nonExistentPemPath, TestContext.Current.CancellationToken));
        Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Negative4CertificateFromDifferentWorkflowIsRejected()
    {
        var (manifestPath, sigPath, _) = await DownloadReleaseArtifactsAsync();

        // Create a custom self-signed certificate with a different subject/workflow identity
        using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var req = new CertificateRequest(
            "CN=Untrusted Workflow Fake Cert",
            ecdsa,
            HashAlgorithmName.SHA256);
        using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1));
        var fakePemPath = Path.Combine(_tempDir, "fake-workflow-cert.pem");
        await File.WriteAllTextAsync(fakePemPath, cert.ExportCertificatePem(), TestContext.Current.CancellationToken);

        var fileStorage = new TestDirectoryFileStorage(_tempDir);
        var httpTransport = new DefaultHttpTransport();
        var verifier = new ProcessSignatureVerifier(fileStorage, httpTransport);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            verifier.VerifySignatureAsync(sigPath, manifestPath, fakePemPath, TestContext.Current.CancellationToken));
        Assert.Contains("Signature verification failed", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    public async ValueTask DisposeAsync()
    {
        _http.Dispose();
        try
        {
            if (Directory.Exists(_tempDir))
            {
                Directory.Delete(_tempDir, recursive: true);
            }
        }
        catch
        {
            // Best effort cleanup
        }
        await Task.CompletedTask;
    }

    private sealed class TestDirectoryFileStorage(string directory) : IFileStorage
    {
        public string GetTempFolder() => directory;
        public bool FileExists(string path) => File.Exists(path);
        public Task WriteAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default) =>
            File.WriteAllBytesAsync(path, bytes, cancellationToken);
        public Task WriteAllTextAsync(string path, string text, System.Threading.CancellationToken cancellationToken = default) =>
            File.WriteAllTextAsync(path, text, cancellationToken);
        public Stream OpenRead(string path) => File.OpenRead(path);
        public void LaunchFile(string path) { }
    }
}
