using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;

namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Serialization;

/// <summary>
/// Round-trip + forward-compat coverage for the M8 site/instance-scoped bundle
/// DTOs (<see cref="SiteDto"/>, <see cref="DataConnectionDto"/>,
/// <see cref="InstanceDto"/> and the extended <see cref="BundleContentDto"/>).
/// </summary>
public sealed class BundleDtoSerializationTests
{
    // Use the production serialization contract (WhenWritingNull + enum-as-string
    // + unknown-member tolerance) so these tests exercise the real wire format.
    private static readonly JsonSerializerOptions Options = BundleJsonOptions.Default;

    private static BundleContentDto NewFullContent() =>
        new(
            TemplateFolders: Array.Empty<TemplateFolderDto>(),
            Templates: Array.Empty<TemplateDto>(),
            SharedScripts: Array.Empty<SharedScriptDto>(),
            ExternalSystems: Array.Empty<ExternalSystemDto>(),
            DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
            NotificationLists: Array.Empty<NotificationListDto>(),
            SmtpConfigs: Array.Empty<SmtpConfigDto>(),
            ApiMethods: Array.Empty<ApiMethodDto>())
        {
            Sites = new[]
            {
                new SiteDto(
                    SiteIdentifier: "site-a",
                    Name: "Site A",
                    Description: "Primary site",
                    NodeAAddress: "akka.tcp://site@10.0.0.1:7000",
                    NodeBAddress: "akka.tcp://site@10.0.0.2:7000",
                    GrpcNodeAAddress: "10.0.0.1:8083",
                    GrpcNodeBAddress: "10.0.0.2:8083"),
            },
            DataConnections = new[]
            {
                new DataConnectionDto(
                    SiteIdentifier: "site-a",
                    Name: "opc-main",
                    Protocol: "OpcUa",
                    FailoverRetryCount: 5,
                    Secrets: new SecretsBlock(new Dictionary<string, string>
                    {
                        ["PrimaryConfiguration"] = "{\"endpoint\":\"opc.tcp://primary:4840\"}",
                        ["BackupConfiguration"] = "{\"endpoint\":\"opc.tcp://backup:4840\"}",
                    })),
            },
            Instances = new[]
            {
                new InstanceDto(
                    UniqueName: "Pump-001",
                    TemplateName: "PumpTemplate",
                    SiteIdentifier: "site-a",
                    AreaName: "AreaWest",
                    State: InstanceState.Enabled,
                    AttributeOverrides: new[]
                    {
                        new InstanceAttributeOverrideDto(
                            AttributeName: "Setpoint",
                            OverrideValue: "42.5",
                            ElementDataType: DataType.Double),
                    },
                    AlarmOverrides: new[]
                    {
                        new InstanceAlarmOverrideDto(
                            AlarmCanonicalName: "HighTemp",
                            TriggerConfigurationOverride: "{\"limit\":90}",
                            PriorityLevelOverride: 800),
                    },
                    NativeAlarmSourceOverrides: new[]
                    {
                        new InstanceNativeAlarmSourceOverrideDto(
                            SourceCanonicalName: "AnC.Source1",
                            ConnectionNameOverride: "opc-main",
                            SourceReferenceOverride: "ns=2;s=Alarm1",
                            ConditionFilterOverride: "severity>=500"),
                    },
                    ConnectionBindings: new[]
                    {
                        new InstanceConnectionBindingDto(
                            AttributeName: "Pressure",
                            ConnectionName: "opc-main",
                            DataSourceReferenceOverride: "ns=2;s=Pressure"),
                    }),
            },
        };

    [Fact]
    public void FullContent_RoundTrips_WithDeepEquality()
    {
        var original = NewFullContent();

        var json = JsonSerializer.Serialize(original, Options);
        var roundTripped = JsonSerializer.Deserialize<BundleContentDto>(json, Options);

        Assert.NotNull(roundTripped);
        // Record value-equality compares collection *references*, not contents, so
        // assert the structural members explicitly to get real deep equality.
        // Assert.Equal over IEnumerable compares element-by-element (each element
        // is itself a record, hence value-compared).
        Assert.Equal(original.Sites, roundTripped!.Sites);
        Assert.Equal(original.DataConnections.Count, roundTripped.DataConnections.Count);
        Assert.Equal(original.Instances.Count, roundTripped.Instances.Count);

        var site = Assert.Single(roundTripped.Sites);
        Assert.Equal(original.Sites[0], site);

        var conn = Assert.Single(roundTripped.DataConnections);
        Assert.Equal("site-a", conn.SiteIdentifier);
        Assert.Equal("opc-main", conn.Name);
        Assert.Equal("OpcUa", conn.Protocol);
        Assert.Equal(5, conn.FailoverRetryCount);
        Assert.NotNull(conn.Secrets);
        Assert.Equal(
            "{\"endpoint\":\"opc.tcp://primary:4840\"}",
            conn.Secrets!.Values["PrimaryConfiguration"]);
        Assert.Equal(
            "{\"endpoint\":\"opc.tcp://backup:4840\"}",
            conn.Secrets.Values["BackupConfiguration"]);

        var inst = Assert.Single(roundTripped.Instances);
        Assert.Equal("Pump-001", inst.UniqueName);
        Assert.Equal("PumpTemplate", inst.TemplateName);
        Assert.Equal("site-a", inst.SiteIdentifier);
        Assert.Equal("AreaWest", inst.AreaName);
        Assert.Equal(InstanceState.Enabled, inst.State);

        Assert.Equal(original.Instances[0].AttributeOverrides[0], inst.AttributeOverrides[0]);
        Assert.Equal(original.Instances[0].AlarmOverrides[0], inst.AlarmOverrides[0]);
        Assert.Equal(original.Instances[0].NativeAlarmSourceOverrides[0], inst.NativeAlarmSourceOverrides[0]);
        Assert.Equal(original.Instances[0].ConnectionBindings[0], inst.ConnectionBindings[0]);
    }

    [Fact]
    public void EnumsSerializeAsStrings_ForHumanReadableWire()
    {
        var json = JsonSerializer.Serialize(NewFullContent(), Options);

        // InstanceState + DataType go out as their names, not integers, because
        // the shared options register JsonStringEnumConverter.
        Assert.Contains("\"Enabled\"", json, StringComparison.Ordinal);
        Assert.Contains("\"Double\"", json, StringComparison.Ordinal);
    }

    [Fact]
    public void LegacyContent_MissingNewArrays_DeserializesToEmpty_NotNull()
    {
        // A pre-M8 bundle content blob: only the original central-config arrays
        // are present; sites/dataConnections/instances fields are entirely absent
        // from the JSON text.
        const string legacyJson = """
            {
              "TemplateFolders": [],
              "Templates": [],
              "SharedScripts": [],
              "ExternalSystems": [],
              "DatabaseConnections": [],
              "NotificationLists": [],
              "SmtpConfigs": [],
              "ApiMethods": []
            }
            """;

        var content = JsonSerializer.Deserialize<BundleContentDto>(legacyJson, Options);

        Assert.NotNull(content);
        // The forward-compat invariant: the new arrays come back EMPTY, never null.
        Assert.NotNull(content!.Sites);
        Assert.Empty(content.Sites);
        Assert.NotNull(content.DataConnections);
        Assert.Empty(content.DataConnections);
        Assert.NotNull(content.Instances);
        Assert.Empty(content.Instances);
    }

    [Fact]
    public void DefaultConstructed_NewArrays_AreEmpty_NotNull()
    {
        // Existing positional callers that never set the M8 members still get
        // empty (not null) collections, so consumers can enumerate unconditionally.
        var content = new BundleContentDto(
            TemplateFolders: Array.Empty<TemplateFolderDto>(),
            Templates: Array.Empty<TemplateDto>(),
            SharedScripts: Array.Empty<SharedScriptDto>(),
            ExternalSystems: Array.Empty<ExternalSystemDto>(),
            DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
            NotificationLists: Array.Empty<NotificationListDto>(),
            SmtpConfigs: Array.Empty<SmtpConfigDto>(),
            ApiMethods: Array.Empty<ApiMethodDto>());

        Assert.NotNull(content.Sites);
        Assert.Empty(content.Sites);
        Assert.NotNull(content.DataConnections);
        Assert.Empty(content.DataConnections);
        Assert.NotNull(content.Instances);
        Assert.Empty(content.Instances);
    }

    // --- S10: SMS recipient PhoneNumber + SmsConfig wire format ---------------

    [Fact]
    public void LegacyRecipient_MissingPhoneNumber_DeserializesToNull()
    {
        // A pre-SMS bundle: notification-list recipients carry only Name + Email,
        // with no PhoneNumber field present in the JSON text at all.
        const string legacyJson = """
            {
              "TemplateFolders": [],
              "Templates": [],
              "SharedScripts": [],
              "ExternalSystems": [],
              "DatabaseConnections": [],
              "NotificationLists": [
                {
                  "Name": "alerts",
                  "Type": "Email",
                  "Recipients": [ { "Name": "Ops", "EmailAddress": "ops@example.com" } ]
                }
              ],
              "SmtpConfigs": [],
              "ApiMethods": []
            }
            """;

        var content = JsonSerializer.Deserialize<BundleContentDto>(legacyJson, Options);

        Assert.NotNull(content);
        var recipient = Assert.Single(Assert.Single(content!.NotificationLists).Recipients);
        Assert.Equal("Ops", recipient.Name);
        Assert.Equal("ops@example.com", recipient.EmailAddress);
        // The backward-compat invariant: an absent PhoneNumber comes back null.
        Assert.Null(recipient.PhoneNumber);
    }

    [Fact]
    public void LegacyContent_MissingSmsConfigsArray_DeserializesToEmpty_NotNull()
    {
        // A pre-SMS bundle has no smsConfigs field; the importer must see an empty
        // (never null) collection — same forward-compat invariant as the M8 arrays.
        const string legacyJson = """
            {
              "TemplateFolders": [],
              "Templates": [],
              "SharedScripts": [],
              "ExternalSystems": [],
              "DatabaseConnections": [],
              "NotificationLists": [],
              "SmtpConfigs": [],
              "ApiMethods": []
            }
            """;

        var content = JsonSerializer.Deserialize<BundleContentDto>(legacyJson, Options);

        Assert.NotNull(content);
        Assert.NotNull(content!.SmsConfigs);
        Assert.Empty(content.SmsConfigs);
    }

    [Fact]
    public void DefaultConstructed_SmsConfigs_IsEmpty_NotNull()
    {
        var content = new BundleContentDto(
            TemplateFolders: Array.Empty<TemplateFolderDto>(),
            Templates: Array.Empty<TemplateDto>(),
            SharedScripts: Array.Empty<SharedScriptDto>(),
            ExternalSystems: Array.Empty<ExternalSystemDto>(),
            DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
            NotificationLists: Array.Empty<NotificationListDto>(),
            SmtpConfigs: Array.Empty<SmtpConfigDto>(),
            ApiMethods: Array.Empty<ApiMethodDto>());

        Assert.NotNull(content.SmsConfigs);
        Assert.Empty(content.SmsConfigs);
    }

    [Fact]
    public void SmsConfig_AuthToken_RidesSecretsBlock_NotCleartextPublicFields()
    {
        // The auth token must travel only inside the SecretsBlock, exactly like the
        // SMTP credentials test pattern — it must NOT leak into any public DTO field.
        var content = new BundleContentDto(
            TemplateFolders: Array.Empty<TemplateFolderDto>(),
            Templates: Array.Empty<TemplateDto>(),
            SharedScripts: Array.Empty<SharedScriptDto>(),
            ExternalSystems: Array.Empty<ExternalSystemDto>(),
            DatabaseConnections: Array.Empty<DatabaseConnectionDto>(),
            NotificationLists: Array.Empty<NotificationListDto>(),
            SmtpConfigs: Array.Empty<SmtpConfigDto>(),
            ApiMethods: Array.Empty<ApiMethodDto>())
        {
            SmsConfigs = new[]
            {
                new SmsConfigDto(
                    AccountSid: "AC123",
                    FromNumber: "+15550001111",
                    MessagingServiceSid: "MG999",
                    ApiBaseUrl: "https://api.twilio.com",
                    ConnectionTimeoutSeconds: 20,
                    MaxRetries: 7,
                    RetryDelay: TimeSpan.FromSeconds(45),
                    Secrets: new SecretsBlock(new Dictionary<string, string>
                    {
                        ["AuthToken"] = "twilio-super-secret",
                    })),
            },
        };

        var json = JsonSerializer.Serialize(content, Options);

        // The secret appears exactly once — inside the SecretsBlock — and the
        // round-trip restores it.
        Assert.Contains("twilio-super-secret", json, StringComparison.Ordinal);
        Assert.Contains("\"AuthToken\"", json, StringComparison.Ordinal);

        var roundTripped = JsonSerializer.Deserialize<BundleContentDto>(json, Options);
        Assert.NotNull(roundTripped);
        var sms = Assert.Single(roundTripped!.SmsConfigs);
        Assert.Equal("AC123", sms.AccountSid);
        Assert.Equal("+15550001111", sms.FromNumber);
        Assert.NotNull(sms.Secrets);
        Assert.Equal("twilio-super-secret", sms.Secrets!.Values["AuthToken"]);
    }
}
