using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;

namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa;

[Trait("Category", "Unit")]
public sealed class EquipmentNodeWalkerTests
{
    [Fact]
    public void Walk_EmptyContent_EmitsNothing()
    {
        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, new EquipmentNamespaceContent([], [], [], []));

        rec.Children.ShouldBeEmpty();
    }

    [Fact]
    public void Walk_EmitsArea_Line_Equipment_Folders_In_UnsOrder()
    {
        var content = new EquipmentNamespaceContent(
            Areas: [Area("area-1", "warsaw"), Area("area-2", "berlin")],
            Lines: [Line("line-1", "area-1", "oven-line"), Line("line-2", "area-2", "press-line")],
            Equipment: [Eq("eq-1", "line-1", "oven-3"), Eq("eq-2", "line-2", "press-7")],
            Tags: []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        rec.Children.Select(c => c.BrowseName).ShouldBe(["berlin", "warsaw"]);   // ordered by Name
        var warsaw = rec.Children.First(c => c.BrowseName == "warsaw");
        warsaw.Children.Select(c => c.BrowseName).ShouldBe(["oven-line"]);
        warsaw.Children[0].Children.Select(c => c.BrowseName).ShouldBe(["oven-3"]);
    }

    [Fact]
    public void Walk_AddsFiveIdentifierProperties_OnEquipmentNode_Skipping_NullZTagSapid()
    {
        var uuid = Guid.NewGuid();
        var eq = Eq("eq-1", "line-1", "oven-3");
        eq.EquipmentUuid = uuid;
        eq.MachineCode = "MC-42";
        eq.ZTag = null;
        eq.SAPID = null;
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        var props = equipmentNode.Properties.Select(p => p.BrowseName).ToList();
        props.ShouldContain("EquipmentId");
        props.ShouldContain("EquipmentUuid");
        props.ShouldContain("MachineCode");
        props.ShouldNotContain("ZTag");
        props.ShouldNotContain("SAPID");
        equipmentNode.Properties.First(p => p.BrowseName == "EquipmentUuid").Value.ShouldBe(uuid.ToString());
    }

    [Fact]
    public void Walk_Adds_ZTag_And_SAPID_When_Present()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        eq.ZTag = "ZT-0042";
        eq.SAPID = "10000042";
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        equipmentNode.Properties.First(p => p.BrowseName == "ZTag").Value.ShouldBe("ZT-0042");
        equipmentNode.Properties.First(p => p.BrowseName == "SAPID").Value.ShouldBe("10000042");
    }

    [Fact]
    public void Walk_Materializes_Identification_Subfolder_When_AnyFieldPresent()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        eq.Manufacturer = "Trumpf";
        eq.Model = "TruLaser-3030";
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        var identification = equipmentNode.Children.FirstOrDefault(c => c.BrowseName == "Identification");
        identification.ShouldNotBeNull();
        identification!.Properties.Select(p => p.BrowseName).ShouldContain("Manufacturer");
        identification.Properties.Select(p => p.BrowseName).ShouldContain("Model");
    }

    [Fact]
    public void Walk_Omits_Identification_Subfolder_When_AllFieldsNull()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");  // no identification fields
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        equipmentNode.Children.ShouldNotContain(c => c.BrowseName == "Identification");
    }

    [Fact]
    public void Walk_Emits_Variable_Per_BoundTag_Under_Equipment()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var tag1 = NewTag("tag-1", "Temperature", "Int32", "plcaddr-01", equipmentId: "eq-1");
        var tag2 = NewTag("tag-2", "Setpoint", "Float32", "plcaddr-02", equipmentId: "eq-1");
        var unboundTag = NewTag("tag-3", "Orphan", "Int32", "plcaddr-03", equipmentId: null); // SystemPlatform-style, walker skips
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
            [eq], [tag1, tag2, unboundTag]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        equipmentNode.Variables.Count.ShouldBe(2);
        equipmentNode.Variables.Select(v => v.BrowseName).ShouldBe(["Setpoint", "Temperature"]);
        equipmentNode.Variables.First(v => v.BrowseName == "Temperature").AttributeInfo.FullName.ShouldBe("plcaddr-01");
        equipmentNode.Variables.First(v => v.BrowseName == "Setpoint").AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Float32);
    }

    [Fact]
    public void Walk_FallsBack_To_String_For_Unparseable_DataType()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var tag = NewTag("tag-1", "Mystery", "NotARealType", "plcaddr-42", equipmentId: "eq-1");
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], [tag]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var variable = rec.Children[0].Children[0].Children[0].Variables.Single();
        variable.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.String);
    }

    [Fact]
    public void Walk_Emits_VirtualTag_Variables_With_Virtual_Source_Discriminator()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var vtag = new VirtualTag
        {
            VirtualTagRowId = Guid.NewGuid(), GenerationId = 1,
            VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "LineRate",
            DataType = "Float32", ScriptId = "scr-1", Historize = true,
        };
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
            [eq], [], VirtualTags: [vtag]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var equipmentNode = rec.Children[0].Children[0].Children[0];
        var v = equipmentNode.Variables.Single(x => x.BrowseName == "LineRate");
        v.AttributeInfo.Source.ShouldBe(NodeSourceKind.Virtual);
        v.AttributeInfo.VirtualTagId.ShouldBe("vt-1");
        v.AttributeInfo.ScriptedAlarmId.ShouldBeNull();
        v.AttributeInfo.IsHistorized.ShouldBeTrue();
        v.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Float32);
    }

    [Fact]
    public void Walk_Emits_ScriptedAlarm_Variables_With_ScriptedAlarm_Source_And_IsAlarm()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var alarm = new ScriptedAlarm
        {
            ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1,
            ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "HighTemp",
            AlarmType = "LimitAlarm", MessageTemplate = "{Temp} exceeded",
            PredicateScriptId = "scr-9", Severity = 800,
        };
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
            [eq], [], ScriptedAlarms: [alarm]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var v = rec.Children[0].Children[0].Children[0].Variables.Single(x => x.BrowseName == "HighTemp");
        v.AttributeInfo.Source.ShouldBe(NodeSourceKind.ScriptedAlarm);
        v.AttributeInfo.ScriptedAlarmId.ShouldBe("al-1");
        v.AttributeInfo.VirtualTagId.ShouldBeNull();
        v.AttributeInfo.IsAlarm.ShouldBeTrue();
        v.AttributeInfo.DriverDataType.ShouldBe(DriverDataType.Boolean);
    }

    [Fact]
    public void Walk_Skips_Disabled_VirtualTags_And_Alarms()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var vtag = new VirtualTag
        {
            VirtualTagRowId = Guid.NewGuid(), GenerationId = 1,
            VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Disabled",
            DataType = "Float32", ScriptId = "scr-1", Enabled = false,
        };
        var alarm = new ScriptedAlarm
        {
            ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1,
            ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "DisabledAlarm",
            AlarmType = "LimitAlarm", MessageTemplate = "x",
            PredicateScriptId = "scr-9", Enabled = false,
        };
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
            [eq], [], VirtualTags: [vtag], ScriptedAlarms: [alarm]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        rec.Children[0].Children[0].Children[0].Variables.ShouldBeEmpty();
    }

    [Fact]
    public void Walk_Null_VirtualTags_And_ScriptedAlarms_Is_Safe()
    {
        // Backwards-compat — callers that don't populate the new collections still work.
        var eq = Eq("eq-1", "line-1", "oven-3");
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")], [eq], []);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);  // must not throw

        rec.Children[0].Children[0].Children[0].Variables.ShouldBeEmpty();
    }

    [Fact]
    public void Driver_tag_default_NodeSourceKind_is_Driver()
    {
        var eq = Eq("eq-1", "line-1", "oven-3");
        var tag = NewTag("t-1", "Temp", "Int32", "plc-01", "eq-1");
        var content = new EquipmentNamespaceContent(
            [Area("area-1", "warsaw")], [Line("line-1", "area-1", "line-a")],
            [eq], [tag]);

        var rec = new RecordingBuilder("root");
        EquipmentNodeWalker.Walk(rec, content);

        var v = rec.Children[0].Children[0].Children[0].Variables.Single();
        v.AttributeInfo.Source.ShouldBe(NodeSourceKind.Driver);
        v.AttributeInfo.VirtualTagId.ShouldBeNull();
        v.AttributeInfo.ScriptedAlarmId.ShouldBeNull();
    }

    // ----- builders for test seed rows -----

    private static UnsArea Area(string id, string name) => new()
    {
        UnsAreaId = id, ClusterId = "c1", Name = name, GenerationId = 1,
    };

    private static UnsLine Line(string id, string areaId, string name) => new()
    {
        UnsLineId = id, UnsAreaId = areaId, Name = name, GenerationId = 1,
    };

    private static Equipment Eq(string equipmentId, string lineId, string name) => new()
    {
        EquipmentRowId = Guid.NewGuid(),
        GenerationId = 1,
        EquipmentId = equipmentId,
        EquipmentUuid = Guid.NewGuid(),
        DriverInstanceId = "drv",
        UnsLineId = lineId,
        Name = name,
        MachineCode = "MC-" + name,
    };

    private static Tag NewTag(string tagId, string name, string dataType, string address, string? equipmentId) => new()
    {
        TagRowId = Guid.NewGuid(),
        GenerationId = 1,
        TagId = tagId,
        DriverInstanceId = "drv",
        EquipmentId = equipmentId,
        Name = name,
        DataType = dataType,
        AccessLevel = ZB.MOM.WW.OtOpcUa.Configuration.Enums.TagAccessLevel.ReadWrite,
        TagConfig = address,
    };

    // ----- recording IAddressSpaceBuilder -----

    private sealed class RecordingBuilder(string browseName) : IAddressSpaceBuilder
    {
        public string BrowseName { get; } = browseName;
        public List<RecordingBuilder> Children { get; } = new();
        public List<RecordingVariable> Variables { get; } = new();
        public List<RecordingProperty> Properties { get; } = new();

        public IAddressSpaceBuilder Folder(string name, string _)
        {
            var child = new RecordingBuilder(name);
            Children.Add(child);
            return child;
        }

        public IVariableHandle Variable(string name, string _, DriverAttributeInfo attr)
        {
            var v = new RecordingVariable(name, attr);
            Variables.Add(v);
            return v;
        }

        public void AddProperty(string name, DriverDataType _, object? value) =>
            Properties.Add(new RecordingProperty(name, value));
    }

    private sealed record RecordingProperty(string BrowseName, object? Value);

    private sealed record RecordingVariable(string BrowseName, DriverAttributeInfo AttributeInfo) : IVariableHandle
    {
        public string FullReference => AttributeInfo.FullName;
        public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => throw new NotSupportedException();
    }
}
