using System.Text.Json;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

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

/// <summary>
///     v3 WP4 byte-parity: the artifact-decode seam (<c>DeploymentArtifact.ParseComposition</c>) and the
///     live compose seam (<c>AddressSpaceComposer.Compose</c>) must resolve <c>{{equip}}/&lt;RefName&gt;</c>
///     script paths to the SAME backing RawPath — both build the per-equipment reference map (effective name
///     → RawPath) from <c>UnsTagReferences</c> + <c>Tags</c> + the raw topology via the shared
///     <c>EquipmentReferenceMap</c> + <c>RawPathResolver</c>. Exercises both a VirtualTag script and a
///     scripted-alarm predicate, plus an override-named reference.
/// </summary>
public sealed class DeploymentArtifactEquipRefParityTests
{
    // Reference effective name "MotorSpeed" (a DisplayNameOverride) backing raw tag leaf "Speed" →
    // RawPath "Modbus/Dev1/Speed" (driver at cluster root, no folder / group).
    private const string VtScript = "return System.Convert.ToInt32(ctx.GetTag(\"{{equip}}/MotorSpeed\").Value) > 50;";
    private const string AlarmScript = "return System.Convert.ToDouble(ctx.GetTag(\"{{equip}}/MotorSpeed\").Value) > 90;";
    private const string ExpectedRawPath = "Modbus/Dev1/Speed";

    private static DriverInstance Driver() => new()
    {
        DriverInstanceId = "drv-1", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus",
        DriverConfig = "{}", RawFolderId = null,
    };

    private static Device Dev() => new()
    {
        DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}",
    };

    private static Tag RawTag() => new()
    {
        TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float",
        AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
    };

    private static UnsTagReference Ref() => new()
    {
        UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed",
    };

    [Fact]
    public void VirtualTag_equip_ref_resolves_byte_parity()
    {
        var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = VtScript, SourceHash = "h" };
        var vt = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" };

        var composed = AddressSpaceComposer.Compose(
            Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
            new[] { Driver() }, Array.Empty<ScriptedAlarm>(),
            virtualTags: new[] { vt }, scripts: new[] { script },
            unsTagReferences: new[] { Ref() }, tags: new[] { RawTag() },
            rawFolders: Array.Empty<RawFolder>(), devices: new[] { Dev() }, tagGroups: Array.Empty<TagGroup>());

        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = (string?)null } },
            Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
            Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
            UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
            Scripts = new[] { new { ScriptId = "s-1", SourceCode = VtScript } },
            VirtualTags = new[] { new { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" } },
        });

        var decoded = DeploymentArtifact.ParseComposition(blob);

        // The token resolved to the backing RawPath on BOTH sides, and the plans are byte-identical.
        var cPlan = composed.EquipmentVirtualTags.ShouldHaveSingleItem();
        cPlan.Expression.ShouldContain($"ctx.GetTag(\"{ExpectedRawPath}\")");
        cPlan.Expression.ShouldNotContain("{{equip}}");
        cPlan.DependencyRefs.ShouldBe(new[] { ExpectedRawPath });

        var dPlan = decoded.EquipmentVirtualTags.ShouldHaveSingleItem();
        dPlan.ShouldBe(cPlan);
    }

    [Fact]
    public void ScriptedAlarm_predicate_equip_ref_resolves_byte_parity()
    {
        var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = AlarmScript, SourceHash = "h" };
        var alarm = new ScriptedAlarm
        {
            ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "Hot", AlarmType = "LimitAlarm",
            Severity = 700, MessageTemplate = "hot", PredicateScriptId = "s-1",
            HistorizeToAveva = true, Retain = true, Enabled = true,
        };

        var composed = AddressSpaceComposer.Compose(
            Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
            new[] { Driver() }, new[] { alarm },
            virtualTags: Array.Empty<VirtualTag>(), scripts: new[] { script },
            unsTagReferences: new[] { Ref() }, tags: new[] { RawTag() },
            rawFolders: Array.Empty<RawFolder>(), devices: new[] { Dev() }, tagGroups: Array.Empty<TagGroup>());

        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = (string?)null } },
            Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
            Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
            UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
            Scripts = new[] { new { ScriptId = "s-1", SourceCode = AlarmScript } },
            ScriptedAlarms = new[]
            {
                new { ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "Hot", AlarmType = "LimitAlarm", Severity = 700, MessageTemplate = "hot", PredicateScriptId = "s-1", HistorizeToAveva = true, Retain = true, Enabled = true },
            },
        });

        var decoded = DeploymentArtifact.ParseComposition(blob);

        var cPlan = composed.EquipmentScriptedAlarms.ShouldHaveSingleItem();
        cPlan.PredicateSource.ShouldContain($"ctx.GetTag(\"{ExpectedRawPath}\")");
        cPlan.PredicateSource.ShouldNotContain("{{equip}}");
        cPlan.DependencyRefs.ShouldBe(new[] { ExpectedRawPath });

        var dPlan = decoded.EquipmentScriptedAlarms.ShouldHaveSingleItem();
        dPlan.ShouldBe(cPlan);
    }

    /// <summary>Wave-B review LOW-3 edge: an UNRESOLVED <c>{{equip}}/&lt;RefName&gt;</c> (no matching reference)
    /// must be left literally intact — identically — on both seams (the deploy gate rejects it separately; the
    /// compose seams must not diverge). Here the only reference is "MotorSpeed", so "Nope" cannot resolve.</summary>
    [Fact]
    public void VirtualTag_equip_ref_unresolved_left_intact_byte_parity()
    {
        const string unresolvedScript = "return ctx.GetTag(\"{{equip}}/Nope\").Value;";
        var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = unresolvedScript, SourceHash = "h" };
        var vt = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Double", ScriptId = "s-1" };

        var composed = AddressSpaceComposer.Compose(
            Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
            new[] { Driver() }, Array.Empty<ScriptedAlarm>(),
            virtualTags: new[] { vt }, scripts: new[] { script },
            unsTagReferences: new[] { Ref() }, tags: new[] { RawTag() },
            rawFolders: Array.Empty<RawFolder>(), devices: new[] { Dev() }, tagGroups: Array.Empty<TagGroup>());

        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = (string?)null } },
            Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
            Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
            UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
            Scripts = new[] { new { ScriptId = "s-1", SourceCode = unresolvedScript } },
            VirtualTags = new[] { new { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Double", ScriptId = "s-1" } },
        });

        var decoded = DeploymentArtifact.ParseComposition(blob);

        var cPlan = composed.EquipmentVirtualTags.ShouldHaveSingleItem();
        cPlan.Expression.ShouldContain("{{equip}}/Nope"); // left intact — not substituted
        var dPlan = decoded.EquipmentVirtualTags.ShouldHaveSingleItem();
        dPlan.ShouldBe(cPlan);
    }

    /// <summary>Wave-B review LOW-3 edge: RawPath ancestry (folder + tag-group) must resolve identically on both
    /// seams. Driver under folder "Cell1", tag under group "Fast" → RawPath "Cell1/Modbus/Dev1/Fast/Speed".</summary>
    [Fact]
    public void VirtualTag_equip_ref_resolves_through_folder_and_group_byte_parity()
    {
        const string deepRawPath = "Cell1/Modbus/Dev1/Fast/Speed";
        var driver = new DriverInstance { DriverInstanceId = "drv-1", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", RawFolderId = "fld-1" };
        var folder = new RawFolder { RawFolderId = "fld-1", ClusterId = "c1", Name = "Cell1", ParentRawFolderId = null };
        var group = new TagGroup { TagGroupId = "grp-1", DeviceId = "dev-1", Name = "Fast", ParentTagGroupId = null };
        var tag = new Tag { TagId = "tag-1", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}" };
        var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = VtScript, SourceHash = "h" };
        var vt = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" };

        var composed = AddressSpaceComposer.Compose(
            Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
            new[] { driver }, Array.Empty<ScriptedAlarm>(),
            virtualTags: new[] { vt }, scripts: new[] { script },
            unsTagReferences: new[] { Ref() }, tags: new[] { tag },
            rawFolders: new[] { folder }, devices: new[] { Dev() }, tagGroups: new[] { group });

        var blob = JsonSerializer.SerializeToUtf8Bytes(new
        {
            RawFolders = new[] { new { RawFolderId = "fld-1", ParentRawFolderId = (string?)null, Name = "Cell1" } },
            DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = "fld-1" } },
            Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
            TagGroups = new[] { new { TagGroupId = "grp-1", ParentTagGroupId = (string?)null, Name = "Fast" } },
            Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
            UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
            Scripts = new[] { new { ScriptId = "s-1", SourceCode = VtScript } },
            VirtualTags = new[] { new { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" } },
        });

        var decoded = DeploymentArtifact.ParseComposition(blob);

        var cPlan = composed.EquipmentVirtualTags.ShouldHaveSingleItem();
        cPlan.Expression.ShouldContain($"ctx.GetTag(\"{deepRawPath}\")");
        cPlan.DependencyRefs.ShouldBe(new[] { deepRawPath });
        var dPlan = decoded.EquipmentVirtualTags.ShouldHaveSingleItem();
        dPlan.ShouldBe(cPlan);
    }
}
