using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using FluentAssertions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;

/// <summary>
/// T17 / D6: per-node CertStoreActor write/list/remove round-trip against a
/// temp trusted-peer PKI store. The cluster broadcast (singleton → every node's
/// CertStoreActor) is validated live at integration E1; here we cover the
/// testable per-node core.
/// </summary>
public class CertStoreActorTests : TestKit, IDisposable
{
    private readonly string _trustedDir;
    private readonly string _rejectedDir;
    private readonly OpcUaGlobalOptions _options;

    public CertStoreActorTests()
    {
        var root = Path.Combine(Path.GetTempPath(), "ScadaBridge-CertStoreTests", Guid.NewGuid().ToString("N"));
        _trustedDir = Path.Combine(root, "trusted");
        _rejectedDir = Path.Combine(root, "rejected");
        _options = new OpcUaGlobalOptions
        {
            TrustedPeerStorePath = _trustedDir,
            RejectedCertificateStorePath = _rejectedDir,
        };
    }

    void IDisposable.Dispose()
    {
        Shutdown();
        TryDeleteDir(Path.GetDirectoryName(_trustedDir)!);
    }

    private static void TryDeleteDir(string dir)
    {
        try
        {
            if (Directory.Exists(dir))
            {
                Directory.Delete(dir, recursive: true);
            }
        }
        catch
        {
            // Best-effort temp cleanup.
        }
    }

    private static (string DerBase64, string Thumbprint) BuildSelfSignedCert()
    {
        using var rsa = RSA.Create(2048);
        var req = new CertificateRequest(
            "CN=ScadaBridge-CertStoreTest",
            rsa,
            HashAlgorithmName.SHA256,
            RSASignaturePadding.Pkcs1);
        using var cert = req.CreateSelfSigned(
            DateTimeOffset.UtcNow.AddDays(-1),
            DateTimeOffset.UtcNow.AddDays(365));
        var der = cert.Export(X509ContentType.Cert);
        return (Convert.ToBase64String(der), cert.Thumbprint);
    }

    [Fact]
    public void Write_ThenList_ThenRemove_RoundTrips()
    {
        var (derBase64, thumbprint) = BuildSelfSignedCert();
        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        // Write
        actor.Tell(new WriteCertToLocalStore(derBase64, thumbprint));
        var writeAck = ExpectMsg<LocalCertOpAck>();
        writeAck.Success.Should().BeTrue();
        writeAck.Error.Should().BeNull();

        var expectedFile = Path.Combine(_trustedDir, $"{thumbprint}.der");
        File.Exists(expectedFile).Should().BeTrue("the cert is written into the trusted-peer store");

        // List — the cert appears, not rejected
        actor.Tell(new ListLocalCerts());
        var listAck = ExpectMsg<LocalCertOpAck>();
        listAck.Success.Should().BeTrue();
        listAck.Certs.Should().NotBeNull();
        listAck.Certs!.Should().ContainSingle(c => c.Thumbprint == thumbprint)
            .Which.Rejected.Should().BeFalse();

        // Remove — ack + file gone
        actor.Tell(new RemoveCertFromLocalStore(thumbprint));
        var removeAck = ExpectMsg<LocalCertOpAck>();
        removeAck.Success.Should().BeTrue();
        File.Exists(expectedFile).Should().BeFalse("the cert file is deleted from the trusted-peer store");

        // List again — empty
        actor.Tell(new ListLocalCerts());
        var emptyAck = ExpectMsg<LocalCertOpAck>();
        emptyAck.Success.Should().BeTrue();
        emptyAck.Certs.Should().NotBeNull();
        emptyAck.Certs!.Should().NotContain(c => c.Thumbprint == thumbprint);
    }

    [Fact]
    public void ExportLocalTrustedCerts_ReturnsThumbprintAndDerBytes()
    {
        var (derBase64, thumbprint) = BuildSelfSignedCert();
        var expectedDer = Convert.FromBase64String(derBase64);
        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        // Seed one trusted cert via the normal write path.
        actor.Tell(new WriteCertToLocalStore(derBase64, thumbprint));
        ExpectMsg<LocalCertOpAck>().Success.Should().BeTrue();

        // Export — thumbprint + raw DER bytes come back for reconciliation.
        actor.Tell(new ExportLocalTrustedCerts());
        var export = ExpectMsg<LocalCertExport>();
        export.Success.Should().BeTrue();
        export.Error.Should().BeNull();
        var entry = export.Certs.Should().ContainSingle().Subject;
        entry.Thumbprint.Should().Be(thumbprint);
        entry.DerBytes.Should().Equal(expectedDer);
    }

    [Fact]
    public void Write_InvalidBase64_ReturnsFailureAck()
    {
        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        actor.Tell(new WriteCertToLocalStore("not-valid-base64!!", "ABC123"));
        var ack = ExpectMsg<LocalCertOpAck>();
        ack.Success.Should().BeFalse();
        ack.Error.Should().NotBeNullOrEmpty();
    }

    [Fact]
    public void Remove_MissingCert_IsNoOpSuccess()
    {
        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        actor.Tell(new RemoveCertFromLocalStore("NONEXISTENTTHUMBPRINT"));
        var ack = ExpectMsg<LocalCertOpAck>();
        ack.Success.Should().BeTrue("removing an absent cert is an idempotent no-op");
    }

    [Fact]
    public void Write_PathTraversalThumbprint_RejectedWithoutTouchingFilesystem()
    {
        var (derBase64, _) = BuildSelfSignedCert();
        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        // A "../escape" thumbprint would otherwise resolve to "<root>/escape.der",
        // OUTSIDE the trusted-store dir, once combined into a ".der" file name.
        actor.Tell(new WriteCertToLocalStore(derBase64, "../escape"));
        var ack = ExpectMsg<LocalCertOpAck>();
        ack.Success.Should().BeFalse("a path-traversal thumbprint must be rejected");
        ack.Error.Should().Be("invalid thumbprint");

        // No file escaped above the trusted-store dir (the shared temp root).
        var escapeRoot = Path.GetDirectoryName(_trustedDir)!;
        File.Exists(Path.Combine(escapeRoot, "escape.der")).Should().BeFalse(
            "the actor must not write outside the trusted-store directory");
    }

    [Fact]
    public void Remove_PathTraversalThumbprint_RejectedWithoutTouchingFilesystem()
    {
        // Seed a decoy file one level up to prove the rejected remove never
        // reaches the filesystem and therefore cannot delete it.
        var escapeRoot = Path.GetDirectoryName(_trustedDir)!;
        Directory.CreateDirectory(escapeRoot);
        var decoy = Path.Combine(escapeRoot, "escape.der");
        File.WriteAllBytes(decoy, new byte[] { 0x01, 0x02, 0x03 });

        var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options)));

        actor.Tell(new RemoveCertFromLocalStore("../escape"));
        var ack = ExpectMsg<LocalCertOpAck>();
        ack.Success.Should().BeFalse("a path-traversal thumbprint must be rejected");
        ack.Error.Should().Be("invalid thumbprint");

        File.Exists(decoy).Should().BeTrue(
            "the actor must not delete files outside the trusted-store directory");
    }
}
