using System.Text;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types.Audit;

/// <summary>
/// C1 codec tests: round-trip, byte-determinism, and null/empty sentinel.
/// Determinism is load-bearing — consumers dedup on canonical AuditEvent value-equality
/// which includes DetailsJson, so two emitters must produce identical bytes for equal inputs.
/// </summary>
public class AuditDetailsCodecTests
{
    // ---------------------------------------------------------------------------
    // Helpers
    // ---------------------------------------------------------------------------

    private static AuditDetails FullDetails() => new()
    {
        Channel            = AuditChannel.ApiOutbound.ToString(),
        Kind               = AuditKind.ApiCall.ToString(),
        Status             = AuditStatus.Delivered.ToString(),
        ExecutionId        = new Guid("11111111-1111-1111-1111-111111111111"),
        ParentExecutionId  = new Guid("22222222-2222-2222-2222-222222222222"),
        SourceSiteId       = "site-a",
        SourceInstanceId   = "inst-001",
        SourceScript       = "MyScript",
        HttpStatus         = 200,
        DurationMs         = 42,
        ErrorMessage       = null,
        ErrorDetail        = null,
        RequestSummary     = "GET /api/data",
        ResponseSummary    = "{\"ok\":true}",
        PayloadTruncated   = false,
        Extra              = null,
        IngestedAtUtc      = new DateTimeOffset(2026, 6, 1, 12, 0, 0, TimeSpan.Zero),
    };

    // ---------------------------------------------------------------------------
    // Round-trip: all non-null fields survive
    // ---------------------------------------------------------------------------

    [Fact]
    public void RoundTrip_AllFields_Populated()
    {
        var original = FullDetails();
        var json = AuditDetailsCodec.Serialize(original);
        var restored = AuditDetailsCodec.Deserialize(json);

        Assert.Equal(original.Channel,           restored.Channel);
        Assert.Equal(original.Kind,              restored.Kind);
        Assert.Equal(original.Status,            restored.Status);
        Assert.Equal(original.ExecutionId,       restored.ExecutionId);
        Assert.Equal(original.ParentExecutionId, restored.ParentExecutionId);
        Assert.Equal(original.SourceSiteId,      restored.SourceSiteId);
        Assert.Equal(original.SourceInstanceId,  restored.SourceInstanceId);
        Assert.Equal(original.SourceScript,      restored.SourceScript);
        Assert.Equal(original.HttpStatus,        restored.HttpStatus);
        Assert.Equal(original.DurationMs,        restored.DurationMs);
        Assert.Equal(original.RequestSummary,    restored.RequestSummary);
        Assert.Equal(original.ResponseSummary,   restored.ResponseSummary);
        Assert.Equal(original.PayloadTruncated,  restored.PayloadTruncated);
        Assert.Equal(original.IngestedAtUtc,     restored.IngestedAtUtc);
    }

    // ---------------------------------------------------------------------------
    // Round-trip: all-null record
    // ---------------------------------------------------------------------------

    [Fact]
    public void RoundTrip_AllNullOptionals_ReturnsEmptyDetails()
    {
        var empty = new AuditDetails();
        var json = AuditDetailsCodec.Serialize(empty);
        var restored = AuditDetailsCodec.Deserialize(json);

        Assert.Null(restored.Channel);
        Assert.Null(restored.Kind);
        Assert.Null(restored.Status);
        Assert.Null(restored.ExecutionId);
        Assert.Null(restored.ParentExecutionId);
        Assert.Null(restored.SourceSiteId);
        Assert.Null(restored.SourceInstanceId);
        Assert.Null(restored.SourceScript);
        Assert.Null(restored.HttpStatus);
        Assert.Null(restored.DurationMs);
        Assert.Null(restored.ErrorMessage);
        Assert.Null(restored.ErrorDetail);
        Assert.Null(restored.RequestSummary);
        Assert.Null(restored.ResponseSummary);
        Assert.False(restored.PayloadTruncated);
        Assert.Null(restored.Extra);
        Assert.Null(restored.IngestedAtUtc);
    }

    // ---------------------------------------------------------------------------
    // Null / empty JSON deserialize → empty AuditDetails (no throw)
    // ---------------------------------------------------------------------------

    [Fact]
    public void Deserialize_Null_ReturnsEmpty()
    {
        var d = AuditDetailsCodec.Deserialize(null);
        Assert.NotNull(d);
        Assert.Null(d.Channel);
    }

    [Fact]
    public void Deserialize_EmptyString_ReturnsEmpty()
    {
        var d = AuditDetailsCodec.Deserialize(string.Empty);
        Assert.NotNull(d);
        Assert.Null(d.Channel);
    }

    // ---------------------------------------------------------------------------
    // Determinism: same input → byte-identical JSON across two calls
    // ---------------------------------------------------------------------------

    [Fact]
    public void Serialize_SameInput_ProducesByteIdenticalOutput_AcrossCalls()
    {
        var details = FullDetails();
        var json1 = AuditDetailsCodec.Serialize(details);
        var json2 = AuditDetailsCodec.Serialize(details);

        Assert.Equal(json1, json2);
        Assert.Equal(
            Encoding.UTF8.GetBytes(json1),
            Encoding.UTF8.GetBytes(json2));
    }

    // ---------------------------------------------------------------------------
    // Determinism: exact expected JSON string (camelCase key order = declaration order,
    // null fields omitted, PayloadTruncated=false present because it is non-nullable bool)
    // ---------------------------------------------------------------------------

    [Fact]
    public void Serialize_KnownInput_MatchesExpectedJson()
    {
        // Build a minimal deterministic fixture (only non-null fields we can pin exactly).
        var details = new AuditDetails
        {
            Channel           = "ApiOutbound",
            Kind              = "ApiCall",
            Status            = "Delivered",
            ExecutionId       = new Guid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
            SourceSiteId      = "site-x",
            HttpStatus        = 200,
            DurationMs        = 10,
            RequestSummary    = "GET /ping",
            ResponseSummary   = "ok",
            PayloadTruncated  = false,
            IngestedAtUtc     = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero),
        };

        // Expected: camelCase, no indent, nulls omitted, PayloadTruncated present (false is default for bool),
        // key order follows property declaration order on AuditDetails.
        // ingestedAtUtc serializes as ISO-8601 with offset.
        const string expected =
            "{\"channel\":\"ApiOutbound\"," +
            "\"kind\":\"ApiCall\"," +
            "\"status\":\"Delivered\"," +
            "\"executionId\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\"," +
            "\"sourceSiteId\":\"site-x\"," +
            "\"httpStatus\":200," +
            "\"durationMs\":10," +
            "\"requestSummary\":\"GET /ping\"," +
            "\"responseSummary\":\"ok\"," +
            "\"payloadTruncated\":false," +
            "\"ingestedAtUtc\":\"2026-01-02T03:04:05+00:00\"}";

        var actual = AuditDetailsCodec.Serialize(details);
        Assert.Equal(expected, actual);
    }

    // ---------------------------------------------------------------------------
    // Redaction marker preserved verbatim (UnsafeRelaxedJsonEscaping must not mangle <>)
    // ---------------------------------------------------------------------------

    [Fact]
    public void Serialize_RedactionMarker_IsPreservedVerbatim()
    {
        var details = new AuditDetails { RequestSummary = "<redacted>" };
        var json = AuditDetailsCodec.Serialize(details);
        Assert.Contains("\"<redacted>\"", json);
    }
}
