using ScadaLink.Commons.Serialization;
using ScadaLink.Commons.Types.DataConnections;

namespace ScadaLink.Commons.Tests.Types.DataConnections;

public class OpcUaEndpointConfigSerializerTests
{
    [Fact]
    public void Serialize_TypedRoundtrip_PreservesAllFields()
    {
        var original = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://plant-a:4840",
            SecurityMode = OpcUaSecurityMode.SignAndEncrypt,
            AutoAcceptUntrustedCerts = false,
            SessionTimeoutMs = 90000,
            OperationTimeoutMs = 20000,
            PublishingIntervalMs = 500,
            SamplingIntervalMs = 250,
            QueueSize = 50,
            KeepAliveCount = 5,
            LifetimeCount = 15,
            MaxNotificationsPerPublish = 200,
            Heartbeat = new OpcUaHeartbeatConfig { TagPath = "Sensors.HB", MaxSilenceSeconds = 60 }
        };

        var json = OpcUaEndpointConfigSerializer.Serialize(original);
        var (round, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(json);

        Assert.False(isLegacy);
        Assert.Equal(original.EndpointUrl, round.EndpointUrl);
        Assert.Equal(original.SecurityMode, round.SecurityMode);
        Assert.Equal(original.AutoAcceptUntrustedCerts, round.AutoAcceptUntrustedCerts);
        Assert.Equal(original.SessionTimeoutMs, round.SessionTimeoutMs);
        Assert.Equal(original.OperationTimeoutMs, round.OperationTimeoutMs);
        Assert.Equal(original.PublishingIntervalMs, round.PublishingIntervalMs);
        Assert.Equal(original.SamplingIntervalMs, round.SamplingIntervalMs);
        Assert.Equal(original.QueueSize, round.QueueSize);
        Assert.Equal(original.KeepAliveCount, round.KeepAliveCount);
        Assert.Equal(original.LifetimeCount, round.LifetimeCount);
        Assert.Equal(original.MaxNotificationsPerPublish, round.MaxNotificationsPerPublish);
        Assert.NotNull(round.Heartbeat);
        Assert.Equal("Sensors.HB", round.Heartbeat!.TagPath);
        Assert.Equal(60, round.Heartbeat.MaxSilenceSeconds);
    }

    [Fact]
    public void Serialize_UsesCamelCase()
    {
        var config = new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://x:4840" };
        var json = OpcUaEndpointConfigSerializer.Serialize(config);
        Assert.Contains("\"endpointUrl\"", json);
        Assert.DoesNotContain("\"EndpointUrl\"", json);
    }

    [Fact]
    public void Serialize_SecurityModeAsCamelCaseString()
    {
        var config = new OpcUaEndpointConfig { SecurityMode = OpcUaSecurityMode.SignAndEncrypt };
        var json = OpcUaEndpointConfigSerializer.Serialize(config);

        using var doc = System.Text.Json.JsonDocument.Parse(json);
        Assert.Equal("signAndEncrypt", doc.RootElement.GetProperty("securityMode").GetString());
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Deserialize_NullOrEmpty_ReturnsDefaults(string? input)
    {
        var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(input);

        Assert.False(isLegacy);
        Assert.Equal("", config.EndpointUrl);
        Assert.Equal(60000, config.SessionTimeoutMs);
        Assert.Null(config.Heartbeat);
    }

    [Fact]
    public void Deserialize_LegacyFlatDict_IsLegacyTrue_PopulatesFields()
    {
        var legacyJson = """
            {
              "endpoint": "opc.tcp://legacy:4840",
              "SessionTimeoutMs": "45000",
              "SamplingIntervalMs": "500",
              "SecurityMode": "Sign",
              "AutoAcceptUntrustedCerts": "false",
              "HeartbeatTagPath": "Old.Heartbeat",
              "HeartbeatMaxSilence": "20"
            }
            """;

        var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(legacyJson);

        Assert.True(isLegacy);
        Assert.Equal("opc.tcp://legacy:4840", config.EndpointUrl);
        Assert.Equal(45000, config.SessionTimeoutMs);
        Assert.Equal(500, config.SamplingIntervalMs);
        Assert.Equal(OpcUaSecurityMode.Sign, config.SecurityMode);
        Assert.False(config.AutoAcceptUntrustedCerts);
        Assert.NotNull(config.Heartbeat);
        Assert.Equal("Old.Heartbeat", config.Heartbeat!.TagPath);
        Assert.Equal(20, config.Heartbeat.MaxSilenceSeconds);
    }

    [Fact]
    public void Deserialize_LegacyWithEndpointUrlPascalKey_Works()
    {
        var legacyJson = """{"EndpointUrl":"opc.tcp://x:4840"}""";
        var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(legacyJson);
        Assert.True(isLegacy);
        Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
    }

    [Fact]
    public void Deserialize_LegacyWithMixedTypeValues_PreservesAllFields()
    {
        // Real-world legacy producer emitted mixed JSON value types:
        // string for endpoint, number for publishInterval, etc.
        var legacyJson = """
            {"endpoint":"opc.tcp://scadalink-opcua:50000","securityMode":"None","publishInterval":1000,"AutoAcceptUntrustedCerts":false,"SessionTimeoutMs":45000}
            """;

        var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(legacyJson);

        Assert.True(isLegacy);
        Assert.Equal("opc.tcp://scadalink-opcua:50000", config.EndpointUrl);
        Assert.Equal(OpcUaSecurityMode.None, config.SecurityMode);
        Assert.False(config.AutoAcceptUntrustedCerts);
        Assert.Equal(45000, config.SessionTimeoutMs);
        // publishInterval is not a recognized key (the real key is PublishingIntervalMs);
        // FromFlatDict ignores unknown keys, so PublishingIntervalMs stays at its POCO default of 1000.
        Assert.Equal(1000, config.PublishingIntervalMs);
    }

    [Theory]
    [InlineData("not json at all")]
    [InlineData("[1,2,3]")]
    [InlineData("{\"foo\":123}")]
    [InlineData("\"just a string\"")]
    public void Deserialize_Malformed_ReturnsDefaultsAsLegacy(string input)
    {
        var (config, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(input);

        Assert.True(isLegacy);
        Assert.Equal("", config.EndpointUrl);
        Assert.Equal(60000, config.SessionTimeoutMs);
        Assert.Null(config.Heartbeat);
    }

    [Fact]
    public void ToFlatDict_OmitsNullHeartbeat()
    {
        var config = new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://x:4840" };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.Equal("opc.tcp://x:4840", dict["endpoint"]);
        Assert.Equal("60000", dict["SessionTimeoutMs"]);
        Assert.Equal("None", dict["SecurityMode"]);
        Assert.Equal("True", dict["AutoAcceptUntrustedCerts"]);
        Assert.False(dict.ContainsKey("HeartbeatTagPath"));
        Assert.False(dict.ContainsKey("HeartbeatMaxSilence"));
    }

    [Fact]
    public void ToFlatDict_IncludesHeartbeat_WhenSet()
    {
        var config = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            Heartbeat = new OpcUaHeartbeatConfig { TagPath = "HB.Tag", MaxSilenceSeconds = 45 }
        };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.Equal("HB.Tag", dict["HeartbeatTagPath"]);
        Assert.Equal("45", dict["HeartbeatMaxSilence"]);
    }

    [Fact]
    public void FromFlatDict_RoundTripsAllKeys()
    {
        var dict = new Dictionary<string, string>
        {
            ["endpoint"] = "opc.tcp://x:4840",
            ["SessionTimeoutMs"] = "30000",
            ["SecurityMode"] = "Sign",
            ["HeartbeatTagPath"] = "Hb",
            ["HeartbeatMaxSilence"] = "15"
        };
        var config = OpcUaEndpointConfigSerializer.FromFlatDict(dict);

        Assert.Equal("opc.tcp://x:4840", config.EndpointUrl);
        Assert.Equal(30000, config.SessionTimeoutMs);
        Assert.Equal(OpcUaSecurityMode.Sign, config.SecurityMode);
        Assert.NotNull(config.Heartbeat);
        Assert.Equal("Hb", config.Heartbeat!.TagPath);
    }

    // ── Layer A/B extensions: subscription tuning, auth, deadband ──

    [Fact]
    public void Serialize_RoundtripsNewSubscriptionScalars()
    {
        var original = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            DiscardOldest = false,
            SubscriptionPriority = 200,
            SubscriptionDisplayName = "ScadaLink-Primary",
            TimestampsToReturn = OpcUaTimestampsToReturn.Both
        };

        var json = OpcUaEndpointConfigSerializer.Serialize(original);
        var (round, isLegacy) = OpcUaEndpointConfigSerializer.Deserialize(json);

        Assert.False(isLegacy);
        Assert.False(round.DiscardOldest);
        Assert.Equal((byte)200, round.SubscriptionPriority);
        Assert.Equal("ScadaLink-Primary", round.SubscriptionDisplayName);
        Assert.Equal(OpcUaTimestampsToReturn.Both, round.TimestampsToReturn);
    }

    [Fact]
    public void Serialize_RoundtripsDeadband()
    {
        var original = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            Deadband = new OpcUaDeadbandConfig
            {
                Type = OpcUaDeadbandType.Percent,
                Value = 2.5
            }
        };

        var json = OpcUaEndpointConfigSerializer.Serialize(original);
        var (round, _) = OpcUaEndpointConfigSerializer.Deserialize(json);

        Assert.NotNull(round.Deadband);
        Assert.Equal(OpcUaDeadbandType.Percent, round.Deadband!.Type);
        Assert.Equal(2.5, round.Deadband.Value);
    }

    [Theory]
    [InlineData(OpcUaUserTokenType.UsernamePassword, "user1", "pass1", "", "")]
    [InlineData(OpcUaUserTokenType.X509Certificate, "", "", "/etc/pki/client.pfx", "pfxpass")]
    [InlineData(OpcUaUserTokenType.Anonymous, "", "", "", "")]
    public void Serialize_RoundtripsUserIdentity(
        OpcUaUserTokenType tokenType, string user, string pass, string certPath, string certPass)
    {
        var original = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            UserIdentity = new OpcUaUserIdentityConfig
            {
                TokenType = tokenType,
                Username = user,
                Password = pass,
                CertificatePath = certPath,
                CertificatePassword = certPass
            }
        };

        var json = OpcUaEndpointConfigSerializer.Serialize(original);
        var (round, _) = OpcUaEndpointConfigSerializer.Deserialize(json);

        Assert.NotNull(round.UserIdentity);
        Assert.Equal(tokenType, round.UserIdentity!.TokenType);
        Assert.Equal(user, round.UserIdentity.Username);
        Assert.Equal(pass, round.UserIdentity.Password);
        Assert.Equal(certPath, round.UserIdentity.CertificatePath);
        Assert.Equal(certPass, round.UserIdentity.CertificatePassword);
    }

    [Fact]
    public void Serialize_NullUserIdentityAndDeadband_OmittedFromTypedJson()
    {
        // Default config: UserIdentity and Deadband are null. Roundtrip should
        // preserve nulls (anonymous = no auth needed in flattened JSON either).
        var original = new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://x:4840" };
        var json = OpcUaEndpointConfigSerializer.Serialize(original);
        var (round, _) = OpcUaEndpointConfigSerializer.Deserialize(json);

        Assert.Null(round.UserIdentity);
        Assert.Null(round.Deadband);
    }

    [Fact]
    public void ToFlatDict_IncludesNewScalars()
    {
        var config = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            DiscardOldest = false,
            SubscriptionPriority = 50,
            SubscriptionDisplayName = "ScadaLink-Edge",
            TimestampsToReturn = OpcUaTimestampsToReturn.Server
        };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.Equal("False", dict["DiscardOldest"]);
        Assert.Equal("50", dict["SubscriptionPriority"]);
        Assert.Equal("ScadaLink-Edge", dict["SubscriptionDisplayName"]);
        Assert.Equal("Server", dict["TimestampsToReturn"]);
    }

    [Fact]
    public void ToFlatDict_OmitsNullUserIdentityAndDeadband()
    {
        var config = new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://x:4840" };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.False(dict.ContainsKey("UserIdentity.TokenType"));
        Assert.False(dict.ContainsKey("UserIdentity.Username"));
        Assert.False(dict.ContainsKey("Deadband.Type"));
        Assert.False(dict.ContainsKey("Deadband.Value"));
    }

    [Fact]
    public void ToFlatDict_IncludesUserIdentity_WhenSet()
    {
        var config = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            UserIdentity = new OpcUaUserIdentityConfig
            {
                TokenType = OpcUaUserTokenType.UsernamePassword,
                Username = "alice",
                Password = "secret"
            }
        };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.Equal("UsernamePassword", dict["UserIdentity.TokenType"]);
        Assert.Equal("alice", dict["UserIdentity.Username"]);
        Assert.Equal("secret", dict["UserIdentity.Password"]);
    }

    [Fact]
    public void ToFlatDict_IncludesDeadband_WhenSet()
    {
        var config = new OpcUaEndpointConfig
        {
            EndpointUrl = "opc.tcp://x:4840",
            Deadband = new OpcUaDeadbandConfig { Type = OpcUaDeadbandType.Percent, Value = 1.5 }
        };
        var dict = OpcUaEndpointConfigSerializer.ToFlatDict(config);

        Assert.Equal("Percent", dict["Deadband.Type"]);
        Assert.Equal("1.5", dict["Deadband.Value"]);
    }

    [Fact]
    public void FromFlatDict_MaterializesUserIdentity()
    {
        var dict = new Dictionary<string, string>
        {
            ["endpoint"] = "opc.tcp://x:4840",
            ["UserIdentity.TokenType"] = "UsernamePassword",
            ["UserIdentity.Username"] = "bob",
            ["UserIdentity.Password"] = "hunter2"
        };
        var config = OpcUaEndpointConfigSerializer.FromFlatDict(dict);

        Assert.NotNull(config.UserIdentity);
        Assert.Equal(OpcUaUserTokenType.UsernamePassword, config.UserIdentity!.TokenType);
        Assert.Equal("bob", config.UserIdentity.Username);
        Assert.Equal("hunter2", config.UserIdentity.Password);
    }

    [Fact]
    public void FromFlatDict_MaterializesDeadband()
    {
        var dict = new Dictionary<string, string>
        {
            ["endpoint"] = "opc.tcp://x:4840",
            ["Deadband.Type"] = "Absolute",
            ["Deadband.Value"] = "0.25"
        };
        var config = OpcUaEndpointConfigSerializer.FromFlatDict(dict);

        Assert.NotNull(config.Deadband);
        Assert.Equal(OpcUaDeadbandType.Absolute, config.Deadband!.Type);
        Assert.Equal(0.25, config.Deadband.Value);
    }

    [Fact]
    public void FromFlatDict_AnonymousTokenTypeStillMaterializesUserIdentity()
    {
        // Explicit Anonymous TokenType (different from "missing") materializes the sub-object.
        var dict = new Dictionary<string, string>
        {
            ["endpoint"] = "opc.tcp://x:4840",
            ["UserIdentity.TokenType"] = "Anonymous"
        };
        var config = OpcUaEndpointConfigSerializer.FromFlatDict(dict);

        Assert.NotNull(config.UserIdentity);
        Assert.Equal(OpcUaUserTokenType.Anonymous, config.UserIdentity!.TokenType);
    }
}
