using Newtonsoft.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;

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

/// <summary>
/// Contract-lock tests for the highest-traffic cross-boundary record contracts.
/// These predate the ClusterClient→gRPC migration, when this traffic rode Akka.NET's
/// default Newtonsoft-JSON serializer; the design still supports system-wide artifact
/// version skew across sites, so these records must still obey additive-only evolution:
///   (a) an old-shape payload (missing newer trailing optional fields) still
///       deserializes, with the missing fields taking their constructor defaults;
///   (b) an unknown field from a *newer* peer is ignored (never throws);
///   (c) a round-trip preserves the values that were sent.
/// If a change breaks one of these tests, it breaks rolling cross-cluster
/// compatibility — do NOT "fix the test"; make the change additive (append optional
/// trailing constructor parameters, never reorder/rename/remove/retype existing ones).
/// </summary>
public class ClusterClientContractLockTests
{
    // Mirrors Akka.NET's NewtonSoftJsonSerializer tolerance: unknown members are ignored
    // rather than treated as errors, which is what lets a newer peer add fields safely.
    private static readonly JsonSerializerSettings AkkaLikeSettings = new()
    {
        MissingMemberHandling = MissingMemberHandling.Ignore,
    };

    private static T RoundTrip<T>(T value)
    {
        var json = JsonConvert.SerializeObject(value, AkkaLikeSettings);
        return JsonConvert.DeserializeObject<T>(json, AkkaLikeSettings)!;
    }

    // ---------------------------------------------------------------------
    // (a) Old-shape payloads: newer trailing optional fields absent → defaults.
    // ---------------------------------------------------------------------

    [Fact]
    public void CreateDataConnectionCommand_OldShape_WithoutNewerTrailingFields_Deserializes()
    {
        // Shape as emitted before BackupConfiguration/FailoverRetryCount existed.
        const string oldShape =
            """{"SiteId":3,"Name":"conn1","Protocol":"OpcUa","PrimaryConfiguration":"{}"}""";

        var cmd = JsonConvert.DeserializeObject<CreateDataConnectionCommand>(oldShape, AkkaLikeSettings);

        Assert.NotNull(cmd);
        Assert.Equal(3, cmd!.SiteId);
        Assert.Equal("conn1", cmd.Name);
        Assert.Equal("OpcUa", cmd.Protocol);
        Assert.Equal("{}", cmd.PrimaryConfiguration);
        // NOTE (locked reality): under Akka's Newtonsoft-JSON serializer a missing trailing
        // field takes default(T) — NOT the C# optional-parameter default. So a reference type
        // is null (BackupConfiguration) and a value type is 0 (FailoverRetryCount), even though
        // the record declares `FailoverRetryCount = 3`. Version-skew callers therefore cannot
        // rely on the C# default reaching the wire; additive fields must be safe at default(T).
        Assert.Null(cmd.BackupConfiguration);    // reference-type missing field → null
        Assert.Equal(0, cmd.FailoverRetryCount); // value-type missing field → default(int), not 3
    }

    [Fact]
    public void DeployInstanceCommand_OldShape_WithoutFetchFields_Deserializes()
    {
        // Shape as emitted before the notify-and-fetch CentralFetchBaseUrl/FetchToken pair.
        const string oldShape =
            """{"DeploymentId":"dep-1","InstanceUniqueName":"i","RevisionHash":"r","FlattenedConfigurationJson":"{}","DeployedBy":"me","Timestamp":"2026-07-08T00:00:00+00:00"}""";

        var cmd = JsonConvert.DeserializeObject<DeployInstanceCommand>(oldShape, AkkaLikeSettings);

        Assert.NotNull(cmd);
        Assert.Equal("dep-1", cmd!.DeploymentId);
        Assert.Equal("i", cmd.InstanceUniqueName);
        Assert.Null(cmd.CentralFetchBaseUrl); // default applied
        Assert.Null(cmd.FetchToken);          // default applied
    }

    [Fact]
    public void NotificationSubmit_OldShape_WithoutExecutionCorrelationFields_Deserializes()
    {
        // Shape as emitted before OriginExecutionId/OriginParentExecutionId/SourceNode.
        const string oldShape =
            """{"NotificationId":"n-1","ListName":"ops","Subject":"s","Body":"b","SourceSiteId":"site-1","SourceInstanceId":null,"SourceScript":null,"SiteEnqueuedAt":"2026-07-08T00:00:00+00:00"}""";

        var msg = JsonConvert.DeserializeObject<NotificationSubmit>(oldShape, AkkaLikeSettings);

        Assert.NotNull(msg);
        Assert.Equal("n-1", msg!.NotificationId);
        Assert.Equal("ops", msg.ListName);
        Assert.Null(msg.OriginExecutionId);       // default applied
        Assert.Null(msg.OriginParentExecutionId); // default applied
        Assert.Null(msg.SourceNode);              // default applied
    }

    // ---------------------------------------------------------------------
    // (b) Unknown fields from a newer peer are ignored (never throw).
    // ---------------------------------------------------------------------

    [Fact]
    public void AttributeValueChanged_UnknownFieldFromNewerPeer_IsIgnored()
    {
        const string newerShape =
            """{"InstanceUniqueName":"i","AttributePath":"a.b","AttributeName":"b","Value":1,"Quality":"Good","Timestamp":"2026-07-08T00:00:00+00:00","SomeFutureField":"x"}""";

        var evt = JsonConvert.DeserializeObject<AttributeValueChanged>(newerShape, AkkaLikeSettings);

        Assert.NotNull(evt);
        Assert.Equal("i", evt!.InstanceUniqueName);
        Assert.Equal("Good", evt.Quality);
    }

    [Fact]
    public void GetAttributeRequest_UnknownFieldFromNewerPeer_IsIgnored()
    {
        const string newerShape =
            """{"CorrelationId":"c-1","InstanceUniqueName":"i","AttributeName":"a","Timestamp":"2026-07-08T00:00:00+00:00","FutureFlag":true}""";

        var req = JsonConvert.DeserializeObject<GetAttributeRequest>(newerShape, AkkaLikeSettings);

        Assert.NotNull(req);
        Assert.Equal("c-1", req!.CorrelationId);
        Assert.Equal("a", req.AttributeName);
    }

    [Fact]
    public void SetStaticAttributeCommand_UnknownFieldFromNewerPeer_IsIgnored()
    {
        const string newerShape =
            """{"CorrelationId":"c-1","InstanceUniqueName":"i","AttributeName":"a","Value":"42","Timestamp":"2026-07-08T00:00:00+00:00","NewerAudience":"x"}""";

        var cmd = JsonConvert.DeserializeObject<SetStaticAttributeCommand>(newerShape, AkkaLikeSettings);

        Assert.NotNull(cmd);
        Assert.Equal("c-1", cmd!.CorrelationId);
        Assert.Equal("42", cmd.Value);
    }

    // ---------------------------------------------------------------------
    // (c) Round-trip preserves values.
    // ---------------------------------------------------------------------

    [Fact]
    public void ManagementEnvelope_RoundTrip_PreservesCorrelationIdAndUser()
    {
        var user = new AuthenticatedUser(
            "multi-role", "Multi Role",
            new[] { "Administrator", "Designer" }, new[] { "site-1", "site-2" });
        var envelope = new ManagementEnvelope(
            user, new GetDataConnectionCommand(7), "corr-1");

        var round = RoundTrip(envelope);

        Assert.Equal("corr-1", round.CorrelationId);
        Assert.Equal("multi-role", round.User.Username);
        Assert.Equal(new[] { "Administrator", "Designer" }, round.User.Roles);
        Assert.Equal(new[] { "site-1", "site-2" }, round.User.PermittedSiteIds);
    }

    [Fact]
    public void AttributeValueChanged_RoundTrip_PreservesValues()
    {
        var evt = new AttributeValueChanged(
            "inst-1", "path.to.attr", "attr", 42, "Good",
            new DateTimeOffset(2026, 7, 8, 12, 0, 0, TimeSpan.Zero));

        var round = RoundTrip(evt);

        Assert.Equal("inst-1", round.InstanceUniqueName);
        Assert.Equal("path.to.attr", round.AttributePath);
        Assert.Equal("attr", round.AttributeName);
        Assert.Equal("Good", round.Quality);
        Assert.Equal(evt.Timestamp, round.Timestamp);
    }

    [Fact]
    public void CreateDataConnectionCommand_RoundTrip_PreservesValues()
    {
        var cmd = new CreateDataConnectionCommand(
            SiteId: 5, Name: "conn-x", Protocol: "MxAccess",
            PrimaryConfiguration: "{\"a\":1}", BackupConfiguration: "{\"b\":2}",
            FailoverRetryCount: 9);

        var round = RoundTrip(cmd);

        Assert.Equal(5, round.SiteId);
        Assert.Equal("conn-x", round.Name);
        Assert.Equal("MxAccess", round.Protocol);
        Assert.Equal("{\"a\":1}", round.PrimaryConfiguration);
        Assert.Equal("{\"b\":2}", round.BackupConfiguration);
        Assert.Equal(9, round.FailoverRetryCount);
    }
}
