using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;

/// <summary>
///     v3 Batch 4 (B4-WP1) — the composer un-darkens BOTH subtrees. Verifies the Raw subtree
///     (Folder→Driver→Device→TagGroup container nodes + raw-tag Variables keyed <c>s=&lt;RawPath&gt;</c>,
///     realm <see cref="AddressSpaceRealm.Raw"/>) and the UNS subtree (one Variable per
///     <see cref="UnsTagReference"/> keyed <c>s=&lt;Area&gt;/&lt;Line&gt;/&lt;Equipment&gt;/&lt;EffectiveName&gt;</c>,
///     realm <see cref="AddressSpaceRealm.Uns"/>, carrying its backing RawPath for the Organizes ref +
///     fan-out). Native-alarm intents attach at the RAW tag and carry the referencing equipment paths.
/// </summary>
public sealed class AddressSpaceComposerDualNamespaceTests
{
    // A raw chain: RawFolder "Plant" → Driver "Modbus1" → Device "PLC-A" → Group "Motors" → tags.
    // Speed sits under the group; Status directly under the device; Levels is an array tag.
    private static AddressSpaceComposition ComposeFullFixture()
    {
        var folder = new RawFolder { RawFolderId = "raw-plant", ClusterId = "c1", Name = "Plant" };
        var driver = new DriverInstance
        {
            DriverInstanceId = "drv-modbus", ClusterId = "c1", RawFolderId = "raw-plant",
            Name = "Modbus1", DriverType = "Modbus", DriverConfig = "{}",
        };
        var device = new Device { DeviceId = "dev-1", DriverInstanceId = "drv-modbus", Name = "PLC-A", DeviceConfig = "{}" };
        var group = new TagGroup { TagGroupId = "g1", DeviceId = "dev-1", Name = "Motors" };

        var speed = new Tag
        {
            TagId = "t-speed", DeviceId = "dev-1", TagGroupId = "g1", Name = "Speed",
            DataType = "Float", AccessLevel = TagAccessLevel.ReadWrite,
            TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"speed_hist\"," +
                        "\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":700}}",
        };
        var status = new Tag
        {
            TagId = "t-status", DeviceId = "dev-1", Name = "Status",
            DataType = "Boolean", AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
        };
        var levels = new Tag
        {
            TagId = "t-levels", DeviceId = "dev-1", Name = "Levels",
            DataType = "Int32", AccessLevel = TagAccessLevel.Read,
            TagConfig = "{\"isArray\":true,\"arrayLength\":8}",
        };

        var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" };
        var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" };
        var equip = new Equipment { EquipmentId = "eq-1", UnsLineId = "line-1", Name = "station-1", MachineCode = "STATION_001" };

        var refSpeed = new UnsTagReference { UnsTagReferenceId = "ref-speed", EquipmentId = "eq-1", TagId = "t-speed" };
        var refStatus = new UnsTagReference
        {
            UnsTagReferenceId = "ref-status", EquipmentId = "eq-1", TagId = "t-status",
            DisplayNameOverride = "MachineStatus",
        };

        return AddressSpaceComposer.Compose(
            new[] { area }, new[] { line }, new[] { equip },
            new[] { driver }, Array.Empty<ScriptedAlarm>(),
            unsTagReferences: new[] { refSpeed, refStatus },
            tags: new[] { speed, status, levels },
            rawFolders: new[] { folder },
            devices: new[] { device },
            tagGroups: new[] { group });
    }

    /// <summary>The Raw subtree emits Folder/Driver/Device/TagGroup container nodes keyed by RawPath, each
    /// realm=Raw, with parents-before-children ordering + correct parent NodeIds.</summary>
    [Fact]
    public void Raw_container_nodes_materialise_with_rawpath_nodeids_and_parents()
    {
        var c = ComposeFullFixture();

        c.RawContainers.ShouldAllBe(n => n.Realm == AddressSpaceRealm.Raw);

        var folder = c.RawContainers.Single(n => n.Kind == RawNodeKind.Folder);
        folder.NodeId.ShouldBe("Plant");
        folder.ParentNodeId.ShouldBeNull(); // cluster root
        folder.DisplayName.ShouldBe("Plant");

        var driver = c.RawContainers.Single(n => n.Kind == RawNodeKind.Driver);
        driver.NodeId.ShouldBe("Plant/Modbus1");
        driver.ParentNodeId.ShouldBe("Plant");

        var device = c.RawContainers.Single(n => n.Kind == RawNodeKind.Device);
        device.NodeId.ShouldBe("Plant/Modbus1/PLC-A");
        device.ParentNodeId.ShouldBe("Plant/Modbus1");

        var group = c.RawContainers.Single(n => n.Kind == RawNodeKind.TagGroup);
        group.NodeId.ShouldBe("Plant/Modbus1/PLC-A/Motors");
        group.ParentNodeId.ShouldBe("Plant/Modbus1/PLC-A");

        // Sorted by NodeId ordinal ⇒ a parent always precedes each of its children.
        c.RawContainers.Select(n => n.NodeId)
            .ShouldBe(new[] { "Plant", "Plant/Modbus1", "Plant/Modbus1/PLC-A", "Plant/Modbus1/PLC-A/Motors" });
    }

    /// <summary>Raw tags are Variables keyed <c>s=&lt;RawPath&gt;</c> (realm=Raw), carrying DataType /
    /// writable / historize + historian tagname / array shape, and hang under their group (or device).</summary>
    [Fact]
    public void Raw_tags_materialise_as_variables_keyed_by_rawpath_with_carried_attributes()
    {
        var c = ComposeFullFixture();

        c.RawTags.ShouldAllBe(t => t.Realm == AddressSpaceRealm.Raw);

        var speed = c.RawTags.Single(t => t.TagId == "t-speed");
        speed.NodeId.ShouldBe("Plant/Modbus1/PLC-A/Motors/Speed");
        speed.ParentNodeId.ShouldBe("Plant/Modbus1/PLC-A/Motors");
        speed.DriverInstanceId.ShouldBe("drv-modbus");
        speed.DataType.ShouldBe("Float");
        speed.Writable.ShouldBeTrue();
        speed.IsHistorized.ShouldBeTrue();
        speed.HistorianTagname.ShouldBe("speed_hist");

        var status = c.RawTags.Single(t => t.TagId == "t-status");
        status.NodeId.ShouldBe("Plant/Modbus1/PLC-A/Status"); // no group ⇒ under the device
        status.ParentNodeId.ShouldBe("Plant/Modbus1/PLC-A");
        status.Writable.ShouldBeFalse();
        status.IsHistorized.ShouldBeFalse();

        var levels = c.RawTags.Single(t => t.TagId == "t-levels");
        levels.IsArray.ShouldBeTrue();
        levels.ArrayLength.ShouldBe(8u);
    }

    /// <summary>A native-alarm intent attaches at the RAW tag (ConditionId = RawPath) and carries the list
    /// of referencing equipment UNS folder paths — one alarm at the raw tag, not one per equipment.</summary>
    [Fact]
    public void Native_alarm_attaches_at_raw_tag_with_referencing_equipment_paths()
    {
        var c = ComposeFullFixture();

        var speed = c.RawTags.Single(t => t.TagId == "t-speed");
        speed.Alarm.ShouldNotBeNull();
        speed.Alarm!.AlarmType.ShouldBe("LimitAlarm");
        speed.Alarm.Severity.ShouldBe(700);
        speed.ReferencingEquipmentPaths.ShouldBe(new[] { "filling/line-1/station-1" });

        // A non-alarm tag carries no condition + its own referencing-equipment list.
        var status = c.RawTags.Single(t => t.TagId == "t-status");
        status.Alarm.ShouldBeNull();
        status.ReferencingEquipmentPaths.ShouldBe(new[] { "filling/line-1/station-1" });

        // An unreferenced tag has an empty referencing-equipment set.
        var levels = c.RawTags.Single(t => t.TagId == "t-levels");
        levels.ReferencingEquipmentPaths.ShouldBeEmpty();
    }

    /// <summary>Each UnsTagReference emits a UNS Variable keyed
    /// <c>s=&lt;Area&gt;/&lt;Line&gt;/&lt;Equipment&gt;/&lt;EffectiveName&gt;</c> (realm=Uns), carrying its backing
    /// RawPath (the Organizes target + fan-out) and inheriting DataType + writable from the raw tag. Effective
    /// name = DisplayNameOverride else the backing raw tag's Name.</summary>
    [Fact]
    public void Uns_reference_variables_carry_backing_rawpath_and_inherit_type_and_access()
    {
        var c = ComposeFullFixture();

        c.UnsReferenceVariables.ShouldAllBe(v => v.Realm == AddressSpaceRealm.Uns);

        var speedRef = c.UnsReferenceVariables.Single(v => v.UnsTagReferenceId == "ref-speed");
        speedRef.EffectiveName.ShouldBe("Speed"); // no override ⇒ backing raw tag Name
        speedRef.NodeId.ShouldBe("filling/line-1/station-1/Speed");
        speedRef.BackingRawPath.ShouldBe("Plant/Modbus1/PLC-A/Motors/Speed"); // Organizes UNS→Raw + fan-out
        speedRef.DataType.ShouldBe("Float");
        speedRef.Writable.ShouldBeTrue(); // inherited from the ReadWrite raw tag

        var statusRef = c.UnsReferenceVariables.Single(v => v.UnsTagReferenceId == "ref-status");
        statusRef.EffectiveName.ShouldBe("MachineStatus"); // display-name override wins
        statusRef.NodeId.ShouldBe("filling/line-1/station-1/MachineStatus");
        statusRef.BackingRawPath.ShouldBe("Plant/Modbus1/PLC-A/Status");
        statusRef.DataType.ShouldBe("Boolean");
        statusRef.Writable.ShouldBeFalse(); // inherited from the Read raw tag
    }

    /// <summary>A composition with no raw/UNS inputs leaves all three new subtree sets empty (the legacy
    /// convenience overloads + earlier callers keep working).</summary>
    [Fact]
    public void No_raw_or_uns_inputs_leaves_both_subtrees_empty()
    {
        var c = AddressSpaceComposer.Compose(
            equipment: Array.Empty<Equipment>(),
            driverInstances: Array.Empty<DriverInstance>(),
            scriptedAlarms: Array.Empty<ScriptedAlarm>());

        c.RawContainers.ShouldBeEmpty();
        c.RawTags.ShouldBeEmpty();
        c.UnsReferenceVariables.ShouldBeEmpty();
    }

    /// <summary>The Raw + UNS emit is deterministic — repeated calls produce element-identical output
    /// (RawTagPlan/UnsReferenceVariable compare by value, so ShouldBe is a content comparison).</summary>
    [Fact]
    public void Dual_subtree_emit_is_deterministic()
    {
        var a = ComposeFullFixture();
        var b = ComposeFullFixture();

        a.RawContainers.ShouldBe(b.RawContainers);
        a.RawTags.ShouldBe(b.RawTags);
        a.UnsReferenceVariables.ShouldBe(b.UnsReferenceVariables);
    }
}
