using System.Linq;
using System.Text;
using System.Text.Json;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;

public sealed class DeploymentArtifactTests
{
    private static byte[] BlobOf(object snapshot) =>
        System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(snapshot);

    private static object MultiClusterSnapshot() => new
    {
        Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
        Nodes = new[]
        {
            new { NodeId = "central-1:4053", ClusterId = "MAIN" },
            new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
        },
        DriverInstances = new[]
        {
            new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "main-galaxy", Name = "g", DriverType = "GalaxyMxGateway", Enabled = true, DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
            new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "sa-modbus",  Name = "m", DriverType = "Modbus",          Enabled = true, DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
        },
    };

    /// <summary>Verifies a single-cluster artifact resolves to None (apply everything).</summary>
    [Fact]
    public void ResolveClusterScope_single_cluster_artifact_returns_None()
    {
        var blob = BlobOf(new { Clusters = new[] { new { ClusterId = "MAIN" } }, Nodes = Array.Empty<object>() });
        var scope = DeploymentArtifact.ResolveClusterScope(blob, "central-1:4053");
        scope.Mode.ShouldBe(ClusterFilterMode.None);
    }

    /// <summary>Verifies a multi-cluster artifact scopes a known node to its own ClusterId.</summary>
    [Fact]
    public void ResolveClusterScope_multi_cluster_known_node_scopes_to_its_cluster()
    {
        var scope = DeploymentArtifact.ResolveClusterScope(BlobOf(MultiClusterSnapshot()), "site-a-1:4053");
        scope.Mode.ShouldBe(ClusterFilterMode.ScopeTo);
        scope.ClusterId.ShouldBe("SITE-A");
    }

    /// <summary>Verifies a multi-cluster artifact suppresses an unknown node.</summary>
    [Fact]
    public void ResolveClusterScope_multi_cluster_unknown_node_suppresses()
    {
        var scope = DeploymentArtifact.ResolveClusterScope(BlobOf(MultiClusterSnapshot()), "ghost-9:4053");
        scope.Mode.ShouldBe(ClusterFilterMode.Suppress);
    }

    /// <summary>Verifies the scoped parse returns only the node's own cluster's drivers.</summary>
    [Fact]
    public void ParseDriverInstances_scoped_returns_only_my_clusters_drivers()
    {
        var specs = DeploymentArtifact.ParseDriverInstances(BlobOf(MultiClusterSnapshot()), "central-1:4053");
        specs.Select(s => s.DriverInstanceId).ShouldBe(new[] { "main-galaxy" });
    }

    /// <summary>Verifies the scoped parse returns nothing for an unknown node.</summary>
    [Fact]
    public void ParseDriverInstances_scoped_unknown_node_returns_empty()
    {
        var specs = DeploymentArtifact.ParseDriverInstances(BlobOf(MultiClusterSnapshot()), "ghost-9:4053");
        specs.ShouldBeEmpty();
    }

    /// <summary>Verifies the scoped parse returns all drivers for a single-cluster artifact.</summary>
    [Fact]
    public void ParseDriverInstances_scoped_single_cluster_returns_all()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "MAIN" } },
            Nodes = new[] { new { NodeId = "n1:4053", ClusterId = "MAIN" } },
            DriverInstances = new[] { new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "d1", Name = "d", DriverType = "Modbus", Enabled = true, DriverConfig = "{}", ClusterId = "MAIN" } },
        });
        DeploymentArtifact.ParseDriverInstances(blob, "anything:4053").Select(s => s.DriverInstanceId).ShouldBe(new[] { "d1" });
    }
    /// <summary>Verifies that empty blob returns empty list.</summary>
    [Fact]
    public void Empty_blob_returns_empty_list()
    {
        DeploymentArtifact.ParseDriverInstances(ReadOnlySpan<byte>.Empty).ShouldBeEmpty();
    }

    /// <summary>Verifies that malformed JSON returns empty list.</summary>
    [Fact]
    public void Malformed_json_returns_empty_list()
    {
        DeploymentArtifact.ParseDriverInstances(Encoding.UTF8.GetBytes("not json")).ShouldBeEmpty();
    }

    /// <summary>Verifies that snapshot without DriverInstances returns empty.</summary>
    [Fact]
    public void Snapshot_without_DriverInstances_returns_empty()
    {
        var blob = Encoding.UTF8.GetBytes("{\"Clusters\":[]}");
        DeploymentArtifact.ParseDriverInstances(blob).ShouldBeEmpty();
    }

    /// <summary>Verifies that driver instances are parsed from composer-shaped blob.</summary>
    [Fact]
    public void Parses_driver_instances_from_composer_shaped_blob()
    {
        // Mirrors the shape ConfigComposer.SnapshotAndFlattenAsync emits — Pascal-case fields
        // serialised directly off the EF entity.
        var rowId = Guid.NewGuid();
        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            DriverInstances = new[]
            {
                new
                {
                    DriverInstanceRowId = rowId,
                    DriverInstanceId = "DI-modbus-1",
                    Name = "Modbus Line A",
                    DriverType = "Modbus",
                    Enabled = true,
                    DriverConfig = "{\"host\":\"127.0.0.1\"}",
                },
                new
                {
                    DriverInstanceRowId = Guid.NewGuid(),
                    DriverInstanceId = "DI-disabled",
                    Name = "Decommissioned",
                    DriverType = "AbCip",
                    Enabled = false,
                    DriverConfig = "{}",
                },
            },
        });

        var specs = DeploymentArtifact.ParseDriverInstances(blob);

        specs.Count.ShouldBe(2);
        specs[0].DriverInstanceRowId.ShouldBe(rowId);
        specs[0].DriverInstanceId.ShouldBe("DI-modbus-1");
        specs[0].DriverType.ShouldBe("Modbus");
        specs[0].Enabled.ShouldBeTrue();
        specs[0].DriverConfig.ShouldContain("127.0.0.1");
        specs[1].Enabled.ShouldBeFalse();
    }

    /// <summary>Verifies that ParseComposition returns empty for empty blob.</summary>
    [Fact]
    public void ParseComposition_returns_empty_for_empty_blob()
    {
        var c = DeploymentArtifact.ParseComposition(ReadOnlySpan<byte>.Empty);
        c.EquipmentNodes.ShouldBeEmpty();
        c.DriverInstancePlans.ShouldBeEmpty();
        c.ScriptedAlarmPlans.ShouldBeEmpty();
    }

    /// <summary>Verifies that ParseComposition reads all three entity classes sorted by ID.</summary>
    [Fact]
    public void ParseComposition_reads_all_three_entity_classes_sorted_by_id()
    {
        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Equipment = new[]
            {
                new { EquipmentId = "eq-z", MachineCode = "Z", UnsLineId = "line-1" },
                new { EquipmentId = "eq-a", MachineCode = "A", UnsLineId = "line-1" },
            },
            DriverInstances = new[]
            {
                new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}" },
            },
            ScriptedAlarms = new[]
            {
                new
                {
                    ScriptedAlarmId = "alarm-1",
                    EquipmentId = "eq-a",
                    PredicateScriptId = "script-1",
                    MessageTemplate = "high",
                },
            },
        });

        var c = DeploymentArtifact.ParseComposition(blob);

        c.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "eq-a", "eq-z" });
        c.DriverInstancePlans.Single().DriverInstanceId.ShouldBe("drv-1");
        c.ScriptedAlarmPlans.Single().ScriptedAlarmId.ShouldBe("alarm-1");
    }

    /// <summary>
    ///     Verifies ParseComposition surfaces Equipment-namespace VirtualTags (joined to their Script
    ///     by ScriptId for the expression source) as <c>EquipmentVirtualTags</c>, with the
    ///     <c>DependencyRefs</c> extracted from the script's <c>ctx.GetTag("…")</c> literals — the
    ///     artifact-decode mirror of <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer.
    /// </summary>
    [Fact]
    public void ParseComposition_reads_EquipmentVirtualTags_from_virtualtags_and_scripts()
    {
        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Scripts = new[]
            {
                new
                {
                    ScriptId = "scr-1",
                    SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
                },
            },
            VirtualTags = new[]
            {
                new
                {
                    VirtualTagId = "vt-1",
                    EquipmentId = "eq-1",
                    Name = "Doubled",
                    DataType = "Float",
                    ScriptId = "scr-1",
                },
            },
        });

        var c = DeploymentArtifact.ParseComposition(blob);

        var vt = c.EquipmentVirtualTags.ShouldHaveSingleItem();
        vt.VirtualTagId.ShouldBe("vt-1");
        vt.EquipmentId.ShouldBe("eq-1");
        vt.Name.ShouldBe("Doubled");
        vt.DataType.ShouldBe("Float");
        vt.FolderPath.ShouldBe("");
        vt.Expression.ShouldBe("return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;");
        vt.DependencyRefs.ShouldBe(new[] { "TestMachine_001.TestDouble" });
    }

    /// <summary>
    ///     Verifies ParseComposition sets the equipment folder DisplayName to the UNS <c>Name</c>
    ///     segment — the source the live rebuild actually uses — not the colloquial MachineCode, so
    ///     equipment browses by its friendly UNS name. NodeId stays the logical EquipmentId.
    /// </summary>
    [Fact]
    public void ParseComposition_equipment_DisplayName_is_UNS_Name_not_MachineCode()
    {
        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            Equipment = new[]
            {
                new { EquipmentId = "eq-1", Name = "filling-eq", MachineCode = "FILLING-EQ", UnsLineId = "line-1" },
            },
        });

        var node = DeploymentArtifact.ParseComposition(blob).EquipmentNodes.ShouldHaveSingleItem();

        node.EquipmentId.ShouldBe("eq-1");
        node.DisplayName.ShouldBe("filling-eq");
    }

    /// <summary>Verifies that specs missing required fields are dropped.</summary>
    [Fact]
    public void Spec_missing_required_fields_is_dropped()
    {
        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            DriverInstances = new object[]
            {
                new { Name = "no-id" },
                new
                {
                    DriverInstanceId = "DI-ok",
                    DriverType = "Modbus",
                    DriverConfig = "{}",
                },
            },
        });

        var specs = DeploymentArtifact.ParseDriverInstances(blob);

        specs.Single().DriverInstanceId.ShouldBe("DI-ok");
    }

    /// <summary>Verifies that a malformed blob resolves to None rather than throwing.</summary>
    [Fact]
    public void ResolveClusterScope_malformed_blob_returns_None()
    {
        var scope = DeploymentArtifact.ResolveClusterScope("not json"u8.ToArray(), "central-1:4053");
        scope.Mode.ShouldBe(ClusterFilterMode.None);
    }

    /// <summary>Verifies that a blank ClusterId in the node row resolves to Suppress.</summary>
    [Fact]
    public void ResolveClusterScope_blank_cluster_id_suppresses()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
            Nodes = new[] { new { NodeId = "central-1:4053", ClusterId = "" } },
        });
        DeploymentArtifact.ResolveClusterScope(blob, "central-1:4053").Mode.ShouldBe(ClusterFilterMode.Suppress);
    }

    /// <summary>Verifies that NodeId matching in ResolveClusterScope is case-insensitive.</summary>
    [Fact]
    public void ResolveClusterScope_node_id_match_is_case_insensitive()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
            Nodes = new[] { new { NodeId = "Central-1:4053", ClusterId = "MAIN" } },
        });
        var scope = DeploymentArtifact.ResolveClusterScope(blob, "central-1:4053");
        scope.Mode.ShouldBe(ClusterFilterMode.ScopeTo);
        scope.ClusterId.ShouldBe("MAIN");
    }

    private static object MultiClusterSnapshotWithTags() => new
    {
        Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
        Nodes = new[]
        {
            new { NodeId = "central-1:4053", ClusterId = "MAIN" },
            new { NodeId = "site-a-1:4053",  ClusterId = "SITE-A" },
        },
        DriverInstances = new[]
        {
            new { DriverInstanceId = "main-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "MAIN",   NamespaceId = "main-ns" },
            new { DriverInstanceId = "sa-galaxy",   DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
        },
        // Galaxy points are ordinary equipment tags now — Equipment-kind namespaces with non-null
        // EquipmentId, so the cluster-scoped decode filters them via EquipmentTags (by their driver's
        // cluster), exactly as it filtered the retired GalaxyTags.
        Namespaces = new[]
        {
            new { NamespaceId = "main-ns", ClusterId = "MAIN",   Kind = 0 },
            new { NamespaceId = "sa-ns",   ClusterId = "SITE-A", Kind = 0 },
        },
        Tags = new[]
        {
            new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)"eq-main", Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
            new { TagId = "t-sa",   DriverInstanceId = "sa-galaxy",   EquipmentId = (string?)"eq-sa",   Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
        },
    };

    [Fact]
    public void ParseComposition_scoped_keeps_only_my_clusters_drivers()
    {
        // v3: DriverInstance cluster scoping still holds. The EquipmentTags leg of the original assertion
        // is dark this batch (equipment-tag plans don't materialize until Batch 4) — see
        // TagConfigCorpusParityTests + DarkAddressSpaceReasons.
        var blob = BlobOf(MultiClusterSnapshotWithTags());

        var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
        main.DriverInstancePlans.Select(d => d.DriverInstanceId).ShouldBe(new[] { "main-galaxy" });
        main.EquipmentTags.ShouldBeEmpty(); // dark this batch

        var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
        siteA.DriverInstancePlans.Select(d => d.DriverInstanceId).ShouldBe(new[] { "sa-galaxy" });
        siteA.EquipmentTags.ShouldBeEmpty(); // dark this batch
    }

    [Fact]
    public void ParseComposition_scoped_unknown_node_is_empty()
    {
        var comp = DeploymentArtifact.ParseComposition(BlobOf(MultiClusterSnapshotWithTags()), "ghost-9:4053");
        comp.EquipmentTags.ShouldBeEmpty();
        comp.DriverInstancePlans.ShouldBeEmpty();
    }

    /// <summary>Verifies the cluster-scoped overload keeps only EquipmentVirtualTags whose EquipmentId
    /// belongs to an in-cluster driver (mirroring how EquipmentTags + ScriptedAlarms are filtered).</summary>
    [Fact]
    public void ParseComposition_scoped_keeps_only_my_clusters_virtual_tags()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
            Nodes = new[]
            {
                new { NodeId = "central-1:4053", ClusterId = "MAIN" },
                new { NodeId = "site-a-1:4053",  ClusterId = "SITE-A" },
            },
            DriverInstances = new[]
            {
                new { DriverInstanceId = "main-modbus", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN",   NamespaceId = "main-ns" },
                new { DriverInstanceId = "sa-modbus",   DriverType = "Modbus", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
            },
            // v3: equipment carries no driver binding — it is attributed to a cluster solely via its UNS
            // line's area cluster (Equipment -> UnsLine.UnsAreaId -> UnsArea.ClusterId).
            UnsAreas = new[]
            {
                new { UnsAreaId = "a-main", Name = "AreaMain", ClusterId = "MAIN" },
                new { UnsAreaId = "a-sa",   Name = "AreaSa",   ClusterId = "SITE-A" },
            },
            UnsLines = new[]
            {
                new { UnsLineId = "l1", UnsAreaId = "a-main", Name = "L1" },
                new { UnsLineId = "l2", UnsAreaId = "a-sa",   Name = "L2" },
            },
            Equipment = new[]
            {
                new { EquipmentId = "eq-main", Name = "eqm", UnsLineId = "l1" },
                new { EquipmentId = "eq-sa",   Name = "eqs", UnsLineId = "l2" },
            },
            Scripts = new[]
            {
                new { ScriptId = "scr", SourceCode = "return 1;" },
            },
            VirtualTags = new[]
            {
                new { VirtualTagId = "vt-main", EquipmentId = "eq-main", Name = "VM", DataType = "Float", ScriptId = "scr" },
                new { VirtualTagId = "vt-sa",   EquipmentId = "eq-sa",   Name = "VS", DataType = "Float", ScriptId = "scr" },
            },
        });

        var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
        main.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-main" });

        var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
        siteA.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-sa" });
    }

    [Fact]
    public void ParseComposition_single_cluster_node_id_overload_matches_legacy()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "MAIN" } },
            Nodes = new[] { new { NodeId = "n1:4053", ClusterId = "MAIN" } },
            DriverInstances = new[] { new { DriverInstanceId = "d1", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "ns" } },
        });
        DeploymentArtifact.ParseComposition(blob, "anything:4053").DriverInstancePlans.Count
            .ShouldBe(DeploymentArtifact.ParseComposition(blob).DriverInstancePlans.Count);
    }

    /// <summary>An artifact where an equipment's driver is in the node's cluster but its UNS line's area
    /// is in another cluster: <paramref name="areaCluster"/> controls the area's ClusterId.</summary>
    private static byte[] OrphanEquipmentBlob(string areaCluster) => BlobOf(new
    {
        Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
        Nodes = new[] { new { NodeId = "central-1:4053", ClusterId = "MAIN" } },
        DriverInstances = new[]
        {
            new { DriverInstanceId = "main-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
        },
        UnsAreas = new[] { new { UnsAreaId = "area-1", Name = "Area1", ClusterId = areaCluster } },
        UnsLines = new[] { new { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "Line1" } },
        Equipment = new[]
        {
            new { EquipmentId = "equip-1", Name = "Equip1", UnsLineId = "line-1", DriverInstanceId = "main-driver" },
        },
    });

    /// <summary>Verifies the inconsistency callback fires when a kept equipment's UNS line belongs to
    /// another cluster (a cross-cluster orphan binding).</summary>
    [Fact(Skip = DarkAddressSpaceReasons.EquipmentDeviceBindingRetired)]
    public void ParseComposition_scoped_flags_cross_cluster_orphan_equipment()
    {
        var warnings = new List<string>();
        var comp = DeploymentArtifact.ParseComposition(
            OrphanEquipmentBlob(areaCluster: "SITE-A"), "central-1:4053", warnings.Add);

        comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "equip-1" }); // still returned
        warnings.Count.ShouldBe(1);
        warnings[0].ShouldContain("equip-1");
        warnings[0].ShouldContain("line-1");
    }

    /// <summary>Verifies the inconsistency callback does NOT fire when the equipment, its driver, and its
    /// UNS line/area are all in the same cluster (the normal, invariant-respecting case).</summary>
    [Fact]
    public void ParseComposition_scoped_consistent_equipment_does_not_warn()
    {
        var warnings = new List<string>();
        var comp = DeploymentArtifact.ParseComposition(
            OrphanEquipmentBlob(areaCluster: "MAIN"), "central-1:4053", warnings.Add);

        comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "equip-1" });
        comp.UnsLines.Select(l => l.UnsLineId).ShouldBe(new[] { "line-1" });
        warnings.ShouldBeEmpty();
    }

    /// <summary>A multi-cluster artifact with one driver-less equipment (no <c>DriverInstanceId</c>
    /// property at all => null driver) attributed only via its UNS line's area cluster:
    /// UnsArea A1 (ClusterId = <paramref name="areaCluster"/>), UnsLine L1 (UnsAreaId = A1),
    /// Equipment E1 (UnsLineId = L1, no driver), plus an EquipmentVirtualTag on E1. The MAIN-cluster
    /// node always carries a driver so the artifact is genuinely multi-cluster (scoped, not None).</summary>
    private static byte[] DriverlessEquipmentBlob(string areaCluster) => BlobOf(new
    {
        Clusters = new[] { new { ClusterId = "C1" }, new { ClusterId = "C2" } },
        Nodes = new[]
        {
            new { NodeId = "c1-1:4053", ClusterId = "C1" },
            new { NodeId = "c2-1:4053", ClusterId = "C2" },
        },
        DriverInstances = new[]
        {
            new { DriverInstanceId = "c1-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C1", NamespaceId = "c1-ns" },
            new { DriverInstanceId = "c2-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C2", NamespaceId = "c2-ns" },
        },
        UnsAreas = new[] { new { UnsAreaId = "A1", Name = "Area1", ClusterId = areaCluster } },
        UnsLines = new[] { new { UnsLineId = "L1", UnsAreaId = "A1", Name = "Line1" } },
        // Driver-less equipment: the DriverInstanceId property is absent => the parser sees null.
        Equipment = new[]
        {
            new { EquipmentId = "E1", Name = "Equip1", UnsLineId = "L1" },
        },
        Scripts = new[] { new { ScriptId = "scr", SourceCode = "return 1;" } },
        VirtualTags = new[]
        {
            new { VirtualTagId = "vt-e1", EquipmentId = "E1", Name = "VE1", DataType = "Float", ScriptId = "scr" },
        },
    });

    /// <summary>Verifies driver-less equipment (null DriverInstanceId) IS kept when its UNS line's area
    /// is in the node's cluster — attributed via the line→area→cluster chain rather than via a driver.
    /// Both the EquipmentNode and its EquipmentVirtualTag must survive the scoped parse.</summary>
    [Fact]
    public void Driverless_equipment_kept_when_its_line_area_is_in_cluster()
    {
        var comp = DeploymentArtifact.ParseComposition(
            DriverlessEquipmentBlob(areaCluster: "C1"), "c1-1:4053");

        comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "E1" });
        comp.EquipmentVirtualTags.Select(v => v.VirtualTagId).ShouldBe(new[] { "vt-e1" });
    }

    /// <summary>Verifies driver-less equipment is EXCLUDED when its UNS line's area is in another cluster:
    /// the node scopes to C1 but area A1 (and thus line L1, equipment E1) lives in C2.</summary>
    [Fact]
    public void Driverless_equipment_excluded_when_its_line_area_in_other_cluster()
    {
        var comp = DeploymentArtifact.ParseComposition(
            DriverlessEquipmentBlob(areaCluster: "C2"), "c1-1:4053");

        comp.EquipmentNodes.ShouldBeEmpty();
        comp.EquipmentVirtualTags.ShouldBeEmpty();
    }

    /// <summary>Verifies driver-less equipment with no UnsLineId (null/absent) is EXCLUDED from the scoped
    /// composition and throws nothing: without a line there is no area chain to resolve a cluster from.</summary>
    [Fact]
    public void Driverless_equipment_excluded_when_it_has_no_line()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "C1" }, new { ClusterId = "C2" } },
            Nodes = new[]
            {
                new { NodeId = "c1-1:4053", ClusterId = "C1" },
                new { NodeId = "c2-1:4053", ClusterId = "C2" },
            },
            DriverInstances = new[]
            {
                // One driver per cluster so the artifact is genuinely multi-cluster (scoped, not None).
                new { DriverInstanceId = "c1-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C1", NamespaceId = "c1-ns" },
                new { DriverInstanceId = "c2-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C2", NamespaceId = "c2-ns" },
            },
            UnsAreas = new[] { new { UnsAreaId = "A1", Name = "Area1", ClusterId = "C1" } },
            // Equipment has no DriverInstanceId (driver-less) and no UnsLineId — cannot resolve a cluster.
            Equipment = new[]
            {
                new { EquipmentId = "E-noline", Name = "NoLine" },
            },
            Scripts = new[] { new { ScriptId = "scr", SourceCode = "return 1;" } },
            VirtualTags = new[]
            {
                new { VirtualTagId = "vt-noline", EquipmentId = "E-noline", Name = "VNL", DataType = "Float", ScriptId = "scr" },
            },
        });

        var comp = DeploymentArtifact.ParseComposition(blob, "c1-1:4053");

        comp.EquipmentNodes.ShouldBeEmpty();
        comp.EquipmentVirtualTags.ShouldBeEmpty();
    }

    /// <summary>RETIRED (v3): equipment no longer carries a DriverInstanceId, so there is no driver-bound
    /// attribution path — equipment is attributed to a cluster solely via its UNS line's area
    /// (Driverless_equipment_kept_when_its_line_area_is_in_cluster is the v3 replacement, and passes).
    /// The original premise ("attribution is still by driver") no longer exists.</summary>
    [Fact(Skip = DarkAddressSpaceReasons.EquipmentDeviceBindingRetired)]
    public void Driverbound_equipment_kept_when_driver_in_cluster()
    {
        var blob = BlobOf(new
        {
            Clusters = new[] { new { ClusterId = "C1" }, new { ClusterId = "C2" } },
            Nodes = new[]
            {
                new { NodeId = "c1-1:4053", ClusterId = "C1" },
                new { NodeId = "c2-1:4053", ClusterId = "C2" },
            },
            DriverInstances = new[]
            {
                new { DriverInstanceId = "c1-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C1", NamespaceId = "c1-ns" },
                new { DriverInstanceId = "c2-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "C2", NamespaceId = "c2-ns" },
            },
            Equipment = new[]
            {
                new { EquipmentId = "E-c1", Name = "Eq-c1", UnsLineId = "l1", DriverInstanceId = "c1-driver" },
                new { EquipmentId = "E-c2", Name = "Eq-c2", UnsLineId = "l2", DriverInstanceId = "c2-driver" },
            },
        });

        var comp = DeploymentArtifact.ParseComposition(blob, "c1-1:4053");
        comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "E-c1" });
    }

    /// <summary>Verifies the per-instance ResilienceConfig column is carried onto the parsed spec.</summary>
    [Fact]
    public void ParseDriverInstances_carries_ResilienceConfig_onto_the_spec()
    {
        var resilienceJson = "{\"bulkheadMaxConcurrent\":16,\"capabilityPolicies\":{\"Write\":{\"retryCount\":0}}}";
        var blob = BlobOf(new
        {
            DriverInstances = new[]
            {
                new { DriverInstanceId = "d1", DriverType = "Modbus", Enabled = true, DriverConfig = "{}", ResilienceConfig = resilienceJson },
            },
        });

        var spec = DeploymentArtifact.ParseDriverInstances(blob).Single();

        spec.ResilienceConfig.ShouldBe(resilienceJson);
    }

    /// <summary>Verifies an absent ResilienceConfig column leaves the spec field null (⇒ tier defaults at spawn).</summary>
    [Fact]
    public void ParseDriverInstances_absent_ResilienceConfig_is_null()
    {
        var blob = BlobOf(new
        {
            DriverInstances = new[]
            {
                new { DriverInstanceId = "d1", DriverType = "Modbus", Enabled = true, DriverConfig = "{}" },
            },
        });

        var spec = DeploymentArtifact.ParseDriverInstances(blob).Single();

        spec.ResilienceConfig.ShouldBeNull();
    }
}
