// Port of Go server/ocsp_test.go — OCSP peer reject and chain validation advisory tests.
// Go reference: golang/nats-server/server/ocsp.go — postOCSPPeerRejectEvent,
//               OCSP chain validation advisory publishing (Gap 10.10).
//
// Tests cover: OcspPeerRejectEventMsg, OcspChainValidationEvent, OcspEventBuilder,
// OcspStatus enum, subject constants, and JSON serialisation round-trip.

using System.Text.Json;
using NATS.Server.Events;

namespace NATS.Server.Monitoring.Tests.Events;

/// <summary>
/// Tests for <see cref="OcspPeerRejectEventMsg"/>, <see cref="OcspChainValidationEvent"/>,
/// <see cref="OcspEventBuilder"/>, and the <see cref="OcspStatus"/> enum.
/// Go reference: ocsp.go — OCSP peer verification advisory events (Gap 10.10).
/// </summary>
public class OcspEventTests
{
    // ========================================================================
    // OcspPeerRejectEventMsg
    // Go reference: ocsp.go postOCSPPeerRejectEvent
    // ========================================================================

    /// <summary>
    /// EventType constant must match the Go advisory type string.
    /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_peer_reject".
    /// </summary>
    [Fact]
    public void OcspPeerRejectEventMsg_HasCorrectEventType()
    {
        // Go reference: ocsp.go — advisory type constant.
        OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
        var ev = new OcspPeerRejectEventMsg();
        ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
    }

    /// <summary>
    /// Each OcspPeerRejectEventMsg gets its own unique non-empty Id.
    /// Go reference: ocsp.go — nuid.Next() generates a unique event ID per advisory.
    /// </summary>
    [Fact]
    public void OcspPeerRejectEventMsg_GeneratesUniqueId()
    {
        // Go reference: ocsp.go — each advisory is assigned a unique ID.
        var ev1 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
        var ev2 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
        ev1.Id.ShouldNotBeNullOrEmpty();
        ev2.Id.ShouldNotBeNullOrEmpty();
        ev1.Id.ShouldNotBe(ev2.Id);
    }

    // ========================================================================
    // OcspChainValidationEvent
    // Go reference: ocsp.go — OCSP chain validation advisory
    // ========================================================================

    /// <summary>
    /// EventType constant must match the advisory type string.
    /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_chain_validation".
    /// </summary>
    [Fact]
    public void OcspChainValidationEvent_HasCorrectEventType()
    {
        // Go reference: ocsp.go — chain validation advisory type constant.
        OcspChainValidationEvent.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_chain_validation");
        var ev = new OcspChainValidationEvent();
        ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
    }

    /// <summary>
    /// All optional fields on OcspChainValidationEvent can be assigned.
    /// Go reference: ocsp.go — chain validation advisory carries cert metadata.
    /// </summary>
    [Fact]
    public void OcspChainValidationEvent_AllFieldsSettable()
    {
        // Go reference: ocsp.go — OCSP advisory fields: subject, issuer, serial, status.
        var server = new EventServerInfo { Id = "srv1", Name = "test-server" };
        var checkedAt = new DateTime(2026, 2, 25, 12, 0, 0, DateTimeKind.Utc);
        var ev = new OcspChainValidationEvent
        {
            Server = server,
            CertSubject = "CN=leaf.example.com",
            CertIssuer = "CN=Intermediate CA",
            SerialNumber = "0123456789abcdef",
            OcspStatus = "good",
            CheckedAt = checkedAt,
            Error = null,
        };

        ev.Server.ShouldBeSameAs(server);
        ev.CertSubject.ShouldBe("CN=leaf.example.com");
        ev.CertIssuer.ShouldBe("CN=Intermediate CA");
        ev.SerialNumber.ShouldBe("0123456789abcdef");
        ev.OcspStatus.ShouldBe("good");
        ev.CheckedAt.ShouldBe(checkedAt);
        ev.Error.ShouldBeNull();
    }

    // ========================================================================
    // OcspEventBuilder
    // Go reference: ocsp.go — postOCSPPeerRejectEvent, chain validation advisory
    // ========================================================================

    /// <summary>
    /// BuildPeerReject populates all required fields of OcspPeerRejectEventMsg.
    /// Go reference: ocsp.go postOCSPPeerRejectEvent — kind and reason always set.
    /// </summary>
    [Fact]
    public void BuildPeerReject_PopulatesAllFields()
    {
        // Go reference: ocsp.go — peer reject advisory carries kind + reason.
        var ev = OcspEventBuilder.BuildPeerReject(
            serverId: "srv-abc",
            serverName: "my-server",
            kind: "client",
            reason: "certificate revoked");

        ev.Id.ShouldNotBeNullOrEmpty();
        ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
        ev.Server.ShouldNotBeNull();
        ev.Server.Id.ShouldBe("srv-abc");
        ev.Server.Name.ShouldBe("my-server");
        ev.Kind.ShouldBe("client");
        ev.Reason.ShouldBe("certificate revoked");
        ev.Time.ShouldNotBe(default);
    }

    /// <summary>
    /// BuildChainValidation populates all fields of OcspChainValidationEvent.
    /// Go reference: ocsp.go — chain validation advisory carries full cert metadata.
    /// </summary>
    [Fact]
    public void BuildChainValidation_PopulatesAllFields()
    {
        // Go reference: ocsp.go — chain validation advisory fields.
        var before = DateTime.UtcNow;
        var ev = OcspEventBuilder.BuildChainValidation(
            serverId: "srv-xyz",
            serverName: "nats-1",
            certSubject: "CN=client.example.com",
            certIssuer: "CN=Root CA",
            serialNumber: "deadbeef",
            ocspStatus: "good",
            error: null);
        var after = DateTime.UtcNow;

        ev.Id.ShouldNotBeNullOrEmpty();
        ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
        ev.Server.ShouldNotBeNull();
        ev.Server!.Id.ShouldBe("srv-xyz");
        ev.Server.Name.ShouldBe("nats-1");
        ev.CertSubject.ShouldBe("CN=client.example.com");
        ev.CertIssuer.ShouldBe("CN=Root CA");
        ev.SerialNumber.ShouldBe("deadbeef");
        ev.OcspStatus.ShouldBe("good");
        ev.CheckedAt.ShouldNotBeNull();
        ev.CheckedAt!.Value.ShouldBeInRange(before, after);
        ev.Error.ShouldBeNull();
    }

    // ========================================================================
    // OcspStatus enum — ParseStatus
    // Go reference: ocsp.go — ocspStatusGood, ocspStatusRevoked constants
    // ========================================================================

    /// <summary>
    /// ParseStatus("good") returns OcspStatus.Good.
    /// Go reference: ocsp.go — ocspStatusGood = "good".
    /// </summary>
    [Fact]
    public void ParseStatus_Good()
    {
        // Go reference: ocsp.go — ocspStatusGood.
        OcspEventBuilder.ParseStatus("good").ShouldBe(OcspStatus.Good);
        OcspEventBuilder.ParseStatus("Good").ShouldBe(OcspStatus.Good);
        OcspEventBuilder.ParseStatus("GOOD").ShouldBe(OcspStatus.Good);
    }

    /// <summary>
    /// ParseStatus("revoked") returns OcspStatus.Revoked.
    /// Go reference: ocsp.go — ocspStatusRevoked = "revoked".
    /// </summary>
    [Fact]
    public void ParseStatus_Revoked()
    {
        // Go reference: ocsp.go — ocspStatusRevoked.
        OcspEventBuilder.ParseStatus("revoked").ShouldBe(OcspStatus.Revoked);
        OcspEventBuilder.ParseStatus("Revoked").ShouldBe(OcspStatus.Revoked);
        OcspEventBuilder.ParseStatus("REVOKED").ShouldBe(OcspStatus.Revoked);
    }

    /// <summary>
    /// ParseStatus with null or unrecognised string returns OcspStatus.Unknown.
    /// Go reference: ocsp.go — unknown/unrecognised OCSP status treated as unknown.
    /// </summary>
    [Fact]
    public void ParseStatus_Unknown()
    {
        // Go reference: ocsp.go — default case maps to unknown status.
        OcspEventBuilder.ParseStatus(null).ShouldBe(OcspStatus.Unknown);
        OcspEventBuilder.ParseStatus("unknown").ShouldBe(OcspStatus.Unknown);
        OcspEventBuilder.ParseStatus("").ShouldBe(OcspStatus.Unknown);
        OcspEventBuilder.ParseStatus("invalid-status").ShouldBe(OcspStatus.Unknown);
    }

    // ========================================================================
    // JSON serialisation round-trip
    // Go reference: ocsp.go — advisories are published as JSON payloads
    // ========================================================================

    /// <summary>
    /// OcspPeerRejectEventMsg and OcspChainValidationEvent round-trip through JSON
    /// preserving all set fields.
    /// Go reference: ocsp.go — OCSP advisories serialised as JSON before publishing.
    /// </summary>
    [Fact]
    public void OcspEvents_SerializeToJson()
    {
        // Go reference: ocsp.go — advisories are JSON-encoded for publishing.
        var peerReject = OcspEventBuilder.BuildPeerReject(
            serverId: "srv-1",
            serverName: "node-a",
            kind: "route",
            reason: "OCSP status revoked");

        var chainValidation = OcspEventBuilder.BuildChainValidation(
            serverId: "srv-2",
            serverName: "node-b",
            certSubject: "CN=server.nats.io",
            certIssuer: "CN=NATS CA",
            serialNumber: "cafebabe",
            ocspStatus: "revoked",
            error: "certificate has been revoked");

        // Serialise OcspPeerRejectEventMsg
        var rejectJson = JsonSerializer.Serialize(peerReject);
        rejectJson.ShouldContain("io.nats.server.advisory.v1.ocsp_peer_reject");
        rejectJson.ShouldContain("route");
        rejectJson.ShouldContain("OCSP status revoked");

        var rejectDeserialized = JsonSerializer.Deserialize<OcspPeerRejectEventMsg>(rejectJson);
        rejectDeserialized.ShouldNotBeNull();
        rejectDeserialized!.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
        rejectDeserialized.Kind.ShouldBe("route");
        rejectDeserialized.Reason.ShouldBe("OCSP status revoked");
        rejectDeserialized.Server.Id.ShouldBe("srv-1");
        rejectDeserialized.Server.Name.ShouldBe("node-a");

        // Serialise OcspChainValidationEvent
        var chainJson = JsonSerializer.Serialize(chainValidation);
        chainJson.ShouldContain("io.nats.server.advisory.v1.ocsp_chain_validation");
        chainJson.ShouldContain("CN=server.nats.io");
        chainJson.ShouldContain("revoked");

        var chainDeserialized = JsonSerializer.Deserialize<OcspChainValidationEvent>(chainJson);
        chainDeserialized.ShouldNotBeNull();
        chainDeserialized!.Type.ShouldBe(OcspChainValidationEvent.EventType);
        chainDeserialized.CertSubject.ShouldBe("CN=server.nats.io");
        chainDeserialized.CertIssuer.ShouldBe("CN=NATS CA");
        chainDeserialized.SerialNumber.ShouldBe("cafebabe");
        chainDeserialized.OcspStatus.ShouldBe("revoked");
        chainDeserialized.Error.ShouldBe("certificate has been revoked");
    }
}
