using Shouldly;
using Xunit;

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

/// <summary>
///     R2-07 T1 — table-driven tests over <see cref="AddressSpaceChangeClassifier.Classify"/>. The
///     classifier is a pure function over an <see cref="AddressSpacePlan"/>: it names the delta class the
///     applier routes each plan through (Empty / AttributeOnly / PureAdd / PureRemove / AddRemoveMix /
///     Rebuild). First-match-wins per the plan's classification table, with the default-closed
///     Rebuild safety valve catching every node-affecting change.
/// </summary>
public sealed class AddressSpaceChangeClassifierTests
{
    [Fact]
    public void Empty_plan_classifies_as_Empty()
    {
        AddressSpaceChangeClassifier.Classify(Plan()).ShouldBe(AddressSpaceChangeKind.Empty);
    }

    // ----- single add class ⇒ PureAdd -----

    [Fact]
    public void Added_equipment_only_is_PureAdd()
    {
        var plan = Plan(addedEquipment: new[] { Eq("eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    [Fact]
    public void Added_alarm_only_is_PureAdd()
    {
        var plan = Plan(addedAlarms: new[] { Alarm("alm-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    [Fact]
    public void Added_equipment_tag_only_is_PureAdd()
    {
        var plan = Plan(addedTags: new[] { Tag("tag-1", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    [Fact]
    public void Added_virtual_tag_only_is_PureAdd()
    {
        var plan = Plan(addedVtags: new[] { Vtag("vt-1", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    // ----- single remove class ⇒ PureRemove -----

    [Fact]
    public void Removed_equipment_only_is_PureRemove()
    {
        var plan = Plan(removedEquipment: new[] { Eq("eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
    }

    [Fact]
    public void Removed_alarm_only_is_PureRemove()
    {
        var plan = Plan(removedAlarms: new[] { Alarm("alm-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
    }

    [Fact]
    public void Removed_equipment_tag_only_is_PureRemove()
    {
        var plan = Plan(removedTags: new[] { Tag("tag-1", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
    }

    [Fact]
    public void Removed_virtual_tag_only_is_PureRemove()
    {
        var plan = Plan(removedVtags: new[] { Vtag("vt-1", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureRemove);
    }

    // ----- add AND remove ⇒ AddRemoveMix -----

    [Fact]
    public void Add_and_remove_is_AddRemoveMix()
    {
        var plan = Plan(addedTags: new[] { Tag("tag-1", "eq-1") }, removedTags: new[] { Tag("tag-2", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AddRemoveMix);
    }

    [Fact]
    public void Add_equipment_and_remove_vtag_is_AddRemoveMix()
    {
        var plan = Plan(addedEquipment: new[] { Eq("eq-2") }, removedVtags: new[] { Vtag("vt-1", "eq-1") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AddRemoveMix);
    }

    // ----- node-affecting change ⇒ Rebuild -----

    [Fact]
    public void Changed_equipment_is_Rebuild()
    {
        var plan = Plan(changedEquipment: new[]
        {
            new AddressSpacePlan.EquipmentDelta(Eq("eq-1"), Eq("eq-1") with { DisplayName = "New" }),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
    }

    [Fact]
    public void Changed_alarm_is_Rebuild()
    {
        var plan = Plan(changedAlarms: new[]
        {
            new AddressSpacePlan.AlarmDelta(Alarm("alm-1"), Alarm("alm-1") with { MessageTemplate = "changed" }),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
    }

    [Fact]
    public void Non_surgical_changed_tag_is_Rebuild()
    {
        // FullName re-routes the node to a different driver point ⇒ NOT surgical-eligible ⇒ Rebuild.
        var plan = Plan(changedTags: new[]
        {
            new AddressSpacePlan.EquipmentTagDelta(
                Tag("tag-1", "eq-1", fullName: "40001"),
                Tag("tag-1", "eq-1", fullName: "40002")),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
    }

    [Fact]
    public void Node_relevant_changed_vtag_is_Rebuild()
    {
        // Name change re-derives the NodeId ⇒ node-relevant ⇒ Rebuild.
        var plan = Plan(changedVtags: new[]
        {
            new AddressSpacePlan.EquipmentVirtualTagDelta(
                Vtag("vt-1", "eq-1", name: "A"),
                Vtag("vt-1", "eq-1", name: "B")),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
    }

    // ----- surgical-only / rename-only / driver-only ⇒ AttributeOnly -----

    [Fact]
    public void Surgical_eligible_changed_tag_only_is_AttributeOnly()
    {
        // Only Writable flips ⇒ surgical-eligible ⇒ AttributeOnly (no adds, no removes).
        var plan = Plan(changedTags: new[]
        {
            new AddressSpacePlan.EquipmentTagDelta(
                Tag("tag-1", "eq-1", writable: false),
                Tag("tag-1", "eq-1", writable: true)),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
    }

    [Fact]
    public void Node_irrelevant_changed_vtag_only_is_AttributeOnly()
    {
        // Only Expression differs ⇒ node-irrelevant ⇒ AttributeOnly.
        var plan = Plan(changedVtags: new[]
        {
            new AddressSpacePlan.EquipmentVirtualTagDelta(
                Vtag("vt-1", "eq-1", expression: "a"),
                Vtag("vt-1", "eq-1", expression: "a * 2")),
        });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
    }

    [Fact]
    public void Folder_renames_only_is_AttributeOnly()
    {
        var plan = Plan(renamedFolders: new[] { new AddressSpacePlan.FolderRename("area-1", "New Area") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
    }

    [Fact]
    public void Driver_only_deltas_are_AttributeOnly_node_inert()
    {
        // Driver adds/removes/changes never touch the address space — excluded from hasAdds/hasRemoves.
        var plan = Plan(
            addedDrivers: new[] { new DriverInstancePlan("d-1", "Modbus", "{}") },
            removedDrivers: new[] { new DriverInstancePlan("d-2", "Modbus", "{}") },
            changedDrivers: new[]
            {
                new AddressSpacePlan.DriverDelta(
                    new DriverInstancePlan("d-3", "Modbus", "{\"v\":1}"),
                    new DriverInstancePlan("d-3", "Modbus", "{\"v\":2}")),
            });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
    }

    // ----- mixed-with-surgical: adds still win over coincident surgical changes -----

    [Fact]
    public void Adds_with_coincident_surgical_change_is_PureAdd()
    {
        var plan = Plan(
            addedTags: new[] { Tag("tag-new", "eq-1") },
            changedTags: new[]
            {
                new AddressSpacePlan.EquipmentTagDelta(
                    Tag("tag-1", "eq-1", writable: false),
                    Tag("tag-1", "eq-1", writable: true)),
            });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    [Fact]
    public void Adds_with_coincident_non_surgical_change_is_Rebuild()
    {
        // Rule 2 (node-affecting change) is evaluated BEFORE the add/remove split, so a non-surgical
        // change forces Rebuild even alongside pure adds.
        var plan = Plan(
            addedTags: new[] { Tag("tag-new", "eq-1") },
            changedTags: new[]
            {
                new AddressSpacePlan.EquipmentTagDelta(
                    Tag("tag-1", "eq-1", fullName: "40001"),
                    Tag("tag-1", "eq-1", fullName: "40002")),
            });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
    }

    [Fact]
    public void Adds_with_coincident_folder_rename_is_PureAdd()
    {
        var plan = Plan(
            addedTags: new[] { Tag("tag-new", "eq-1") },
            renamedFolders: new[] { new AddressSpacePlan.FolderRename("area-1", "New Area") });
        AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
    }

    // ----- construction helpers -----

    private static EquipmentNode Eq(string id) => new(id, id, "line-1");

    private static ScriptedAlarmPlan Alarm(string id) => new(id, "eq-1", "scr-1", "msg");

    private static EquipmentTagPlan Tag(string id, string equipmentId, string fullName = "40001", bool writable = false) =>
        new(id, equipmentId, "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: fullName, Writable: writable, Alarm: null);

    private static EquipmentVirtualTagPlan Vtag(string id, string equipmentId, string name = "Efficiency", string expression = "ctx.GetTag(\"a\")") =>
        new(id, equipmentId, FolderPath: "", Name: name, DataType: "Float64", Expression: expression, DependencyRefs: new[] { "a" });

    private static AddressSpacePlan Plan(
        IReadOnlyList<EquipmentNode>? addedEquipment = null,
        IReadOnlyList<EquipmentNode>? removedEquipment = null,
        IReadOnlyList<AddressSpacePlan.EquipmentDelta>? changedEquipment = null,
        IReadOnlyList<DriverInstancePlan>? addedDrivers = null,
        IReadOnlyList<DriverInstancePlan>? removedDrivers = null,
        IReadOnlyList<AddressSpacePlan.DriverDelta>? changedDrivers = null,
        IReadOnlyList<ScriptedAlarmPlan>? addedAlarms = null,
        IReadOnlyList<ScriptedAlarmPlan>? removedAlarms = null,
        IReadOnlyList<AddressSpacePlan.AlarmDelta>? changedAlarms = null,
        IReadOnlyList<EquipmentTagPlan>? addedTags = null,
        IReadOnlyList<EquipmentTagPlan>? removedTags = null,
        IReadOnlyList<AddressSpacePlan.EquipmentTagDelta>? changedTags = null,
        IReadOnlyList<EquipmentVirtualTagPlan>? addedVtags = null,
        IReadOnlyList<EquipmentVirtualTagPlan>? removedVtags = null,
        IReadOnlyList<AddressSpacePlan.EquipmentVirtualTagDelta>? changedVtags = null,
        IReadOnlyList<AddressSpacePlan.FolderRename>? renamedFolders = null) =>
        new(
            addedEquipment ?? Array.Empty<EquipmentNode>(),
            removedEquipment ?? Array.Empty<EquipmentNode>(),
            changedEquipment ?? Array.Empty<AddressSpacePlan.EquipmentDelta>(),
            addedDrivers ?? Array.Empty<DriverInstancePlan>(),
            removedDrivers ?? Array.Empty<DriverInstancePlan>(),
            changedDrivers ?? Array.Empty<AddressSpacePlan.DriverDelta>(),
            addedAlarms ?? Array.Empty<ScriptedAlarmPlan>(),
            removedAlarms ?? Array.Empty<ScriptedAlarmPlan>(),
            changedAlarms ?? Array.Empty<AddressSpacePlan.AlarmDelta>())
        {
            AddedEquipmentTags = addedTags ?? Array.Empty<EquipmentTagPlan>(),
            RemovedEquipmentTags = removedTags ?? Array.Empty<EquipmentTagPlan>(),
            ChangedEquipmentTags = changedTags ?? Array.Empty<AddressSpacePlan.EquipmentTagDelta>(),
            AddedEquipmentVirtualTags = addedVtags ?? Array.Empty<EquipmentVirtualTagPlan>(),
            RemovedEquipmentVirtualTags = removedVtags ?? Array.Empty<EquipmentVirtualTagPlan>(),
            ChangedEquipmentVirtualTags = changedVtags ?? Array.Empty<AddressSpacePlan.EquipmentVirtualTagDelta>(),
            RenamedFolders = renamedFolders ?? Array.Empty<AddressSpacePlan.FolderRename>(),
        };
}
