using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

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

/// <summary>
/// Field-coverage + edge tests for the <see cref="SiteCallDtoMapper"/> that
/// decodes <see cref="SiteCallOperationalDto"/> (proto) into the
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.SiteCall"/> persistence entity.
/// Only the DTO→entity direction exists — nothing in the system maps a
/// <c>SiteCall</c> back onto the wire — so there is no round-trip test.
/// <c>IngestedAtUtc</c> is a site-side placeholder the central ingest actor
/// overwrites, so it is asserted as "recent UTC" rather than a fixed value.
/// </summary>
public class SiteCallDtoMapperTests
{
    [Fact]
    public void FromDto_FullyPopulated_MapsEveryField()
    {
        var trackedOperationId = Guid.NewGuid();
        var createdAt = new DateTime(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc);
        var updatedAt = new DateTime(2026, 5, 20, 10, 5, 0, DateTimeKind.Utc);
        var terminalAt = new DateTime(2026, 5, 20, 10, 10, 0, DateTimeKind.Utc);

        var dto = new SiteCallOperationalDto
        {
            TrackedOperationId = trackedOperationId.ToString(),
            Channel = "ApiOutbound",
            Target = "ERP.GetOrder",
            SourceSite = "site-melbourne",
            SourceNode = "node-a",
            Status = "Delivered",
            RetryCount = 3,
            LastError = "transient 503",
            HttpStatus = 200,
            CreatedAtUtc = Timestamp.FromDateTime(createdAt),
            UpdatedAtUtc = Timestamp.FromDateTime(updatedAt),
            TerminalAtUtc = Timestamp.FromDateTime(terminalAt),
        };

        var entity = SiteCallDtoMapper.FromDto(dto);

        Assert.Equal(trackedOperationId, entity.TrackedOperationId.Value);
        Assert.Equal("ApiOutbound", entity.Channel);
        Assert.Equal("ERP.GetOrder", entity.Target);
        Assert.Equal("site-melbourne", entity.SourceSite);
        Assert.Equal("node-a", entity.SourceNode);
        Assert.Equal("Delivered", entity.Status);
        Assert.Equal(3, entity.RetryCount);
        Assert.Equal("transient 503", entity.LastError);
        Assert.Equal(200, entity.HttpStatus);
        Assert.Equal(createdAt, entity.CreatedAtUtc);
        Assert.Equal(updatedAt, entity.UpdatedAtUtc);
        Assert.Equal(terminalAt, entity.TerminalAtUtc);
    }

    [Fact]
    public void FromDto_EmptyLastError_BecomesNull()
    {
        var dto = NewMinimalDto();
        dto.LastError = string.Empty;

        var entity = SiteCallDtoMapper.FromDto(dto);

        Assert.Null(entity.LastError);
    }

    [Fact]
    public void FromDto_AbsentHttpStatus_StaysNull()
    {
        // Int32Value wrapper unset on the wire — preserves true null semantics
        // for non-API cached writes.
        var dto = NewMinimalDto();

        Assert.Null(dto.HttpStatus);

        var entity = SiteCallDtoMapper.FromDto(dto);

        Assert.Null(entity.HttpStatus);
    }

    [Fact]
    public void FromDto_AbsentTerminalAt_StaysNull()
    {
        // Timestamp wrapper unset while the call is still active.
        var dto = NewMinimalDto();

        Assert.Null(dto.TerminalAtUtc);

        var entity = SiteCallDtoMapper.FromDto(dto);

        Assert.Null(entity.TerminalAtUtc);
    }

    [Fact]
    public void FromDto_Timestamps_RehydrateAsUtcKind()
    {
        var dto = NewMinimalDto();

        var entity = SiteCallDtoMapper.FromDto(dto);

        Assert.Equal(DateTimeKind.Utc, entity.CreatedAtUtc.Kind);
        Assert.Equal(DateTimeKind.Utc, entity.UpdatedAtUtc.Kind);
    }

    [Fact]
    public void FromDto_IngestedAtUtc_StampedAsRecentPlaceholder()
    {
        // IngestedAtUtc is a site-side DateTime.UtcNow placeholder; the central
        // ingest actor overwrites it inside the dual-write transaction.
        var before = DateTime.UtcNow;

        var entity = SiteCallDtoMapper.FromDto(NewMinimalDto());

        var after = DateTime.UtcNow;
        Assert.InRange(entity.IngestedAtUtc, before, after);
        Assert.Equal(DateTimeKind.Utc, entity.IngestedAtUtc.Kind);
    }

    [Fact]
    public void FromDto_Null_Throws()
    {
        Assert.Throws<ArgumentNullException>(() => SiteCallDtoMapper.FromDto(null!));
    }

    [Fact]
    public void SiteCallOperationalDto_round_trip_preserves_SourceNode()
    {
        // Populated SourceNode travels verbatim across the wire and through
        // the DTO→entity mapper.
        var dto = NewMinimalDto();
        dto.SourceNode = "node-a";

        var bytes = dto.ToByteArray();
        var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes);
        Assert.Equal("node-a", onWire.SourceNode);

        var entity = SiteCallDtoMapper.FromDto(onWire);

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

    [Fact]
    public void SiteCallOperationalDto_round_trip_preserves_null_SourceNode()
    {
        // The DTO uses an empty-string-means-null convention on the wire;
        // FromDto rehydrates that back to a true null on the entity.
        var dto = NewMinimalDto();
        // SourceNode left at proto default (empty string) — semantically null.

        var bytes = dto.ToByteArray();
        var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes);
        Assert.Equal(string.Empty, onWire.SourceNode);

        var entity = SiteCallDtoMapper.FromDto(onWire);

        Assert.Null(entity.SourceNode);
    }

    private static SiteCallOperationalDto NewMinimalDto() => new()
    {
        TrackedOperationId = Guid.NewGuid().ToString(),
        Channel = "DbOutbound",
        Target = "warehouse.dbo.WriteOrder",
        SourceSite = "site-brisbane",
        Status = "Submitted",
        RetryCount = 0,
        CreatedAtUtc = Timestamp.FromDateTime(DateTime.UtcNow),
        UpdatedAtUtc = Timestamp.FromDateTime(DateTime.UtcNow),
    };
}
