using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.Flattening;

public class FlatteningServiceMergeTests
{
    // ── MergeHiLoConfig ────────────────────────────────────────────────────

    [Fact]
    public void MergeHiLoConfig_DerivedKeysWin_InheritedKeysSurvive()
    {
        const string inherited = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":80,""hiHi"":100}";
        const string derived = @"{""hi"":90}"; // derived only overrides Hi

        var result = FlatteningService.MergeHiLoConfig(inherited, derived);

        Assert.NotNull(result);
        using var doc = JsonDocument.Parse(result!);
        Assert.Equal("Temp", doc.RootElement.GetProperty("attributeName").GetString());
        Assert.Equal(0, doc.RootElement.GetProperty("loLo").GetDouble());
        Assert.Equal(10, doc.RootElement.GetProperty("lo").GetDouble());
        Assert.Equal(90, doc.RootElement.GetProperty("hi").GetDouble()); // overridden
        Assert.Equal(100, doc.RootElement.GetProperty("hiHi").GetDouble()); // inherited
    }

    [Fact]
    public void MergeHiLoConfig_DerivedCanOverrideAttribute()
    {
        const string inherited = @"{""attributeName"":""Temp"",""hi"":80}";
        const string derived = @"{""attributeName"":""Pressure"",""hi"":50}";

        var result = FlatteningService.MergeHiLoConfig(inherited, derived);

        using var doc = JsonDocument.Parse(result!);
        Assert.Equal("Pressure", doc.RootElement.GetProperty("attributeName").GetString());
        Assert.Equal(50, doc.RootElement.GetProperty("hi").GetDouble());
    }

    [Fact]
    public void MergeHiLoConfig_DerivedNull_ReturnsInherited()
    {
        const string inherited = @"{""hi"":80}";

        var result = FlatteningService.MergeHiLoConfig(inherited, null);

        Assert.Equal(inherited, result);
    }

    [Fact]
    public void MergeHiLoConfig_InheritedNull_ReturnsDerived()
    {
        const string derived = @"{""hi"":80}";

        var result = FlatteningService.MergeHiLoConfig(null, derived);

        Assert.Equal(derived, result);
    }

    [Fact]
    public void MergeHiLoConfig_BothNull_ReturnsNull()
    {
        Assert.Null(FlatteningService.MergeHiLoConfig(null, null));
    }

    [Fact]
    public void MergeHiLoConfig_MalformedInherited_FallsBackToDerived()
    {
        // Safe fallback — never throw on malformed input.
        const string derived = @"{""hi"":80}";

        var result = FlatteningService.MergeHiLoConfig("{not valid", derived);

        Assert.Equal(derived, result);
    }

    [Fact]
    public void MergeHiLoConfig_DerivedAddsNewKey_PreservesInheritedRest()
    {
        // Derived adds a deadband that the base didn't have.
        const string inherited = @"{""hi"":80,""hiHi"":100}";
        const string derived = @"{""hiDeadband"":3}";

        var result = FlatteningService.MergeHiLoConfig(inherited, derived);

        using var doc = JsonDocument.Parse(result!);
        Assert.Equal(80, doc.RootElement.GetProperty("hi").GetDouble());
        Assert.Equal(100, doc.RootElement.GetProperty("hiHi").GetDouble());
        Assert.Equal(3, doc.RootElement.GetProperty("hiDeadband").GetDouble());
    }

    // ── DiffHiLoConfig ─────────────────────────────────────────────────────

    [Fact]
    public void DiffHiLoConfig_NoChanges_ReturnsNull()
    {
        const string both = @"{""attributeName"":""Temp"",""hi"":80}";
        Assert.Null(FlatteningService.DiffHiLoConfig(both, both));
    }

    [Fact]
    public void DiffHiLoConfig_ChangedKey_ReturnsOnlyChangedKey()
    {
        const string inherited = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":80,""hiHi"":100}";
        const string edited = @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":90,""hiHi"":100}";

        var diff = FlatteningService.DiffHiLoConfig(inherited, edited);

        Assert.NotNull(diff);
        using var doc = JsonDocument.Parse(diff!);
        var prop = Assert.Single(doc.RootElement.EnumerateObject());
        Assert.Equal("hi", prop.Name);
        Assert.Equal(90, prop.Value.GetDouble());
    }

    [Fact]
    public void DiffHiLoConfig_NewKey_AddedToDiff()
    {
        const string inherited = @"{""attributeName"":""Temp"",""hi"":80}";
        const string edited = @"{""attributeName"":""Temp"",""hi"":80,""hiDeadband"":3}";

        var diff = FlatteningService.DiffHiLoConfig(inherited, edited);

        Assert.NotNull(diff);
        using var doc = JsonDocument.Parse(diff!);
        Assert.Equal(3, doc.RootElement.GetProperty("hiDeadband").GetDouble());
        Assert.False(doc.RootElement.TryGetProperty("hi", out _));
    }

    [Fact]
    public void DiffHiLoConfig_NullInherited_ReturnsEditedVerbatim()
    {
        const string edited = @"{""attributeName"":""Temp"",""hi"":80}";
        Assert.Equal(edited, FlatteningService.DiffHiLoConfig(null, edited));
    }

    [Fact]
    public void DiffHiLoConfig_NullEdited_ReturnsNull()
    {
        Assert.Null(FlatteningService.DiffHiLoConfig(@"{""hi"":80}", null));
    }

    [Fact]
    public void DiffHiLoConfig_IgnoresStringEscapeDifferences()
    {
        // Inherited has literal em-dash; edited has the unicode-escaped form.
        // Decoded values are identical, so the key should NOT be in the diff.
        const string inherited = @"{""attributeName"":""Temp"",""hi"":80,""hiMessage"":""High — investigate""}";
        const string edited    = @"{""attributeName"":""Temp"",""hi"":80,""hiMessage"":""High — investigate""}";

        var diff = FlatteningService.DiffHiLoConfig(inherited, edited);

        Assert.Null(diff); // no real change once values are decoded
    }

    [Fact]
    public void DiffHiLoConfig_IgnoresNumericFormatDifferences()
    {
        // 85 vs 85.0 are the same number — should not produce a diff.
        const string inherited = @"{""hi"":85}";
        const string edited    = @"{""hi"":85.0}";
        Assert.Null(FlatteningService.DiffHiLoConfig(inherited, edited));
    }

    [Fact]
    public void DiffHiLoConfig_RoundTripsThroughMerge()
    {
        // Merge(inherited, Diff(inherited, edited)) ≡ edited — when the
        // edited config is itself a superset/equivalent of inherited.
        const string inherited = @"{""attributeName"":""Temp"",""hi"":80,""hiHi"":100}";
        const string edited = @"{""attributeName"":""Temp"",""hi"":90,""hiHi"":100,""hiDeadband"":5}";

        var diff = FlatteningService.DiffHiLoConfig(inherited, edited);
        var merged = FlatteningService.MergeHiLoConfig(inherited, diff);

        using var origDoc = JsonDocument.Parse(edited);
        using var mergedDoc = JsonDocument.Parse(merged!);
        Assert.Equal(origDoc.RootElement.GetProperty("hi").GetDouble(),
                     mergedDoc.RootElement.GetProperty("hi").GetDouble());
        Assert.Equal(origDoc.RootElement.GetProperty("hiHi").GetDouble(),
                     mergedDoc.RootElement.GetProperty("hiHi").GetDouble());
        Assert.Equal(origDoc.RootElement.GetProperty("hiDeadband").GetDouble(),
                     mergedDoc.RootElement.GetProperty("hiDeadband").GetDouble());
    }

    // ── Instance-level alarm override (end-to-end Flatten) ─────────────────

    private static (Template, Instance) BuildHiLoFixture(string inheritedJson, InstanceAlarmOverride? ovr = null, bool locked = false)
    {
        var template = new Template("PumpTpl")
        {
            Id = 1,
            Alarms = new List<TemplateAlarm>
            {
                new("Temp")
                {
                    Id = 10,
                    TemplateId = 1,
                    TriggerType = AlarmTriggerType.HiLo,
                    TriggerConfiguration = inheritedJson,
                    PriorityLevel = 500,
                    IsLocked = locked
                }
            }
        };

        var instance = new Instance("Pump-1") { Id = 100, TemplateId = 1, SiteId = 1 };
        if (ovr != null) instance.AlarmOverrides.Add(ovr);
        return (template, instance);
    }

    private static FlattenedConfiguration Flatten(Template template, Instance instance)
    {
        var sut = new FlatteningService();
        var result = sut.Flatten(
            instance,
            new[] { template },
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());
        if (!result.IsSuccess) Assert.Fail(result.Error);
        return result.Value!;
    }

    [Fact]
    public void Flatten_InstanceAlarmOverride_HiLo_MergesSetpoints()
    {
        // Template has {hi=80, hiHi=100, lo=10, loLo=0}. Instance overrides hi=90 only.
        var (tpl, inst) = BuildHiLoFixture(
            @"{""attributeName"":""Temp"",""loLo"":0,""lo"":10,""hi"":80,""hiHi"":100}",
            new InstanceAlarmOverride("Temp")
            {
                InstanceId = 100,
                TriggerConfigurationOverride = @"{""hi"":90}"
            });

        var flat = Flatten(tpl, inst);
        var alarm = flat.Alarms.Single();

        using var doc = JsonDocument.Parse(alarm.TriggerConfiguration!);
        Assert.Equal(0, doc.RootElement.GetProperty("loLo").GetDouble());
        Assert.Equal(10, doc.RootElement.GetProperty("lo").GetDouble());
        Assert.Equal(90, doc.RootElement.GetProperty("hi").GetDouble()); // overridden
        Assert.Equal(100, doc.RootElement.GetProperty("hiHi").GetDouble()); // inherited
        Assert.Equal("Override", alarm.Source);
    }

    [Fact]
    public void Flatten_InstanceAlarmOverride_OverridesPriority()
    {
        var (tpl, inst) = BuildHiLoFixture(
            @"{""attributeName"":""Temp"",""hi"":80}",
            new InstanceAlarmOverride("Temp")
            {
                InstanceId = 100,
                PriorityLevelOverride = 950
            });

        var flat = Flatten(tpl, inst);
        var alarm = flat.Alarms.Single();

        Assert.Equal(950, alarm.PriorityLevel);
        Assert.Equal("Override", alarm.Source);
    }

    [Fact]
    public void Flatten_InstanceAlarmOverride_LockedAlarm_OverrideSilentlyIgnored()
    {
        // Locked alarm — override should be a no-op at flatten time. (The
        // InstanceService.SetAlarmOverrideAsync write-time check is what
        // prevents the override from being persisted in the first place;
        // this test covers the runtime safety net.)
        var (tpl, inst) = BuildHiLoFixture(
            @"{""attributeName"":""Temp"",""hi"":80}",
            new InstanceAlarmOverride("Temp")
            {
                InstanceId = 100,
                TriggerConfigurationOverride = @"{""hi"":999}"
            },
            locked: true);

        var flat = Flatten(tpl, inst);
        var alarm = flat.Alarms.Single();

        using var doc = JsonDocument.Parse(alarm.TriggerConfiguration!);
        Assert.Equal(80, doc.RootElement.GetProperty("hi").GetDouble()); // not overridden
    }

    [Fact]
    public void Flatten_InstanceAlarmOverride_UnknownName_DoesNothing()
    {
        // Override targets an alarm name that doesn't exist on the template —
        // silently ignored (same behavior as attribute overrides).
        var (tpl, inst) = BuildHiLoFixture(
            @"{""attributeName"":""Temp"",""hi"":80}",
            new InstanceAlarmOverride("DoesNotExist")
            {
                InstanceId = 100,
                TriggerConfigurationOverride = @"{""hi"":999}"
            });

        var flat = Flatten(tpl, inst);
        var alarm = flat.Alarms.Single();

        using var doc = JsonDocument.Parse(alarm.TriggerConfiguration!);
        Assert.Equal(80, doc.RootElement.GetProperty("hi").GetDouble());
        Assert.NotEqual("Override", alarm.Source);
    }

    [Fact]
    public void Flatten_InstanceAlarmOverride_BinaryTrigger_ReplacesWholeConfig()
    {
        // For non-HiLo trigger types, an instance override replaces the whole
        // TriggerConfiguration (no per-key merge).
        var template = new Template("PumpTpl")
        {
            Id = 1,
            Alarms = new List<TemplateAlarm>
            {
                new("Temp")
                {
                    Id = 10,
                    TemplateId = 1,
                    TriggerType = AlarmTriggerType.RangeViolation,
                    TriggerConfiguration = @"{""attributeName"":""T"",""min"":0,""max"":100}",
                    PriorityLevel = 500
                }
            }
        };
        var instance = new Instance("Pump-1") { Id = 100, TemplateId = 1, SiteId = 1 };
        instance.AlarmOverrides.Add(new InstanceAlarmOverride("Temp")
        {
            InstanceId = 100,
            TriggerConfigurationOverride = @"{""attributeName"":""T"",""min"":-50,""max"":50}"
        });

        var flat = Flatten(template, instance);
        var alarm = flat.Alarms.Single();

        using var doc = JsonDocument.Parse(alarm.TriggerConfiguration!);
        Assert.Equal(-50, doc.RootElement.GetProperty("min").GetDouble());
        Assert.Equal(50, doc.RootElement.GetProperty("max").GetDouble());
    }
}
