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.TemplateEngine.Flattening;

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

public class FlatteningServiceTests
{
    private readonly FlatteningService _sut = new();

    private static Instance CreateInstance(string name = "TestInstance", int templateId = 1, int siteId = 1) =>
        new(name) { Id = 1, TemplateId = templateId, SiteId = siteId };

    private static Template CreateTemplate(int id, string name, int? parentId = null)
    {
        var t = new Template(name) { Id = id, ParentTemplateId = parentId };
        return t;
    }

    [Fact]
    public void Flatten_EmptyTemplateChain_ReturnsFailure()
    {
        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsFailure);
        Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Flatten_SingleTemplate_ResolvesAttributes()
    {
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Temperature") { DataType = DataType.Double, Value = "25.0" });
        template.Attributes.Add(new TemplateAttribute("Status") { DataType = DataType.String, Value = "OK" });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Equal(2, result.Value.Attributes.Count);
        Assert.Equal("Temperature", result.Value.Attributes[1].CanonicalName); // Sorted
        Assert.Equal("25.0", result.Value.Attributes[1].Value);
        Assert.Equal("Status", result.Value.Attributes[0].CanonicalName);
    }

    [Fact]
    public void Flatten_InheritanceChain_DerivedOverridesBase()
    {
        var baseTemplate = CreateTemplate(2, "Base");
        baseTemplate.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Double, Value = "100.0" });
        baseTemplate.Attributes.Add(new TemplateAttribute("BaseOnly") { DataType = DataType.String, Value = "base" });

        var childTemplate = CreateTemplate(1, "Child", parentId: 2);
        childTemplate.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Double, Value = "200.0" });
        childTemplate.Attributes.Add(new TemplateAttribute("ChildOnly") { DataType = DataType.Int32, Value = "42" });

        // Chain: [child, base] — most-derived first
        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [childTemplate, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Equal(3, result.Value.Attributes.Count);

        var speed = result.Value.Attributes.First(a => a.CanonicalName == "Speed");
        Assert.Equal("200.0", speed.Value); // Child's value wins
    }

    [Fact]
    public void Flatten_LockedAttribute_NotOverriddenByDerived()
    {
        var baseTemplate = CreateTemplate(2, "Base");
        baseTemplate.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "locked", IsLocked = true });

        var childTemplate = CreateTemplate(1, "Child", parentId: 2);
        childTemplate.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "overridden" });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [childTemplate, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var locked = result.Value.Attributes.First(a => a.CanonicalName == "Locked");
        Assert.Equal("locked", locked.Value); // Base locked value preserved
    }

    [Fact]
    public void Flatten_InstanceOverride_AppliedToUnlockedAttribute()
    {
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Threshold") { DataType = DataType.Double, Value = "50.0" });

        var instance = CreateInstance();
        instance.AttributeOverrides.Add(new InstanceAttributeOverride("Threshold") { OverrideValue = "75.0" });

        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var attr = result.Value.Attributes.First(a => a.CanonicalName == "Threshold");
        Assert.Equal("75.0", attr.Value);
        Assert.Equal("Override", attr.Source);
    }

    [Fact]
    public void Flatten_InstanceOverride_SkippedForLockedAttribute()
    {
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "original", IsLocked = true });

        var instance = CreateInstance();
        instance.AttributeOverrides.Add(new InstanceAttributeOverride("Locked") { OverrideValue = "changed" });

        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var attr = result.Value.Attributes.First(a => a.CanonicalName == "Locked");
        Assert.Equal("original", attr.Value); // Lock honored
    }

    [Fact]
    public void Flatten_ComposedModule_PathQualifiedNames()
    {
        var composedTemplate = CreateTemplate(2, "Pump");
        composedTemplate.Attributes.Add(new TemplateAttribute("RPM") { DataType = DataType.Double, Value = "1500" });
        composedTemplate.Scripts.Add(new TemplateScript("Start", "// start") { Id = 10 });

        var parentTemplate = CreateTemplate(1, "Station");
        parentTemplate.Attributes.Add(new TemplateAttribute("StationName") { DataType = DataType.String, Value = "S1" });

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition>
            {
                new("MainPump") { ComposedTemplateId = 2 }
            }
        };

        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [composedTemplate]
        };

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [parentTemplate],
            compositions,
            composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "MainPump.RPM");
        Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "StationName");
        Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "MainPump.Start");
    }

    [Fact]
    public void Flatten_ConnectionBindings_ResolvedCorrectly()
    {
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Temp")
        {
            DataType = DataType.Double,
            DataSourceReference = "ns=2;s=Temperature"
        });

        var instance = CreateInstance();
        instance.ConnectionBindings.Add(new InstanceConnectionBinding("Temp") { DataConnectionId = 100 });

        var connections = new Dictionary<int, DataConnection>
        {
            [100] = new("OPC-Server1", "OpcUa", 1) { Id = 100, PrimaryConfiguration = "opc.tcp://localhost:4840" }
        };

        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            connections);

        Assert.True(result.IsSuccess);
        var attr = result.Value.Attributes.First(a => a.CanonicalName == "Temp");
        Assert.Equal(100, attr.BoundDataConnectionId);
        Assert.Equal("OPC-Server1", attr.BoundDataConnectionName);
        Assert.Equal("OpcUa", attr.BoundDataConnectionProtocol);
    }

    [Fact]
    public void Flatten_Alarms_ResolvedFromChain()
    {
        var template = CreateTemplate(1, "Base");
        template.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
            PriorityLevel = 1
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Single(result.Value.Alarms);
        Assert.Equal("HighTemp", result.Value.Alarms[0].CanonicalName);
        Assert.Equal("RangeViolation", result.Value.Alarms[0].TriggerType);
    }

    [Fact]
    public void Flatten_InstanceMetadata_SetCorrectly()
    {
        var template = CreateTemplate(1, "Base");
        var instance = CreateInstance("MyInstance", templateId: 1, siteId: 5);
        instance.AreaId = 3;

        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Equal("MyInstance", result.Value.InstanceUniqueName);
        Assert.Equal(1, result.Value.TemplateId);
        Assert.Equal(5, result.Value.SiteId);
        Assert.Equal(3, result.Value.AreaId);
    }

    [Fact]
    public void Flatten_InheritedAttributeOnDerived_BaseValueWins()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Attributes.Add(new TemplateAttribute("SetPoint") { DataType = DataType.Double, Value = "100.0" });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Attributes.Add(new TemplateAttribute("SetPoint")
        {
            DataType = DataType.Double,
            Value = "STALE",
            IsInherited = true
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var setPoint = result.Value.Attributes.First(a => a.CanonicalName == "SetPoint");
        Assert.Equal("100.0", setPoint.Value);
    }

    [Fact]
    public void Flatten_OverriddenAttributeOnDerived_DerivedValueWins()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Attributes.Add(new TemplateAttribute("SetPoint") { DataType = DataType.Double, Value = "100.0" });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Attributes.Add(new TemplateAttribute("SetPoint")
        {
            DataType = DataType.Double,
            Value = "150.0",
            IsInherited = false
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var setPoint = result.Value.Attributes.First(a => a.CanonicalName == "SetPoint");
        Assert.Equal("150.0", setPoint.Value);
    }

    [Fact]
    public void Flatten_LockedInDerivedOverride_Fails()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Attributes.Add(new TemplateAttribute("SetPoint")
        {
            DataType = DataType.Double,
            Value = "100.0",
            LockedInDerived = true
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Attributes.Add(new TemplateAttribute("SetPoint")
        {
            DataType = DataType.Double,
            Value = "150.0",
            IsInherited = false
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsFailure);
        Assert.Contains("LockedInDerived", result.Error);
        Assert.Contains("SetPoint", result.Error);
    }

    [Fact]
    public void Flatten_InheritedScriptOnDerived_BaseCodeWins()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Scripts.Add(new TemplateScript("Sample", "return base;"));

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Scripts.Add(new TemplateScript("Sample", "stale code") { IsInherited = true });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var script = result.Value.Scripts.First(s => s.CanonicalName == "Sample");
        Assert.Equal("return base;", script.Code);
    }

    // ── M2.5 (#9): per-script execution timeout threads to ResolvedScript ───

    [Fact]
    public void Flatten_SingleTemplate_ScriptExecutionTimeoutSecondsThreadsThrough()
    {
        var template = CreateTemplate(1, "Base");
        template.Scripts.Add(new TemplateScript("Slow", "// slow") { ExecutionTimeoutSeconds = 5 });
        template.Scripts.Add(new TemplateScript("Default", "// default")); // null → use global

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var slow = result.Value.Scripts.First(s => s.CanonicalName == "Slow");
        Assert.Equal(5, slow.ExecutionTimeoutSeconds);
        var dflt = result.Value.Scripts.First(s => s.CanonicalName == "Default");
        Assert.Null(dflt.ExecutionTimeoutSeconds);
    }

    [Fact]
    public void Flatten_DerivedScriptOverride_ExecutionTimeoutFollowsWinningRow()
    {
        // Scripts inherit/override at whole-row granularity: an explicit override
        // row on the derived template (IsInherited = false) fully replaces the
        // base row, so its ExecutionTimeoutSeconds wins — exactly like the body.
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Scripts.Add(new TemplateScript("Sample", "return base;")
        {
            ExecutionTimeoutSeconds = 10
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Scripts.Add(new TemplateScript("Sample", "return derived;")
        {
            ExecutionTimeoutSeconds = 3
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var script = result.Value.Scripts.First(s => s.CanonicalName == "Sample");
        Assert.Equal("return derived;", script.Code);
        Assert.Equal(3, script.ExecutionTimeoutSeconds);
    }

    [Fact]
    public void Flatten_InheritedScriptOnDerived_ExecutionTimeoutFollowsBaseRow()
    {
        // A stale inherited copy on the derived template (IsInherited = true) is
        // ignored; the base row wins, carrying the base ExecutionTimeoutSeconds.
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Scripts.Add(new TemplateScript("Sample", "return base;")
        {
            ExecutionTimeoutSeconds = 10
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Scripts.Add(new TemplateScript("Sample", "stale code")
        {
            IsInherited = true,
            ExecutionTimeoutSeconds = 3
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var script = result.Value.Scripts.First(s => s.CanonicalName == "Sample");
        Assert.Equal("return base;", script.Code);
        Assert.Equal(10, script.ExecutionTimeoutSeconds);
    }

    // ── TemplateEngine-002: per-slot alarm override ────────────────────────

    [Fact]
    public void Flatten_InheritedAlarmOnDerived_BaseValueWins()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
            PriorityLevel = 5
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":999}",
            PriorityLevel = 99,
            IsInherited = true
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var alarm = result.Value.Alarms.First(a => a.CanonicalName == "HighTemp");
        Assert.Equal(5, alarm.PriorityLevel);
        Assert.Equal("{\"attributeName\":\"Temp\",\"high\":100}", alarm.TriggerConfiguration);
    }

    [Fact]
    public void Flatten_OverriddenAlarmOnDerived_DerivedValueWins()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
            PriorityLevel = 5
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":120}",
            PriorityLevel = 42,
            IsInherited = false
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var alarm = result.Value.Alarms.First(a => a.CanonicalName == "HighTemp");
        Assert.Equal(42, alarm.PriorityLevel);
        Assert.Equal("{\"attributeName\":\"Temp\",\"high\":120}", alarm.TriggerConfiguration);
    }

    [Fact]
    public void Flatten_LockedInDerivedAlarmOverride_Fails()
    {
        var baseTemplate = CreateTemplate(2, "Sensor");
        baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
            PriorityLevel = 5,
            LockedInDerived = true
        });

        var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
        derived.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":120}",
            PriorityLevel = 42,
            IsInherited = false
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [derived, baseTemplate],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsFailure);
        Assert.Contains("LockedInDerived", result.Error);
        Assert.Contains("HighTemp", result.Error);
    }

    // ── TemplateEngine-001: deep composition nesting ───────────────────────

    [Fact]
    public void Flatten_ThreeLevelComposition_AttributesAlarmsScriptsAllResolved()
    {
        // Station composes Pump (level 1); Pump composes Motor (level 2);
        // Motor composes Bearing (level 3).
        var bearing = CreateTemplate(4, "Bearing");
        bearing.Attributes.Add(new TemplateAttribute("Vibration") { DataType = DataType.Double, Value = "0.1" });
        bearing.Alarms.Add(new TemplateAlarm("HighVibration")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Vibration\",\"high\":5}",
            PriorityLevel = 1
        });
        bearing.Scripts.Add(new TemplateScript("MonitorBearing", "// monitor") { Id = 40 });

        var motor = CreateTemplate(3, "Motor");
        motor.Attributes.Add(new TemplateAttribute("Current") { DataType = DataType.Double, Value = "10" });

        var pump = CreateTemplate(2, "Pump");
        pump.Attributes.Add(new TemplateAttribute("RPM") { DataType = DataType.Double, Value = "1500" });

        var station = CreateTemplate(1, "Station");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("MainPump") { ComposedTemplateId = 2 } },
            [2] = new List<TemplateComposition> { new("DriveMotor") { ComposedTemplateId = 3 } },
            [3] = new List<TemplateComposition> { new("FrontBearing") { ComposedTemplateId = 4 } },
        };
        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [pump],
            [3] = [motor],
            [4] = [bearing],
        };

        var instance = CreateInstance();
        var result = _sut.Flatten(instance, [station], compositions, composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        // Level 3 attribute must be present with the full path-qualified name.
        Assert.Contains(result.Value.Attributes,
            a => a.CanonicalName == "MainPump.DriveMotor.FrontBearing.Vibration");
        // Level 3 alarm must be present (was dropped entirely before).
        Assert.Contains(result.Value.Alarms,
            a => a.CanonicalName == "MainPump.DriveMotor.FrontBearing.HighVibration");
        // Level 3 script must be present (was dropped entirely before).
        Assert.Contains(result.Value.Scripts,
            s => s.CanonicalName == "MainPump.DriveMotor.FrontBearing.MonitorBearing");
    }

    [Fact]
    public void Flatten_NestedComposedAlarm_TriggerAttributePrefixed()
    {
        var bearing = CreateTemplate(4, "Bearing");
        bearing.Attributes.Add(new TemplateAttribute("Vibration") { DataType = DataType.Double, Value = "0.1" });
        bearing.Alarms.Add(new TemplateAlarm("HighVibration")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Vibration\",\"high\":5}",
            PriorityLevel = 1
        });

        var motor = CreateTemplate(3, "Motor");
        var pump = CreateTemplate(2, "Pump");
        var station = CreateTemplate(1, "Station");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("MainPump") { ComposedTemplateId = 2 } },
            [2] = new List<TemplateComposition> { new("DriveMotor") { ComposedTemplateId = 3 } },
            [3] = new List<TemplateComposition> { new("FrontBearing") { ComposedTemplateId = 4 } },
        };
        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [pump], [3] = [motor], [4] = [bearing],
        };

        var instance = CreateInstance();
        var result = _sut.Flatten(instance, [station], compositions, composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var alarm = result.Value.Alarms.First(a => a.CanonicalName.EndsWith("HighVibration"));
        // The trigger's attribute reference must carry the full nested prefix.
        Assert.Contains("MainPump.DriveMotor.FrontBearing.Vibration", alarm.TriggerConfiguration);
    }

    // ── TemplateEngine-004: alarm on-trigger script resolution ─────────────

    [Fact]
    public void Flatten_AlarmOnTriggerScript_ResolvedToCanonicalName()
    {
        var template = CreateTemplate(1, "Base");
        template.Scripts.Add(new TemplateScript("HandleAlarm", "// handle") { Id = 50 });
        template.Alarms.Add(new TemplateAlarm("HighTemp")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
            PriorityLevel = 1,
            OnTriggerScriptId = 50
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(instance, [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var alarm = result.Value.Alarms.First(a => a.CanonicalName == "HighTemp");
        Assert.Equal("HandleAlarm", alarm.OnTriggerScriptCanonicalName);
    }

    [Fact]
    public void Flatten_ComposedAlarmOnTriggerScript_ResolvedWithPrefix()
    {
        var composedTemplate = CreateTemplate(2, "Pump");
        composedTemplate.Scripts.Add(new TemplateScript("PumpAlarmHandler", "// h") { Id = 60 });
        composedTemplate.Alarms.Add(new TemplateAlarm("PumpFault")
        {
            TriggerType = AlarmTriggerType.ValueMatch,
            TriggerConfiguration = "{\"attributeName\":\"State\",\"value\":\"FAULT\"}",
            PriorityLevel = 5,
            OnTriggerScriptId = 60
        });

        var station = CreateTemplate(1, "Station");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("MainPump") { ComposedTemplateId = 2 } }
        };
        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [composedTemplate]
        };

        var instance = CreateInstance();
        var result = _sut.Flatten(instance, [station], compositions, composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var alarm = result.Value.Alarms.First(a => a.CanonicalName == "MainPump.PumpFault");
        Assert.Equal("MainPump.PumpAlarmHandler", alarm.OnTriggerScriptCanonicalName);
    }

    // ── TemplateEngine-016: composed-script ScriptScope.ParentPath ─────────

    [Fact]
    public void Flatten_NestedComposedScript_ScopeCarriesCorrectParentPath()
    {
        // Station composes Pump (level 1); Pump composes Motor (level 2).
        // The depth-1 script's parent is the root template (ParentPath "");
        // the depth-2 script's parent is the Pump module (ParentPath "MainPump").
        var motor = CreateTemplate(3, "Motor");
        motor.Scripts.Add(new TemplateScript("MonitorMotor", "// m") { Id = 70 });

        var pump = CreateTemplate(2, "Pump");
        pump.Scripts.Add(new TemplateScript("MonitorPump", "// p") { Id = 71 });

        var station = CreateTemplate(1, "Station");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("MainPump") { ComposedTemplateId = 2 } },
            [2] = new List<TemplateComposition> { new("DriveMotor") { ComposedTemplateId = 3 } },
        };
        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [pump], [3] = [motor],
        };

        var instance = CreateInstance();
        var result = _sut.Flatten(instance, [station], compositions, composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);

        var depth1 = result.Value.Scripts.First(s => s.CanonicalName == "MainPump.MonitorPump");
        Assert.Equal("MainPump", depth1.Scope.SelfPath);
        Assert.Equal("", depth1.Scope.ParentPath);

        var depth2 = result.Value.Scripts.First(s => s.CanonicalName == "MainPump.DriveMotor.MonitorMotor");
        Assert.Equal("MainPump.DriveMotor", depth2.Scope.SelfPath);
        // Parent module of a depth-2 script is the enclosing Pump module.
        Assert.Equal("MainPump", depth2.Scope.ParentPath);
    }

    [Fact]
    public void Flatten_ResolvesNativeAlarmSources_FromTemplate()
    {
        var template = CreateTemplate(1, "Base");
        template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
        { ConnectionName = "Opc", SourceReference = "ns=2;s=P1" });
        var result = _sut.Flatten(CreateInstance(), [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());
        Assert.True(result.IsSuccess);
        Assert.Single(result.Value.NativeAlarmSources);
        Assert.Equal("Pressure", result.Value.NativeAlarmSources[0].CanonicalName);
        Assert.Equal("ns=2;s=P1", result.Value.NativeAlarmSources[0].SourceReference);
    }

    [Fact]
    public void Flatten_InstanceOverride_ReplacesNativeAlarmSourceReference()
    {
        var template = CreateTemplate(1, "Base");
        template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
        { ConnectionName = "Opc", SourceReference = "ns=2;s=DEFAULT" });
        var instance = CreateInstance();
        instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
        { SourceReferenceOverride = "ns=2;s=Tank07" });
        var result = _sut.Flatten(instance, [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());
        Assert.True(result.IsSuccess);
        Assert.Equal("ns=2;s=Tank07", result.Value.NativeAlarmSources[0].SourceReference);
        Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
    }

    [Fact]
    public void Flatten_LockedNativeAlarmSource_IgnoresInstanceOverride()
    {
        // #05-T13: a native alarm source locked at the template level must not be
        // repointed by an instance override — the resolved source keeps the template
        // values and its Source stays "Template" (mirrors attribute/alarm lock rules).
        var template = CreateTemplate(1, "Base");
        template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
        { ConnectionName = "Opc", SourceReference = "ns=2;s=DEFAULT", IsLocked = true });
        var instance = CreateInstance();
        instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
        { ConnectionNameOverride = "OtherOpc", SourceReferenceOverride = "ns=2;s=Tank07" });

        var result = _sut.Flatten(instance, [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var src = result.Value.NativeAlarmSources[0];
        Assert.Equal("Opc", src.ConnectionName);          // override ignored
        Assert.Equal("ns=2;s=DEFAULT", src.SourceReference); // override ignored
        Assert.Equal("Template", src.Source);
        Assert.True(src.IsLocked);
    }

    // ── MV-4: ElementDataType carried through flatten/override ────────────

    [Fact]
    public void Flatten_ListAttribute_ElementDataTypeCarriedToResolvedAttribute()
    {
        // A template with a List attribute whose ElementDataType is String.
        // The flattened result must carry DataType == "List" and ElementDataType == "String".
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Tags")
        {
            DataType = DataType.List,
            ElementDataType = DataType.String,
            Value = null
        });

        var instance = CreateInstance();
        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var attr = result.Value.Attributes.First(a => a.CanonicalName == "Tags");
        Assert.Equal("List", attr.DataType);
        Assert.Equal("String", attr.ElementDataType);
    }

    [Fact]
    public void Flatten_ListAttributeWithInstanceOverride_ElementDataTypePreservedThroughOverride()
    {
        // An instance override replaces the VALUE of a List attribute.
        // The ElementDataType must survive the override path unchanged.
        var template = CreateTemplate(1, "Base");
        template.Attributes.Add(new TemplateAttribute("Tags")
        {
            DataType = DataType.List,
            ElementDataType = DataType.String,
            Value = "[\"a\",\"b\"]"
        });

        var instance = CreateInstance();
        instance.AttributeOverrides.Add(new InstanceAttributeOverride("Tags")
        {
            OverrideValue = "[\"x\",\"y\",\"z\"]"
        });

        var result = _sut.Flatten(
            instance,
            [template],
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        var attr = result.Value.Attributes.First(a => a.CanonicalName == "Tags");
        Assert.Equal("[\"x\",\"y\",\"z\"]", attr.Value);      // override applied
        Assert.Equal("Override", attr.Source);               // came from override path
        Assert.Equal("List", attr.DataType);                 // type unchanged
        Assert.Equal("String", attr.ElementDataType);        // element type preserved
    }

    // ── Repeated composition: same template under multiple slots ──────────

    [Fact]
    public void Flatten_SameTemplateComposedTwice_ResolvesNestedMembersUnderBothSlots()
    {
        // Root(1) composes X(2) as "x"; X composes Y(3) as "y1" AND "y2";
        // Y composes Z(4) as "z"; Z carries one member of each kind.
        // Because y1 and y2 share one recursion subtree, a globally-keyed cycle
        // guard would descend into Z under y1 but skip it under y2, dropping the
        // whole x.y2.z.* subtree. The path-keyed guard resolves both.
        var z = CreateTemplate(4, "Z");
        z.Attributes.Add(new TemplateAttribute("Val") { DataType = DataType.Double, Value = "1" });
        z.Alarms.Add(new TemplateAlarm("Alm")
        {
            TriggerType = AlarmTriggerType.RangeViolation,
            TriggerConfiguration = "{\"attributeName\":\"Val\",\"high\":100}",
            PriorityLevel = 1
        });
        z.Scripts.Add(new TemplateScript("Run", "// run") { Id = 40 });
        z.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        { ConnectionName = "Opc", SourceReference = "ns=2;s=Z" });

        var y = CreateTemplate(3, "Y");
        var x = CreateTemplate(2, "X");
        var root = CreateTemplate(1, "Root");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("x") { ComposedTemplateId = 2 } },
            [2] = new List<TemplateComposition>
            {
                new("y1") { ComposedTemplateId = 3 },
                new("y2") { ComposedTemplateId = 3 }
            },
            [3] = new List<TemplateComposition> { new("z") { ComposedTemplateId = 4 } }
        };

        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [2] = [x],
            [3] = [y],
            [4] = [z]
        };

        var result = _sut.Flatten(
            CreateInstance(),
            [root],
            compositions,
            composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);

        Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "x.y1.z.Val");
        Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "x.y2.z.Val");

        Assert.Contains(result.Value.Alarms, a => a.CanonicalName == "x.y1.z.Alm");
        Assert.Contains(result.Value.Alarms, a => a.CanonicalName == "x.y2.z.Alm");

        Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "x.y1.z.Run");
        Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "x.y2.z.Run");

        Assert.Contains(result.Value.NativeAlarmSources, s => s.CanonicalName == "x.y1.z.Src");
        Assert.Contains(result.Value.NativeAlarmSources, s => s.CanonicalName == "x.y2.z.Src");
    }

    [Fact]
    public void Flatten_CompositionCycle_StillTerminates()
    {
        // X(1) composes Y(2) as "y"; Y composes X(1) as "x" — a composition
        // cycle. The path-keyed guard must still break the cycle (a template
        // already on the CURRENT path) so flattening terminates.
        var x = CreateTemplate(1, "X");
        x.Attributes.Add(new TemplateAttribute("Root") { DataType = DataType.String, Value = "r" });
        var y = CreateTemplate(2, "Y");

        var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
        {
            [1] = new List<TemplateComposition> { new("y") { ComposedTemplateId = 2 } },
            [2] = new List<TemplateComposition> { new("x") { ComposedTemplateId = 1 } }
        };

        var composedChains = new Dictionary<int, IReadOnlyList<Template>>
        {
            [1] = [x],
            [2] = [y]
        };

        var result = _sut.Flatten(
            CreateInstance(),
            [x],
            compositions,
            composedChains,
            new Dictionary<int, DataConnection>());

        Assert.True(result.IsSuccess);
        Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "Root");
    }
}
