using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Messages.Integration;

/// <summary>
/// Audit Log #23 (M3 Bundle A — Task A4) — tests for the combined
/// audit + operational telemetry packet emitted per cached-call lifecycle event
/// (<c>Submit</c> → per-attempt <c>ApiCallCached</c> / <c>DbWriteCached</c> →
/// terminal <c>Resolve</c>). The site emits one packet per event; central writes
/// <c>AuditLog</c> + <c>SiteCalls</c> in one MS SQL transaction.
/// </summary>
public class CachedCallTelemetryTests
{
    private static readonly DateTime FixedNowUtc = new(2026, 5, 20, 10, 0, 0, DateTimeKind.Utc);

    private const string SiteId = "site-77";
    private const string InstanceName = "Plant.Pump42";
    private const string SourceScript = "ScriptActor:OnTick";

    private static AuditEvent BuildAuditEvent(
        TrackedOperationId trackedId,
        AuditKind kind,
        AuditStatus status,
        Guid? correlationId = null,
        string? errorMessage = null,
        int? httpStatus = null)
    {
        // C3 (Task 2.5): build the canonical ZB.MOM.WW.Audit.AuditEvent via the
        // shared factory; the ScadaBridge domain fields ride in DetailsJson and
        // are read back as typed properties via AsRow() in the assertions.
        // ForwardState is no longer a packet field — it is a site-storage-only
        // concern handled by the SQLite writer.
        return ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.ApiOutbound,
            kind: kind,
            status: status,
            occurredAtUtc: FixedNowUtc,
            target: "ERP.GetOrder",
            correlationId: correlationId ?? trackedId.Value,
            sourceSiteId: SiteId,
            sourceInstanceId: InstanceName,
            sourceScript: SourceScript,
            httpStatus: httpStatus,
            errorMessage: errorMessage);
    }

    private static SiteCallOperational BuildOperational(
        TrackedOperationId trackedId,
        AuditStatus status,
        int retryCount,
        string? lastError = null,
        int? httpStatus = null,
        DateTime? terminalAtUtc = null)
    {
        return new SiteCallOperational(
            TrackedOperationId: trackedId,
            Channel: nameof(AuditChannel.ApiOutbound),
            Target: "ERP.GetOrder",
            SourceSite: SiteId,
            // SourceNode: actual stamping arrives with Task 14; for now the
            // packet builder leaves the column null so existing assertions on
            // the packet's other fields stay intact.
            SourceNode: null,
            Status: status.ToString(),
            RetryCount: retryCount,
            LastError: lastError,
            HttpStatus: httpStatus,
            CreatedAtUtc: FixedNowUtc,
            UpdatedAtUtc: terminalAtUtc ?? FixedNowUtc,
            TerminalAtUtc: terminalAtUtc);
    }

    [Fact]
    public void SubmitPacket_AuditCarriesCachedSubmit_AndOperationalRetryCountZero()
    {
        var trackedId = TrackedOperationId.New();
        var audit = BuildAuditEvent(trackedId, AuditKind.CachedSubmit, AuditStatus.Submitted);
        var operational = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);

        var packet = new CachedCallTelemetry(audit, operational);

        Assert.Equal(AuditKind.CachedSubmit, packet.Audit.AsRow().Kind);
        Assert.Equal(AuditStatus.Submitted, packet.Audit.AsRow().Status);
        Assert.Equal(nameof(AuditStatus.Submitted), packet.Operational.Status);
        Assert.Equal(0, packet.Operational.RetryCount);
        Assert.Null(packet.Operational.TerminalAtUtc);
        Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
    }

    [Fact]
    public void AttemptedPacket_AuditCarriesApiCallCached_RetryCountAlignsBetweenAuditAndOperational()
    {
        var trackedId = TrackedOperationId.New();
        var audit = BuildAuditEvent(
            trackedId,
            AuditKind.ApiCallCached,
            AuditStatus.Attempted,
            errorMessage: "HTTP 503 from ERP",
            httpStatus: 503);
        var operational = BuildOperational(
            trackedId,
            AuditStatus.Attempted,
            retryCount: 2,
            lastError: "HTTP 503 from ERP",
            httpStatus: 503);

        var packet = new CachedCallTelemetry(audit, operational);

        Assert.Equal(AuditKind.ApiCallCached, packet.Audit.AsRow().Kind);
        Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
        Assert.Equal(nameof(AuditStatus.Attempted), packet.Operational.Status);
        // Retry-count alignment: the operational row carries the canonical N;
        // the audit row's error/http surface the same attempt's outcome.
        Assert.Equal(packet.Audit.AsRow().ErrorMessage, packet.Operational.LastError);
        Assert.Equal(packet.Audit.AsRow().HttpStatus, packet.Operational.HttpStatus);
        Assert.Equal(2, packet.Operational.RetryCount);
        Assert.Null(packet.Operational.TerminalAtUtc);
    }

    [Fact]
    public void AttemptedPacket_DbWriteCached_CarriesDbWriteCachedKind()
    {
        var trackedId = TrackedOperationId.New();
        var audit = BuildAuditEvent(
            trackedId,
            AuditKind.DbWriteCached,
            AuditStatus.Attempted,
            errorMessage: "Timeout",
            httpStatus: null);
        var operational = BuildOperational(
            trackedId,
            AuditStatus.Attempted,
            retryCount: 1,
            lastError: "Timeout");

        var packet = new CachedCallTelemetry(audit, operational);

        Assert.Equal(AuditKind.DbWriteCached, packet.Audit.AsRow().Kind);
        Assert.Equal(AuditStatus.Attempted, packet.Audit.AsRow().Status);
        Assert.Equal(1, packet.Operational.RetryCount);
    }

    [Theory]
    [InlineData(AuditStatus.Delivered)]
    [InlineData(AuditStatus.Failed)]
    [InlineData(AuditStatus.Parked)]
    [InlineData(AuditStatus.Discarded)]
    public void ResolvePacket_AuditCarriesCachedResolve_OperationalTerminalAtUtcSet(AuditStatus terminalStatus)
    {
        var trackedId = TrackedOperationId.New();
        var terminalAt = FixedNowUtc.AddMinutes(5);
        var audit = BuildAuditEvent(trackedId, AuditKind.CachedResolve, terminalStatus);
        var operational = BuildOperational(
            trackedId,
            terminalStatus,
            retryCount: 3,
            terminalAtUtc: terminalAt);

        var packet = new CachedCallTelemetry(audit, operational);

        Assert.Equal(AuditKind.CachedResolve, packet.Audit.AsRow().Kind);
        Assert.Equal(terminalStatus, packet.Audit.AsRow().Status);
        Assert.Equal(terminalStatus.ToString(), packet.Operational.Status);
        Assert.NotNull(packet.Operational.TerminalAtUtc);
        Assert.Equal(terminalAt, packet.Operational.TerminalAtUtc);
    }

    [Fact]
    public void CachedCallTelemetry_RoundTripEquality()
    {
        var trackedId = TrackedOperationId.New();
        var audit = BuildAuditEvent(trackedId, AuditKind.CachedSubmit, AuditStatus.Submitted);
        var operational = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);

        var a = new CachedCallTelemetry(audit, operational);
        var b = new CachedCallTelemetry(audit, operational);

        Assert.Equal(a, b);
        Assert.Equal(a.GetHashCode(), b.GetHashCode());

        var differentOperational = operational with { RetryCount = 1 };
        var c = a with { Operational = differentOperational };

        Assert.NotEqual(a, c);
        Assert.Equal(0, a.Operational.RetryCount);
        Assert.Equal(1, c.Operational.RetryCount);
    }

    [Fact]
    public void SiteCallOperational_RoundTripEquality_AndWithExpression()
    {
        var trackedId = TrackedOperationId.New();
        var a = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);
        var b = BuildOperational(trackedId, AuditStatus.Submitted, retryCount: 0);

        Assert.Equal(a, b);
        Assert.Equal(a.GetHashCode(), b.GetHashCode());

        var withDifferentRetry = a with { RetryCount = 5 };
        Assert.NotEqual(a, withDifferentRetry);
        Assert.Equal(0, a.RetryCount);
        Assert.Equal(5, withDifferentRetry.RetryCount);
    }
}
