using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;

public class AlarmTriggerConfigCodecTests
{
    // ── Parse: ValueMatch ──────────────────────────────────────────────────

    [Fact]
    public void Parse_ValueMatch_ReadsCanonicalKeys()
    {
        const string json = @"{""attributeName"":""Status"",""matchValue"":""Critical""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);

        Assert.Equal("Status", model.AttributeName);
        Assert.Equal("Critical", model.MatchValue);
        Assert.False(model.NotEquals);
    }

    [Fact]
    public void Parse_ValueMatch_AcceptsLegacyAttributeAndValueKeys()
    {
        // Older configs used "attribute" and "value" instead of the canonical names.
        const string json = @"{""attribute"":""Status"",""value"":""Critical""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);

        Assert.Equal("Status", model.AttributeName);
        Assert.Equal("Critical", model.MatchValue);
    }

    [Fact]
    public void Parse_ValueMatch_NotEqualsPrefix_SetsFlagAndStripsPrefix()
    {
        const string json = @"{""attributeName"":""Status"",""matchValue"":""!=Good""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);

        Assert.True(model.NotEquals);
        Assert.Equal("Good", model.MatchValue);
    }

    [Fact]
    public void Parse_ValueMatch_MissingMatchValue_LeavesNull()
    {
        const string json = @"{""attributeName"":""Status""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);

        Assert.Equal("Status", model.AttributeName);
        Assert.Null(model.MatchValue);
        Assert.False(model.NotEquals);
    }

    // ── Parse: RangeViolation ──────────────────────────────────────────────

    [Fact]
    public void Parse_RangeViolation_ReadsCanonicalKeys()
    {
        const string json = @"{""attributeName"":""Temp"",""min"":0,""max"":100}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);

        Assert.Equal(0, model.Min);
        Assert.Equal(100, model.Max);
    }

    [Fact]
    public void Parse_RangeViolation_AcceptsLegacyLowHighKeys()
    {
        const string json = @"{""attributeName"":""Temp"",""low"":-10,""high"":50}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);

        Assert.Equal(-10, model.Min);
        Assert.Equal(50, model.Max);
    }

    [Fact]
    public void Parse_RangeViolation_CanonicalKeysWinOverLegacy()
    {
        // If both canonical and legacy aliases are present, the canonical key wins.
        const string json = @"{""attributeName"":""T"",""min"":0,""low"":-999,""max"":100,""high"":999}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);

        Assert.Equal(0, model.Min);
        Assert.Equal(100, model.Max);
    }

    [Fact]
    public void Parse_RangeViolation_StringNumericValues_AreParsed()
    {
        // Some configs serialize min/max as JSON strings. Codec accepts both.
        const string json = @"{""attributeName"":""T"",""min"":""1.5"",""max"":""9.75""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);

        Assert.Equal(1.5, model.Min);
        Assert.Equal(9.75, model.Max);
    }

    // ── Parse: RateOfChange ────────────────────────────────────────────────

    [Fact]
    public void Parse_RateOfChange_ReadsAllFields()
    {
        const string json = @"{""attributeName"":""Pressure"",""thresholdPerSecond"":25,""windowSeconds"":2,""direction"":""rising""}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);

        Assert.Equal("Pressure", model.AttributeName);
        Assert.Equal(25, model.ThresholdPerSecond);
        Assert.Equal(2, model.WindowSeconds);
        Assert.Equal("rising", model.Direction);
    }

    [Theory]
    [InlineData("rising", "rising")]
    [InlineData("Rising", "rising")]
    [InlineData("up", "rising")]
    [InlineData("positive", "rising")]
    [InlineData("falling", "falling")]
    [InlineData("Down", "falling")]
    [InlineData("negative", "falling")]
    [InlineData("either", "either")]
    [InlineData("bogus", "either")]
    [InlineData("", "either")]
    public void Parse_RateOfChange_NormalizesDirectionAliases(string input, string expected)
    {
        var json = $@"{{""attributeName"":""x"",""direction"":""{input}""}}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);

        Assert.Equal(expected, model.Direction);
    }

    [Fact]
    public void Parse_RateOfChange_MissingDirection_DefaultsToEither()
    {
        // Older configs predate the direction field — the codec must default it
        // so existing data round-trips without surprises.
        const string json = @"{""attributeName"":""x"",""thresholdPerSecond"":10,""windowSeconds"":1}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);

        Assert.Equal("either", model.Direction);
    }

    // ── Parse: misc ────────────────────────────────────────────────────────

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Parse_NullOrWhitespace_ReturnsDefaultModel(string? input)
    {
        var model = AlarmTriggerConfigCodec.Parse(input, AlarmTriggerType.ValueMatch);

        Assert.Null(model.AttributeName);
        Assert.Null(model.MatchValue);
        Assert.False(model.NotEquals);
        Assert.Equal("either", model.Direction);
    }

    [Fact]
    public void Parse_MalformedJson_ReturnsDefaultModel_DoesNotThrow()
    {
        var model = AlarmTriggerConfigCodec.Parse("{not valid", AlarmTriggerType.RangeViolation);

        Assert.Null(model.Min);
        Assert.Null(model.Max);
    }

    // ── Serialize: ValueMatch ──────────────────────────────────────────────

    [Fact]
    public void Serialize_ValueMatch_WritesCanonicalKeysOnly()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Status",
            MatchValue = "Critical",
            // Foreign fields from other trigger types must NOT leak into the JSON.
            Min = 5,
            ThresholdPerSecond = 99,
            Direction = "rising"
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        Assert.Equal("Status", root.GetProperty("attributeName").GetString());
        Assert.Equal("Critical", root.GetProperty("matchValue").GetString());
        Assert.False(root.TryGetProperty("min", out _));
        Assert.False(root.TryGetProperty("thresholdPerSecond", out _));
        Assert.False(root.TryGetProperty("direction", out _));
    }

    [Fact]
    public void Serialize_ValueMatch_NotEquals_PrependsBangEqualsToMatchValue()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Status",
            MatchValue = "Good",
            NotEquals = true
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);

        using var doc = JsonDocument.Parse(json);
        Assert.Equal("!=Good", doc.RootElement.GetProperty("matchValue").GetString());
    }

    [Fact]
    public void Serialize_ValueMatch_NullAttributeName_WritesEmptyString()
    {
        // AlarmActor uses attributeName for subscription filtering, so the key
        // must always be present even when the user hasn't picked one yet.
        var model = new AlarmTriggerModel { MatchValue = "x" };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.ValueMatch);

        using var doc = JsonDocument.Parse(json);
        Assert.Equal("", doc.RootElement.GetProperty("attributeName").GetString());
    }

    // ── Serialize: RangeViolation ──────────────────────────────────────────

    [Fact]
    public void Serialize_RangeViolation_WritesCanonicalNumericKeys()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Min = 0,
            Max = 100,
            MatchValue = "ignored"
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        Assert.Equal(0, root.GetProperty("min").GetDouble());
        Assert.Equal(100, root.GetProperty("max").GetDouble());
        Assert.False(root.TryGetProperty("matchValue", out _));
    }

    [Fact]
    public void Serialize_RangeViolation_NullBound_OmitsKey()
    {
        var model = new AlarmTriggerModel { AttributeName = "Temp", Min = 0, Max = null };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RangeViolation);

        using var doc = JsonDocument.Parse(json);
        Assert.True(doc.RootElement.TryGetProperty("min", out _));
        Assert.False(doc.RootElement.TryGetProperty("max", out _));
    }

    // ── Serialize: RateOfChange ────────────────────────────────────────────

    [Fact]
    public void Serialize_RateOfChange_WritesThresholdWindowAndDirection()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Pressure",
            ThresholdPerSecond = 25,
            WindowSeconds = 2,
            Direction = "falling"
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RateOfChange);

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        Assert.Equal(25, root.GetProperty("thresholdPerSecond").GetDouble());
        Assert.Equal(2, root.GetProperty("windowSeconds").GetDouble());
        Assert.Equal("falling", root.GetProperty("direction").GetString());
    }

    [Fact]
    public void Serialize_RateOfChange_AlwaysIncludesDirection()
    {
        // Even with a default-constructed model, the runtime needs to know how
        // to evaluate — direction defaults to "either" and is always emitted.
        var model = new AlarmTriggerModel { AttributeName = "x" };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.RateOfChange);

        using var doc = JsonDocument.Parse(json);
        Assert.Equal("either", doc.RootElement.GetProperty("direction").GetString());
    }

    // ── Round-trip ─────────────────────────────────────────────────────────

    [Fact]
    public void RoundTrip_ValueMatch_NotEquals_Preserved()
    {
        var original = new AlarmTriggerModel
        {
            AttributeName = "Status",
            MatchValue = "Good",
            NotEquals = true
        };

        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.ValueMatch);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.ValueMatch);

        Assert.Equal(original.AttributeName, round.AttributeName);
        Assert.Equal(original.MatchValue, round.MatchValue);
        Assert.True(round.NotEquals);
    }

    [Fact]
    public void RoundTrip_RangeViolation_Preserved()
    {
        var original = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Min = -10.5,
            Max = 42.25
        };

        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.RangeViolation);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RangeViolation);

        Assert.Equal(original.Min, round.Min);
        Assert.Equal(original.Max, round.Max);
    }

    [Fact]
    public void RoundTrip_RateOfChange_Preserved()
    {
        var original = new AlarmTriggerModel
        {
            AttributeName = "Pressure",
            ThresholdPerSecond = 25,
            WindowSeconds = 2,
            Direction = "rising"
        };

        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.RateOfChange);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.RateOfChange);

        Assert.Equal(original.AttributeName, round.AttributeName);
        Assert.Equal(original.ThresholdPerSecond, round.ThresholdPerSecond);
        Assert.Equal(original.WindowSeconds, round.WindowSeconds);
        Assert.Equal(original.Direction, round.Direction);
    }

    // ── Parse: HiLo ────────────────────────────────────────────────────────

    [Fact]
    public void Parse_HiLo_ReadsAllSetpointsAndPriorities()
    {
        const string json = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":90,""hiHi"":100,""loLoPriority"":900,""loPriority"":500,""hiPriority"":500,""hiHiPriority"":900}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);

        Assert.Equal("Temp", model.AttributeName);
        Assert.Equal(0, model.LoLo);
        Assert.Equal(10, model.Lo);
        Assert.Equal(90, model.Hi);
        Assert.Equal(100, model.HiHi);
        Assert.Equal(900, model.LoLoPriority);
        Assert.Equal(500, model.LoPriority);
        Assert.Equal(500, model.HiPriority);
        Assert.Equal(900, model.HiHiPriority);
    }

    [Fact]
    public void Parse_HiLo_AcceptsPartialSetpoints_MissingOnesAreNull()
    {
        // Common case: only Hi/HiHi configured for over-temp protection.
        const string json = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);

        Assert.Null(model.LoLo);
        Assert.Null(model.Lo);
        Assert.Equal(80, model.Hi);
        Assert.Equal(100, model.HiHi);
        Assert.Null(model.HiPriority);
    }

    // ── Serialize: HiLo ────────────────────────────────────────────────────

    [Fact]
    public void Serialize_HiLo_OmitsNullSetpointsAndPriorities()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Hi = 80,
            HiHi = 100,
            HiHiPriority = 900
            // Lo, LoLo, and the other priorities left null
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        Assert.Equal(80, root.GetProperty("hi").GetDouble());
        Assert.Equal(100, root.GetProperty("hiHi").GetDouble());
        Assert.Equal(900, root.GetProperty("hiHiPriority").GetInt32());
        Assert.False(root.TryGetProperty("lo", out _));
        Assert.False(root.TryGetProperty("loLo", out _));
        Assert.False(root.TryGetProperty("hiPriority", out _));
        Assert.False(root.TryGetProperty("loPriority", out _));
    }

    [Fact]
    public void Serialize_HiLo_DoesNotLeakForeignTriggerTypeFields()
    {
        // matchValue, min/max, threshold/window/direction must NOT show up in
        // HiLo output even if the model happens to carry them.
        var model = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Hi = 80,
            MatchValue = "ignored",
            Min = 1,
            ThresholdPerSecond = 99,
            Direction = "rising"
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        Assert.False(root.TryGetProperty("matchValue", out _));
        Assert.False(root.TryGetProperty("min", out _));
        Assert.False(root.TryGetProperty("thresholdPerSecond", out _));
        Assert.False(root.TryGetProperty("direction", out _));
    }

    [Fact]
    public void Parse_HiLo_ReadsDeadbands()
    {
        const string json = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100,""hiDeadband"":2,""hiHiDeadband"":5}";

        var model = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);

        Assert.Equal(2, model.HiDeadband);
        Assert.Equal(5, model.HiHiDeadband);
        Assert.Null(model.LoDeadband);
        Assert.Null(model.LoLoDeadband);
    }

    [Fact]
    public void Serialize_HiLo_OmitsNullDeadbands()
    {
        var model = new AlarmTriggerModel
        {
            AttributeName = "Temp",
            Hi = 80,
            HiDeadband = 2
            // HiHiDeadband / LoDeadband / LoLoDeadband null
        };

        var json = AlarmTriggerConfigCodec.Serialize(model, AlarmTriggerType.HiLo);

        using var doc = JsonDocument.Parse(json);
        Assert.Equal(2, doc.RootElement.GetProperty("hiDeadband").GetDouble());
        Assert.False(doc.RootElement.TryGetProperty("hiHiDeadband", out _));
        Assert.False(doc.RootElement.TryGetProperty("loDeadband", out _));
    }

    [Fact]
    public void RoundTrip_HiLo_PreservesAllFields()
    {
        var original = new AlarmTriggerModel
        {
            AttributeName = "Pressure",
            LoLo = -5,
            Lo = 0,
            Hi = 90,
            HiHi = 110,
            LoLoPriority = 800,
            LoPriority = 400,
            HiPriority = 400,
            HiHiPriority = 800,
            LoLoDeadband = 1,
            LoDeadband = 2,
            HiDeadband = 3,
            HiHiDeadband = 4
        };

        var json = AlarmTriggerConfigCodec.Serialize(original, AlarmTriggerType.HiLo);
        var round = AlarmTriggerConfigCodec.Parse(json, AlarmTriggerType.HiLo);

        Assert.Equal(original.AttributeName, round.AttributeName);
        Assert.Equal(original.LoLo, round.LoLo);
        Assert.Equal(original.Lo, round.Lo);
        Assert.Equal(original.Hi, round.Hi);
        Assert.Equal(original.HiHi, round.HiHi);
        Assert.Equal(original.LoLoPriority, round.LoLoPriority);
        Assert.Equal(original.LoPriority, round.LoPriority);
        Assert.Equal(original.HiPriority, round.HiPriority);
        Assert.Equal(original.HiHiPriority, round.HiHiPriority);
        Assert.Equal(original.LoLoDeadband, round.LoLoDeadband);
        Assert.Equal(original.LoDeadband, round.LoDeadband);
        Assert.Equal(original.HiDeadband, round.HiDeadband);
        Assert.Equal(original.HiHiDeadband, round.HiHiDeadband);
    }

    // ── NormalizeDirection (direct) ────────────────────────────────────────

    [Theory]
    [InlineData("rising", "rising")]
    [InlineData("RISING", "rising")]
    [InlineData("falling", "falling")]
    [InlineData("up", "rising")]
    [InlineData("down", "falling")]
    [InlineData("positive", "rising")]
    [InlineData("negative", "falling")]
    [InlineData("either", "either")]
    [InlineData("", "either")]
    [InlineData(null, "either")]
    [InlineData("nonsense", "either")]
    public void NormalizeDirection_HandlesAllAliasesAndFallsBackToEither(string? input, string expected)
    {
        Assert.Equal(expected, AlarmTriggerConfigCodec.NormalizeDirection(input));
    }
}
