using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Admin.Services;

namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;

[Trait("Category", "Unit")]
public sealed class CertTrustServiceTests : IDisposable
{
    private readonly string _root;

    public CertTrustServiceTests()
    {
        _root = Path.Combine(Path.GetTempPath(), $"otopcua-cert-test-{Guid.NewGuid():N}");
        Directory.CreateDirectory(Path.Combine(_root, "rejected", "certs"));
        Directory.CreateDirectory(Path.Combine(_root, "trusted", "certs"));
    }

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

    private CertTrustService Service() => new(
        Options.Create(new CertTrustOptions { PkiStoreRoot = _root }),
        NullLogger<CertTrustService>.Instance);

    private X509Certificate2 WriteTestCert(CertStoreKind kind, string subject)
    {
        using var rsa = RSA.Create(2048);
        var req = new CertificateRequest($"CN={subject}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1));
        var dir = Path.Combine(_root, kind == CertStoreKind.Rejected ? "rejected" : "trusted", "certs");
        var path = Path.Combine(dir, $"{subject} [{cert.Thumbprint}].der");
        File.WriteAllBytes(path, cert.Export(X509ContentType.Cert));
        return cert;
    }

    [Fact]
    public void ListRejected_returns_parsed_cert_info_for_each_der_in_rejected_certs_dir()
    {
        var c = WriteTestCert(CertStoreKind.Rejected, "test-client-A");

        var rows = Service().ListRejected();

        rows.Count.ShouldBe(1);
        rows[0].Thumbprint.ShouldBe(c.Thumbprint);
        rows[0].Subject.ShouldContain("test-client-A");
        rows[0].Store.ShouldBe(CertStoreKind.Rejected);
    }

    [Fact]
    public void ListTrusted_is_separate_from_rejected()
    {
        WriteTestCert(CertStoreKind.Rejected, "rej");
        WriteTestCert(CertStoreKind.Trusted, "trust");

        var svc = Service();
        svc.ListRejected().Count.ShouldBe(1);
        svc.ListTrusted().Count.ShouldBe(1);
        svc.ListRejected()[0].Subject.ShouldContain("rej");
        svc.ListTrusted()[0].Subject.ShouldContain("trust");
    }

    [Fact]
    public void TrustRejected_moves_file_from_rejected_to_trusted()
    {
        var c = WriteTestCert(CertStoreKind.Rejected, "promoteme");
        var svc = Service();

        svc.TrustRejected(c.Thumbprint).ShouldBeTrue();

        svc.ListRejected().ShouldBeEmpty();
        var trusted = svc.ListTrusted();
        trusted.Count.ShouldBe(1);
        trusted[0].Thumbprint.ShouldBe(c.Thumbprint);
    }

    [Fact]
    public void TrustRejected_returns_false_when_thumbprint_not_in_rejected()
    {
        var svc = Service();
        svc.TrustRejected("00DEADBEEF00DEADBEEF00DEADBEEF00DEADBEEF").ShouldBeFalse();
    }

    [Fact]
    public void DeleteRejected_removes_the_file()
    {
        var c = WriteTestCert(CertStoreKind.Rejected, "killme");
        var svc = Service();

        svc.DeleteRejected(c.Thumbprint).ShouldBeTrue();
        svc.ListRejected().ShouldBeEmpty();
    }

    [Fact]
    public void UntrustCert_removes_from_trusted_only()
    {
        var c = WriteTestCert(CertStoreKind.Trusted, "revoke");
        var svc = Service();

        svc.UntrustCert(c.Thumbprint).ShouldBeTrue();
        svc.ListTrusted().ShouldBeEmpty();
    }

    [Fact]
    public void Thumbprint_match_is_case_insensitive()
    {
        var c = WriteTestCert(CertStoreKind.Rejected, "case");
        var svc = Service();

        // X509Certificate2.Thumbprint is upper-case hex; operators pasting from logs often
        // lowercase it. IsAllowed-style case-insensitive match keeps the UX forgiving.
        svc.TrustRejected(c.Thumbprint.ToLowerInvariant()).ShouldBeTrue();
    }

    [Fact]
    public void Missing_store_directories_produce_empty_lists_not_exceptions()
    {
        // Fresh root with no certs subfolder — service should tolerate a pristine install.
        var altRoot = Path.Combine(Path.GetTempPath(), $"otopcua-cert-empty-{Guid.NewGuid():N}");
        try
        {
            var svc = new CertTrustService(
                Options.Create(new CertTrustOptions { PkiStoreRoot = altRoot }),
                NullLogger<CertTrustService>.Instance);
            svc.ListRejected().ShouldBeEmpty();
            svc.ListTrusted().ShouldBeEmpty();
        }
        finally
        {
            if (Directory.Exists(altRoot)) Directory.Delete(altRoot, recursive: true);
        }
    }

    [Fact]
    public void Malformed_file_is_skipped_not_fatal()
    {
        // Drop junk bytes that don't parse as a cert into the rejected/certs directory. The
        // service must skip it and still return the valid certs — one bad file can't take the
        // whole management page offline.
        File.WriteAllText(Path.Combine(_root, "rejected", "certs", "junk.der"), "not a cert");
        var c = WriteTestCert(CertStoreKind.Rejected, "valid");

        var rows = Service().ListRejected();
        rows.Count.ShouldBe(1);
        rows[0].Thumbprint.ShouldBe(c.Thumbprint);
    }
}
