using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;

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

/// <summary>
/// Notification Outbox: construction and value-equality tests for the
/// site/central notification message contracts and the outbox UI query/action contracts.
/// </summary>
public class NotificationMessagesTests
{
    // ── Task 7: site/central notification message contracts ──

    [Fact]
    public void NotificationSubmit_PositionalConstruction_SetsAllFields()
    {
        var enqueuedAt = DateTimeOffset.UtcNow;
        var msg = new NotificationSubmit(
            "notif-1", "Operators", "Tank overflow", "Tank 3 has overflowed.",
            "site-01", "inst-7", "OnAlarm", enqueuedAt);

        Assert.Equal("notif-1", msg.NotificationId);
        Assert.Equal("Operators", msg.ListName);
        Assert.Equal("Tank overflow", msg.Subject);
        Assert.Equal("Tank 3 has overflowed.", msg.Body);
        Assert.Equal("site-01", msg.SourceSiteId);
        Assert.Equal("inst-7", msg.SourceInstanceId);
        Assert.Equal("OnAlarm", msg.SourceScript);
        Assert.Equal(enqueuedAt, msg.SiteEnqueuedAt);
    }

    [Fact]
    public void NotificationSubmit_AllowsNullOptionalSourceFields()
    {
        var msg = new NotificationSubmit(
            "notif-2", "Operators", "Subject", "Body",
            "site-01", null, null, DateTimeOffset.UtcNow);

        Assert.Null(msg.SourceInstanceId);
        Assert.Null(msg.SourceScript);
    }

    [Fact]
    public void NotificationSubmit_OriginExecutionId_DefaultsToNull()
    {
        // Audit Log #23: OriginExecutionId is an additive trailing member — a
        // submit built without it (old call sites / old serialized payloads)
        // leaves the id null.
        var msg = new NotificationSubmit(
            "notif-3", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow);

        Assert.Null(msg.OriginExecutionId);
    }

    [Fact]
    public void NotificationSubmit_OriginExecutionId_RoundTripsWhenSupplied()
    {
        var executionId = Guid.NewGuid();
        var msg = new NotificationSubmit(
            "notif-4", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow, executionId);

        Assert.Equal(executionId, msg.OriginExecutionId);
    }

    [Fact]
    public void NotificationSubmit_OriginExecutionId_SurvivesJsonRoundTrip()
    {
        // The buffered S&F payload IS a serialized NotificationSubmit; the
        // forwarder deserializes it, so OriginExecutionId must survive JSON.
        var executionId = Guid.NewGuid();
        var msg = new NotificationSubmit(
            "notif-5", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow, executionId);

        var json = System.Text.Json.JsonSerializer.Serialize(msg);
        var roundTripped = System.Text.Json.JsonSerializer.Deserialize<NotificationSubmit>(json);

        Assert.NotNull(roundTripped);
        Assert.Equal(executionId, roundTripped!.OriginExecutionId);
    }

    [Fact]
    public void NotificationSubmit_OriginParentExecutionId_DefaultsToNull()
    {
        // Audit Log ParentExecutionId: OriginParentExecutionId is an additive
        // trailing member — a submit built without it (old call sites / old
        // serialized payloads, or non-routed runs) leaves the id null.
        var msg = new NotificationSubmit(
            "notif-6", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow);

        Assert.Null(msg.OriginParentExecutionId);
    }

    [Fact]
    public void NotificationSubmit_OriginParentExecutionId_RoundTripsWhenSupplied()
    {
        var executionId = Guid.NewGuid();
        var parentExecutionId = Guid.NewGuid();
        var msg = new NotificationSubmit(
            "notif-7", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow,
            executionId, parentExecutionId);

        Assert.Equal(parentExecutionId, msg.OriginParentExecutionId);
    }

    [Fact]
    public void NotificationSubmit_OriginParentExecutionId_SurvivesJsonRoundTrip()
    {
        // The buffered S&F payload IS a serialized NotificationSubmit; the
        // forwarder deserializes it, so OriginParentExecutionId must survive JSON.
        var executionId = Guid.NewGuid();
        var parentExecutionId = Guid.NewGuid();
        var msg = new NotificationSubmit(
            "notif-8", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow,
            executionId, parentExecutionId);

        var json = System.Text.Json.JsonSerializer.Serialize(msg);
        var roundTripped = System.Text.Json.JsonSerializer.Deserialize<NotificationSubmit>(json);

        Assert.NotNull(roundTripped);
        Assert.Equal(parentExecutionId, roundTripped!.OriginParentExecutionId);
    }

    [Fact]
    public void NotificationSubmit_carries_SourceNode()
    {
        // SourceNode is an additive trailing member — old call sites and old
        // serialized payloads leave it null. When supplied it round-trips
        // through both construction and JSON (the buffered S&F payload IS a
        // serialized NotificationSubmit).
        var defaulted = new NotificationSubmit(
            "notif-9", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow);
        Assert.Null(defaulted.SourceNode);

        var stamped = new NotificationSubmit(
            "notif-10", "Operators", "Subject", "Body",
            "site-01", "inst-1", "OnAlarm", DateTimeOffset.UtcNow,
            OriginExecutionId: null,
            OriginParentExecutionId: null,
            SourceNode: "node-a");
        Assert.Equal("node-a", stamped.SourceNode);

        var json = System.Text.Json.JsonSerializer.Serialize(stamped);
        var roundTripped = System.Text.Json.JsonSerializer.Deserialize<NotificationSubmit>(json);
        Assert.NotNull(roundTripped);
        Assert.Equal("node-a", roundTripped!.SourceNode);
    }

    [Fact]
    public void NotificationSubmit_ValueEquality_EqualWhenAllFieldsMatch()
    {
        var enqueuedAt = DateTimeOffset.UtcNow;
        var a = new NotificationSubmit("n", "L", "S", "B", "site", "inst", "scr", enqueuedAt);
        var b = new NotificationSubmit("n", "L", "S", "B", "site", "inst", "scr", enqueuedAt);

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

    [Fact]
    public void NotificationSubmitAck_WithExpression_ChangesSingleField()
    {
        var ack = new NotificationSubmitAck("notif-1", true, null);
        var rejected = ack with { Accepted = false, Error = "duplicate" };

        Assert.True(ack.Accepted);
        Assert.False(rejected.Accepted);
        Assert.Equal("duplicate", rejected.Error);
        Assert.Equal("notif-1", rejected.NotificationId);
        Assert.NotEqual(ack, rejected);
    }

    [Fact]
    public void NotificationStatusQuery_PositionalConstruction_SetsAllFields()
    {
        var msg = new NotificationStatusQuery("corr-1", "notif-9");

        Assert.Equal("corr-1", msg.CorrelationId);
        Assert.Equal("notif-9", msg.NotificationId);
    }

    [Fact]
    public void NotificationStatusResponse_PositionalConstruction_SetsAllFields()
    {
        var deliveredAt = DateTimeOffset.UtcNow;
        var msg = new NotificationStatusResponse(
            "corr-1", true, "Delivered", 2, null, deliveredAt);

        Assert.Equal("corr-1", msg.CorrelationId);
        Assert.True(msg.Found);
        Assert.Equal("Delivered", msg.Status);
        Assert.Equal(2, msg.RetryCount);
        Assert.Null(msg.LastError);
        Assert.Equal(deliveredAt, msg.DeliveredAt);
    }

    // ── Task 8: outbox UI query/action contracts ──

    [Fact]
    public void NotificationOutboxQueryRequest_PositionalConstruction_SetsAllFields()
    {
        var from = DateTimeOffset.UtcNow.AddDays(-1);
        var to = DateTimeOffset.UtcNow;
        var msg = new NotificationOutboxQueryRequest(
            "corr-1", "Stuck", "Email", "site-01", "Operators", true, "overflow",
            from, to, 2, 50);

        Assert.Equal("corr-1", msg.CorrelationId);
        Assert.Equal("Stuck", msg.StatusFilter);
        Assert.Equal("Email", msg.TypeFilter);
        Assert.Equal("site-01", msg.SourceSiteFilter);
        Assert.Equal("Operators", msg.ListNameFilter);
        Assert.True(msg.StuckOnly);
        Assert.Equal("overflow", msg.SubjectKeyword);
        Assert.Equal(from, msg.From);
        Assert.Equal(to, msg.To);
        Assert.Equal(2, msg.PageNumber);
        Assert.Equal(50, msg.PageSize);
    }

    [Fact]
    public void NotificationSummary_ValueEquality_EqualWhenAllFieldsMatch()
    {
        var createdAt = DateTimeOffset.UtcNow;
        var a = new NotificationSummary(
            "n", "Email", "Ops", "S", "Pending", 1, null, "site-01", "inst-1",
            createdAt, null, false);
        var b = new NotificationSummary(
            "n", "Email", "Ops", "S", "Pending", 1, null, "site-01", "inst-1",
            createdAt, null, false);

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

    [Fact]
    public void NotificationOutboxQueryResponse_PositionalConstruction_SetsAllFields()
    {
        var summary = new NotificationSummary(
            "n", "Email", "Ops", "S", "Delivered", 0, null, "site-01", null,
            DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, false);
        var msg = new NotificationOutboxQueryResponse(
            "corr-1", true, null, new[] { summary }, 1);

        Assert.Equal("corr-1", msg.CorrelationId);
        Assert.True(msg.Success);
        Assert.Null(msg.ErrorMessage);
        Assert.Single(msg.Notifications);
        Assert.Equal(1, msg.TotalCount);
    }

    [Fact]
    public void RetryNotificationRequestAndResponse_RoundTripFields()
    {
        var request = new RetryNotificationRequest("corr-1", "notif-1");
        var response = new RetryNotificationResponse("corr-1", true, null);

        Assert.Equal("corr-1", request.CorrelationId);
        Assert.Equal("notif-1", request.NotificationId);
        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
    }

    [Fact]
    public void DiscardNotificationRequestAndResponse_RoundTripFields()
    {
        var request = new DiscardNotificationRequest("corr-1", "notif-1");
        var response = new DiscardNotificationResponse("corr-1", false, "not found");

        Assert.Equal("notif-1", request.NotificationId);
        Assert.False(response.Success);
        Assert.Equal("not found", response.ErrorMessage);
    }

    [Fact]
    public void NotificationKpiResponse_WithExpression_ChangesSingleField()
    {
        var kpi = new NotificationKpiResponse(
            "corr-1", Success: true, ErrorMessage: null, 10, 2, 1, 5, TimeSpan.FromMinutes(3));
        var updated = kpi with { QueueDepth = 12 };

        Assert.True(kpi.Success);
        Assert.Null(kpi.ErrorMessage);
        Assert.Equal(10, kpi.QueueDepth);
        Assert.Equal(12, updated.QueueDepth);
        Assert.Equal(2, updated.StuckCount);
        Assert.Equal(TimeSpan.FromMinutes(3), updated.OldestPendingAge);
        Assert.NotEqual(kpi, updated);
    }

    [Fact]
    public void NotificationKpiRequest_PositionalConstruction_SetsCorrelationId()
    {
        var msg = new NotificationKpiRequest("corr-1");
        Assert.Equal("corr-1", msg.CorrelationId);
    }

    [Fact]
    public void PerSiteNotificationKpiRequest_CarriesCorrelationId()
    {
        var request = new PerSiteNotificationKpiRequest("corr-1");
        Assert.Equal("corr-1", request.CorrelationId);
    }

    [Fact]
    public void PerSiteNotificationKpiResponse_CarriesPerSiteSnapshots()
    {
        var sites = new[]
        {
            new SiteNotificationKpiSnapshot("plant-a", 3, 1, 0, 10, TimeSpan.FromMinutes(4)),
        };
        var response = new PerSiteNotificationKpiResponse("corr-1", Success: true, ErrorMessage: null, sites);

        Assert.True(response.Success);
        Assert.Null(response.ErrorMessage);
        Assert.Single(response.Sites);
        Assert.Equal("plant-a", response.Sites[0].SourceSiteId);
    }
}
