using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

namespace ZB.MOM.WW.ScadaBridge.Communication.Tests;

/// <summary>
/// Round-trip + edge tests for the <see cref="AuditEventDtoMapper"/> that bridges
/// the canonical <see cref="AuditEvent"/> ↔ <see cref="AuditEventDto"/> (proto).
/// </summary>
/// <remarks>
/// C3 (Task 2.5): the canonical record carries the ScadaBridge domain fields inside
/// <c>DetailsJson</c>; the proto contract is unchanged (24-field wire). Domain fields
/// are read back as typed properties via <c>AsRow()</c>. <c>ForwardState</c> is
/// site-storage-only (never on the wire) and <c>IngestedAtUtc</c> is central-set
/// (left null by the mapper), so neither survives the proto round-trip.
/// </remarks>
public class AuditEventDtoMapperTests
{
    [Fact]
    public void ToDto_FromDto_Roundtrip_FullyPopulated_PreservesAllFields()
    {
        var occurredAt = new DateTime(2026, 5, 20, 10, 15, 30, 123, DateTimeKind.Utc);
        var ingestedAt = new DateTimeOffset(new DateTime(2026, 5, 20, 10, 15, 31, 0, DateTimeKind.Utc));
        var correlationId = Guid.NewGuid();
        var executionId = Guid.NewGuid();
        var parentExecutionId = Guid.NewGuid();
        var eventId = Guid.NewGuid();

        var original = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: AuditKind.ApiCallCached,
            status: AuditStatus.Forwarded,
            eventId: eventId,
            occurredAtUtc: occurredAt,
            actor: "design-key",
            target: "weather-api",
            sourceNode: "node-a",
            correlationId: correlationId,
            executionId: executionId,
            parentExecutionId: parentExecutionId,
            sourceSiteId: "site-1",
            sourceInstanceId: "Pump01",
            sourceScript: "OnDemand",
            httpStatus: 200,
            durationMs: 42,
            errorMessage: "transient timeout",
            errorDetail: "stack-trace",
            requestSummary: "GET /weather",
            responseSummary: "{ \"ok\": true }",
            payloadTruncated: true,
            extra: "{ \"retryCount\": 1 }",
            ingestedAtUtc: ingestedAt);

        var dto = AuditEventDtoMapper.ToDto(original);
        var roundTripped = AuditEventDtoMapper.FromDto(dto);

        var o = original.AsRow();
        var rt = roundTripped.AsRow();

        Assert.Equal(o.EventId, rt.EventId);
        Assert.Equal(o.OccurredAtUtc, rt.OccurredAtUtc);
        Assert.Equal(o.Channel, rt.Channel);
        Assert.Equal(o.Kind, rt.Kind);
        Assert.Equal(o.CorrelationId, rt.CorrelationId);
        Assert.Equal(o.ExecutionId, rt.ExecutionId);
        Assert.Equal(o.ParentExecutionId, rt.ParentExecutionId);
        Assert.Equal(o.SourceSiteId, rt.SourceSiteId);
        Assert.Equal(o.SourceNode, rt.SourceNode);
        Assert.Equal(o.SourceInstanceId, rt.SourceInstanceId);
        Assert.Equal(o.SourceScript, rt.SourceScript);
        Assert.Equal(o.Actor, rt.Actor);
        Assert.Equal(o.Target, rt.Target);
        Assert.Equal(o.Status, rt.Status);
        Assert.Equal(o.HttpStatus, rt.HttpStatus);
        Assert.Equal(o.DurationMs, rt.DurationMs);
        Assert.Equal(o.ErrorMessage, rt.ErrorMessage);
        Assert.Equal(o.ErrorDetail, rt.ErrorDetail);
        Assert.Equal(o.RequestSummary, rt.RequestSummary);
        Assert.Equal(o.ResponseSummary, rt.ResponseSummary);
        Assert.Equal(o.PayloadTruncated, rt.PayloadTruncated);
        Assert.Equal(o.Extra, rt.Extra);

        // ForwardState is site-storage-only (never on the wire); IngestedAtUtc is
        // central-set at ingest, so the mapper leaves it null on the wire.
        Assert.Null(rt.IngestedAtUtc);
    }

    [Fact]
    public void ToDto_NullableStringFields_BecomeEmptyString()
    {
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.Notification,
            kind: AuditKind.NotifySend,
            status: AuditStatus.Submitted);
        // all string? fields left null; CorrelationId null

        var dto = AuditEventDtoMapper.ToDto(evt);

        Assert.Equal(string.Empty, dto.CorrelationId);
        Assert.Equal(string.Empty, dto.ExecutionId);
        Assert.Equal(string.Empty, dto.ParentExecutionId);
        Assert.Equal(string.Empty, dto.SourceSiteId);
        Assert.Equal(string.Empty, dto.SourceNode);
        Assert.Equal(string.Empty, dto.SourceInstanceId);
        Assert.Equal(string.Empty, dto.SourceScript);
        Assert.Equal(string.Empty, dto.Actor);
        Assert.Equal(string.Empty, dto.Target);
        Assert.Equal(string.Empty, dto.ErrorMessage);
        Assert.Equal(string.Empty, dto.ErrorDetail);
        Assert.Equal(string.Empty, dto.RequestSummary);
        Assert.Equal(string.Empty, dto.ResponseSummary);
        Assert.Equal(string.Empty, dto.Extra);
    }

    [Fact]
    public void FromDto_EmptyString_BecomesNullProperty()
    {
        var dto = new AuditEventDto
        {
            EventId = Guid.NewGuid().ToString(),
            OccurredAtUtc = Timestamp.FromDateTime(DateTime.UtcNow),
            Channel = nameof(AuditChannel.ApiOutbound),
            Kind = nameof(AuditKind.ApiCall),
            Status = nameof(AuditStatus.Submitted),
            CorrelationId = string.Empty,
            ExecutionId = string.Empty,
            ParentExecutionId = string.Empty,
            SourceSiteId = string.Empty,
            SourceNode = string.Empty,
            SourceInstanceId = string.Empty,
            SourceScript = string.Empty,
            Actor = string.Empty,
            Target = string.Empty,
            ErrorMessage = string.Empty,
            ErrorDetail = string.Empty,
            RequestSummary = string.Empty,
            ResponseSummary = string.Empty,
            Extra = string.Empty
        };

        var evt = AuditEventDtoMapper.FromDto(dto).AsRow();

        Assert.Null(evt.CorrelationId);
        Assert.Null(evt.ExecutionId);
        Assert.Null(evt.ParentExecutionId);
        Assert.Null(evt.SourceSiteId);
        Assert.Null(evt.SourceNode);
        Assert.Null(evt.SourceInstanceId);
        Assert.Null(evt.SourceScript);
        Assert.Null(evt.Actor);
        Assert.Null(evt.Target);
        Assert.Null(evt.ErrorMessage);
        Assert.Null(evt.ErrorDetail);
        Assert.Null(evt.RequestSummary);
        Assert.Null(evt.ResponseSummary);
        Assert.Null(evt.Extra);
    }

    [Fact]
    public void ToDto_OccurredAtUtc_PreservesUtcKind()
    {
        var occurredAt = new DateTime(2026, 5, 20, 8, 0, 0, DateTimeKind.Utc);
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.DbOutbound,
            kind: AuditKind.DbWrite,
            status: AuditStatus.Delivered,
            occurredAtUtc: occurredAt);

        var dto = AuditEventDtoMapper.ToDto(evt);
        var roundTripped = AuditEventDtoMapper.FromDto(dto).AsRow();

        Assert.Equal(DateTimeKind.Utc, roundTripped.OccurredAtUtc.Kind);
        Assert.Equal(occurredAt, roundTripped.OccurredAtUtc);
    }

    [Fact]
    public void ToDto_NullableInt_BecomesNullInt32Value()
    {
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.Notification,
            kind: AuditKind.NotifySend,
            status: AuditStatus.Submitted,
            httpStatus: null,
            durationMs: null);

        var dto = AuditEventDtoMapper.ToDto(evt);

        Assert.Null(dto.HttpStatus);
        Assert.Null(dto.DurationMs);
    }

    [Fact]
    public void FromDto_NullInt32Value_BecomesNullProperty()
    {
        var dto = new AuditEventDto
        {
            EventId = Guid.NewGuid().ToString(),
            OccurredAtUtc = Timestamp.FromDateTime(DateTime.UtcNow),
            Channel = nameof(AuditChannel.ApiInbound),
            Kind = nameof(AuditKind.InboundRequest),
            Status = nameof(AuditStatus.Delivered)
            // HttpStatus + DurationMs intentionally left absent
        };

        Assert.Null(dto.HttpStatus);
        Assert.Null(dto.DurationMs);

        var evt = AuditEventDtoMapper.FromDto(dto).AsRow();

        Assert.Null(evt.HttpStatus);
        Assert.Null(evt.DurationMs);
    }

    [Fact]
    public void ToDto_EnumValues_StoredAsStringNames()
    {
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: AuditKind.ApiCallCached,
            status: AuditStatus.Parked);

        var dto = AuditEventDtoMapper.ToDto(evt);

        Assert.Equal("ApiOutbound", dto.Channel);
        Assert.Equal("ApiCallCached", dto.Kind);
        Assert.Equal("Parked", dto.Status);
    }

    [Fact]
    public void AuditEventDto_round_trip_preserves_SourceNode()
    {
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: AuditKind.ApiCall,
            status: AuditStatus.Delivered,
            sourceNode: "node-a");

        var dto = AuditEventDtoMapper.ToDto(evt);

        // Wire form: empty-string-means-null convention; populated value
        // travels verbatim.
        Assert.Equal("node-a", dto.SourceNode);

        var roundTripped = AuditEventDtoMapper.FromDto(dto);

        Assert.Equal("node-a", roundTripped.SourceNode);
    }

    [Fact]
    public void AuditEventDto_round_trip_preserves_null_SourceNode()
    {
        var evt = ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: AuditKind.ApiCall,
            status: AuditStatus.Delivered,
            sourceNode: null);

        var dto = AuditEventDtoMapper.ToDto(evt);

        // ToDto collapses null → empty on the wire…
        Assert.Equal(string.Empty, dto.SourceNode);

        var roundTripped = AuditEventDtoMapper.FromDto(dto);

        // …and FromDto rehydrates empty → null.
        Assert.Null(roundTripped.SourceNode);
    }

    /// <summary>
    /// C3 hardening (Task 2.5): a DTO that carries an unknown/renamed enum string
    /// for Channel, Kind, or Status must NOT throw on <see cref="AuditEventDtoMapper.FromDto"/>;
    /// it degrades gracefully to the same fallbacks used by <c>AuditRowProjection.Decompose</c>
    /// (ApiInbound / InboundRequest / Submitted).
    /// </summary>
    [Fact]
    public void FromDto_UnknownEnumStrings_DoNotThrow_YieldFallbackValues()
    {
        var dto = new AuditEventDto
        {
            EventId = Guid.NewGuid().ToString(),
            OccurredAtUtc = Timestamp.FromDateTime(DateTime.UtcNow),
            Channel = "ObsoleteChannelV0",   // unknown — not a declared AuditChannel member
            Kind    = "LegacyKindName",       // unknown — not a declared AuditKind member
            Status  = "RenamedStatus99",      // unknown — not a declared AuditStatus member
        };

        // Must not throw (previously would throw ArgumentException from Enum.Parse).
        var row = AuditEventDtoMapper.FromDto(dto).AsRow();

        Assert.Equal(AuditChannel.ApiInbound,     row.Channel);
        Assert.Equal(AuditKind.InboundRequest,    row.Kind);
        Assert.Equal(AuditStatus.Submitted,       row.Status);
    }
}
