using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Certificates;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Certificates;

public sealed class CertificateStoreManagerTests : IDisposable
{
    private readonly string _root;
    private readonly RecordingAuditWriter _audit;
    private readonly CertificateStoreManager _sut;

    public CertificateStoreManagerTests()
    {
        _root = Path.Combine(Path.GetTempPath(), $"pki-test-{Guid.NewGuid():N}");
        _audit = new RecordingAuditWriter();
        _sut = new CertificateStoreManager(_root, _audit);
    }

    // ── Recording audit-writer fake ───────────────────────────────────────────

    private sealed class RecordingAuditWriter : ZB.MOM.WW.Audit.IAuditWriter
    {
        public List<ZB.MOM.WW.Audit.AuditEvent> Events { get; } = new();

        public Task WriteAsync(ZB.MOM.WW.Audit.AuditEvent evt, CancellationToken ct = default)
        {
            Events.Add(evt);
            return Task.CompletedTask;
        }
    }

    public void Dispose()
    {
        if (Directory.Exists(_root))
            Directory.Delete(_root, recursive: true);
    }

    // ── Seed helper ──────────────────────────────────────────────────────────

    private static string SeedCert(string storeCertsDir, string cn)
    {
        Directory.CreateDirectory(storeCertsDir);
        using var rsa = RSA.Create(2048);
        var req = new CertificateRequest(
            $"CN={cn}", rsa, HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        using var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
        var file = Path.Combine(storeCertsDir, $"{cn} [{cert.Thumbprint}].der");
        File.WriteAllBytes(file, cert.Export(X509ContentType.Cert));
        return cert.Thumbprint!;
    }

    // ── Assertion helpers ─────────────────────────────────────────────────────

    private static bool ThumbprintExistsInDir(string certsDir, string thumbprint)
    {
        if (!Directory.Exists(certsDir)) return false;
        string[] exts = [".der", ".cer", ".crt"];
        foreach (var file in Directory.EnumerateFiles(certsDir))
        {
            if (!exts.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) continue;
            try
            {
                using var cert = X509CertificateLoader.LoadCertificateFromFile(file);
                if (string.Equals(cert.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase))
                    return true;
            }
            catch { /* ignore */ }
        }
        return false;
    }

    private static int CertCountInDir(string certsDir)
    {
        if (!Directory.Exists(certsDir)) return 0;
        string[] exts = [".der", ".cer", ".crt"];
        int count = 0;
        foreach (var file in Directory.EnumerateFiles(certsDir))
        {
            if (!exts.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) continue;
            try
            {
                using var cert = X509CertificateLoader.LoadCertificateFromFile(file);
                count++;
            }
            catch { /* ignore */ }
        }
        return count;
    }

    // ── Tests ─────────────────────────────────────────────────────────────────

    [Fact]
    public void Trust_MovesRejectedToTrusted()
    {
        var rejectedCerts = Path.Combine(_root, "rejected", "certs");
        var tp = SeedCert(rejectedCerts, "TestClient1");

        var result = _sut.Trust(tp, "tester");

        result.Success.ShouldBeTrue(result.Error);
        ThumbprintExistsInDir(rejectedCerts, tp).ShouldBeFalse("cert should have left rejected");
        ThumbprintExistsInDir(Path.Combine(_root, "trusted", "certs"), tp).ShouldBeTrue("cert should be in trusted");
    }

    [Fact]
    public void Untrust_MovesTrustedToRejected()
    {
        var trustedCerts = Path.Combine(_root, "trusted", "certs");
        var tp = SeedCert(trustedCerts, "TestClient2");

        var result = _sut.Untrust(tp, "tester");

        result.Success.ShouldBeTrue(result.Error);
        ThumbprintExistsInDir(trustedCerts, tp).ShouldBeFalse("cert should have left trusted");
        ThumbprintExistsInDir(Path.Combine(_root, "rejected", "certs"), tp).ShouldBeTrue("cert should be in rejected");
    }

    [Fact]
    public void Delete_Rejected_RemovesFile()
    {
        var rejectedCerts = Path.Combine(_root, "rejected", "certs");
        var tp = SeedCert(rejectedCerts, "TestClient3");

        var result = _sut.Delete("rejected", tp, "tester");

        result.Success.ShouldBeTrue(result.Error);
        ThumbprintExistsInDir(rejectedCerts, tp).ShouldBeFalse("cert should be gone from rejected");
    }

    [Fact]
    public void Delete_Trusted_RemovesFile()
    {
        var trustedCerts = Path.Combine(_root, "trusted", "certs");
        var tp = SeedCert(trustedCerts, "TestClient4");

        var result = _sut.Delete("trusted", tp, "tester");

        result.Success.ShouldBeTrue(result.Error);
        ThumbprintExistsInDir(trustedCerts, tp).ShouldBeFalse("cert should be gone from trusted");
    }

    [Fact]
    public void Trust_UnknownThumbprint_FailsNoChange()
    {
        var tp = new string('A', 40);

        var result = _sut.Trust(tp, "tester");

        result.Success.ShouldBeFalse();
        CertCountInDir(Path.Combine(_root, "trusted", "certs")).ShouldBe(0);
    }

    [Fact]
    public void Trust_PathTraversalThumbprint_Rejected()
    {
        var result = _sut.Trust("../../x", "tester");

        result.Success.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("invalid", Case.Insensitive);
        // Verify no files were created anywhere under root
        if (Directory.Exists(_root))
            Directory.GetFiles(_root, "*", SearchOption.AllDirectories).ShouldBeEmpty();
    }

    [Fact]
    public void Delete_DisallowedStore_Fails()
    {
        var ownCerts = Path.Combine(_root, "own", "certs");
        var tp = SeedCert(ownCerts, "OwnCert");

        var result = _sut.Delete("own", tp, "tester");

        result.Success.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error.ShouldContain("store", Case.Insensitive);
        ThumbprintExistsInDir(ownCerts, tp).ShouldBeTrue("own cert should still be present");
    }

    [Fact]
    public void Trust_AlreadyInDest_IdempotentSuccess()
    {
        var rejectedCerts = Path.Combine(_root, "rejected", "certs");
        var trustedCerts = Path.Combine(_root, "trusted", "certs");

        // Seed into rejected, then copy that exact file into trusted so both have identical bytes/thumbprint
        var tp = SeedCert(rejectedCerts, "TestClient8");
        Directory.CreateDirectory(trustedCerts);
        var srcFile = Directory.GetFiles(rejectedCerts).Single();
        File.Copy(srcFile, Path.Combine(trustedCerts, Path.GetFileName(srcFile)));

        var result = _sut.Trust(tp, "tester");

        result.Success.ShouldBeTrue(result.Error);
        ThumbprintExistsInDir(rejectedCerts, tp).ShouldBeFalse("source (rejected) should no longer have it");
        CertCountInDir(trustedCerts).ShouldBe(1, "trusted should still have exactly one copy");
    }

    [Fact]
    public void Trust_NullOrEmptyThumbprint_Fails()
    {
        var result = _sut.Trust("", "tester");

        result.Success.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
    }

    // ── Audit tests ───────────────────────────────────────────────────────────

    [Fact]
    public void Trust_success_writes_one_success_audit_event()
    {
        var rejectedCerts = Path.Combine(_root, "rejected", "certs");
        var tp = SeedCert(rejectedCerts, "AuditTrust");

        var result = _sut.Trust(tp, "alice");

        result.Success.ShouldBeTrue(result.Error);
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Trust");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Success);
        evt.SourceNode.ShouldBe(tp);
        evt.Actor.ShouldBe("alice");
    }

    [Fact]
    public void Untrust_success_writes_one_success_audit_event()
    {
        var trustedCerts = Path.Combine(_root, "trusted", "certs");
        var tp = SeedCert(trustedCerts, "AuditUntrust");

        var result = _sut.Untrust(tp, "alice");

        result.Success.ShouldBeTrue(result.Error);
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Untrust");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Success);
        evt.SourceNode.ShouldBe(tp);
        evt.Actor.ShouldBe("alice");
    }

    [Fact]
    public void Delete_success_writes_one_success_audit_event()
    {
        var rejectedCerts = Path.Combine(_root, "rejected", "certs");
        var tp = SeedCert(rejectedCerts, "AuditDelete");

        var result = _sut.Delete("rejected", tp, "alice");

        result.Success.ShouldBeTrue(result.Error);
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Delete");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Success);
        evt.SourceNode.ShouldBe(tp);
        evt.Actor.ShouldBe("alice");
    }

    [Fact]
    public void Delete_not_found_writes_one_failure_audit_event()
    {
        var absent = new string('0', 40);

        var result = _sut.Delete("trusted", absent, "bob");

        result.Success.ShouldBeFalse();
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Delete");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Failure);
        evt.SourceNode.ShouldBe(absent);
        evt.Actor.ShouldBe("bob");
    }

    [Fact]
    public void Trust_not_found_writes_one_failure_audit_event()
    {
        var absent = new string('0', 40);

        var result = _sut.Trust(absent, "bob");

        result.Success.ShouldBeFalse();
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Trust");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Failure);
        evt.SourceNode.ShouldBe(absent);
        evt.Actor.ShouldBe("bob");
    }

    [Fact]
    public void Untrust_not_found_writes_one_failure_audit_event()
    {
        var absent = new string('0', 40);

        var result = _sut.Untrust(absent, "erin");

        result.Success.ShouldBeFalse();
        var evt = _audit.Events.ShouldHaveSingleItem();
        evt.Action.ShouldBe("Untrust");
        evt.Outcome.ShouldBe(ZB.MOM.WW.Audit.AuditOutcome.Failure);
        evt.SourceNode.ShouldBe(absent);
        evt.Actor.ShouldBe("erin");
    }
}
