using Google.Protobuf.WellKnownTypes;
using ScadaLink.AuditLog.Telemetry;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Communication.Grpc;

namespace ScadaLink.AuditLog.Tests.Telemetry;

/// <summary>
/// Round-trip + edge tests for the <see cref="AuditEventMapper"/> that bridges
/// <see cref="AuditEvent"/> (Commons) ↔ <see cref="AuditEventDto"/> (proto).
/// ForwardState is site-local and IngestedAtUtc is central-set, so neither survives
/// the proto round-trip.
/// </summary>
public class AuditEventMapperTests
{
    [Fact]
    public void ToDto_FromDto_Roundtrip_FullyPopulated_PreservesAllFields()
    {
        var occurredAt = new DateTime(2026, 5, 20, 10, 15, 30, 123, DateTimeKind.Utc);
        var ingestedAt = new DateTime(2026, 5, 20, 10, 15, 31, 0, DateTimeKind.Utc);
        var correlationId = Guid.NewGuid();
        var eventId = Guid.NewGuid();

        var original = new AuditEvent
        {
            EventId = eventId,
            OccurredAtUtc = occurredAt,
            IngestedAtUtc = ingestedAt,
            Channel = AuditChannel.ApiOutbound,
            Kind = AuditKind.ApiCallCached,
            CorrelationId = correlationId,
            SourceSiteId = "site-1",
            SourceInstanceId = "Pump01",
            SourceScript = "OnDemand",
            Actor = "design-key",
            Target = "weather-api",
            Status = AuditStatus.Forwarded,
            HttpStatus = 200,
            DurationMs = 42,
            ErrorMessage = "transient timeout",
            ErrorDetail = "stack-trace",
            RequestSummary = "GET /weather",
            ResponseSummary = "{ \"ok\": true }",
            PayloadTruncated = true,
            Extra = "{ \"retryCount\": 1 }",
            ForwardState = AuditForwardState.Pending
        };

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

        Assert.Equal(original.EventId, roundTripped.EventId);
        Assert.Equal(original.OccurredAtUtc, roundTripped.OccurredAtUtc);
        Assert.Equal(original.Channel, roundTripped.Channel);
        Assert.Equal(original.Kind, roundTripped.Kind);
        Assert.Equal(original.CorrelationId, roundTripped.CorrelationId);
        Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId);
        Assert.Equal(original.SourceInstanceId, roundTripped.SourceInstanceId);
        Assert.Equal(original.SourceScript, roundTripped.SourceScript);
        Assert.Equal(original.Actor, roundTripped.Actor);
        Assert.Equal(original.Target, roundTripped.Target);
        Assert.Equal(original.Status, roundTripped.Status);
        Assert.Equal(original.HttpStatus, roundTripped.HttpStatus);
        Assert.Equal(original.DurationMs, roundTripped.DurationMs);
        Assert.Equal(original.ErrorMessage, roundTripped.ErrorMessage);
        Assert.Equal(original.ErrorDetail, roundTripped.ErrorDetail);
        Assert.Equal(original.RequestSummary, roundTripped.RequestSummary);
        Assert.Equal(original.ResponseSummary, roundTripped.ResponseSummary);
        Assert.Equal(original.PayloadTruncated, roundTripped.PayloadTruncated);
        Assert.Equal(original.Extra, roundTripped.Extra);

        // ForwardState + IngestedAtUtc are NOT carried in the proto contract.
        Assert.Null(roundTripped.ForwardState);
        Assert.Null(roundTripped.IngestedAtUtc);
    }

    [Fact]
    public void ToDto_NullableStringFields_BecomeEmptyString()
    {
        var evt = new AuditEvent
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTime.UtcNow,
            Channel = AuditChannel.Notification,
            Kind = AuditKind.NotifySend,
            Status = AuditStatus.Submitted
            // all string? fields left null; CorrelationId null
        };

        var dto = AuditEventMapper.ToDto(evt);

        Assert.Equal(string.Empty, dto.CorrelationId);
        Assert.Equal(string.Empty, dto.SourceSiteId);
        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,
            SourceSiteId = 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 = AuditEventMapper.FromDto(dto);

        Assert.Null(evt.CorrelationId);
        Assert.Null(evt.SourceSiteId);
        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 = new AuditEvent
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = occurredAt,
            Channel = AuditChannel.DbOutbound,
            Kind = AuditKind.DbWrite,
            Status = AuditStatus.Delivered
        };

        var dto = AuditEventMapper.ToDto(evt);
        var roundTripped = AuditEventMapper.FromDto(dto);

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

    [Fact]
    public void ToDto_NullableInt_BecomesNullInt32Value()
    {
        var evt = new AuditEvent
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTime.UtcNow,
            Channel = AuditChannel.Notification,
            Kind = AuditKind.NotifySend,
            Status = AuditStatus.Submitted,
            HttpStatus = null,
            DurationMs = null
        };

        var dto = AuditEventMapper.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 = AuditEventMapper.FromDto(dto);

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

    [Fact]
    public void ToDto_EnumValues_StoredAsStringNames()
    {
        var evt = new AuditEvent
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTime.UtcNow,
            Channel = AuditChannel.ApiOutbound,
            Kind = AuditKind.ApiCallCached,
            Status = AuditStatus.Parked
        };

        var dto = AuditEventMapper.ToDto(evt);

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