using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

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

public sealed class Phase7ComposerPurityTests
{
    /// <summary>Verifies empty inputs produce empty result.</summary>
    [Fact]
    public void Empty_inputs_produce_empty_result()
    {
        var result = Phase7Composer.Compose(
            equipment: Array.Empty<Equipment>(),
            driverInstances: Array.Empty<DriverInstance>(),
            scriptedAlarms: Array.Empty<ScriptedAlarm>());

        result.EquipmentNodes.ShouldBeEmpty();
        result.DriverInstancePlans.ShouldBeEmpty();
        result.ScriptedAlarmPlans.ShouldBeEmpty();
    }

    /// <summary>Verifies same inputs in different order produce structurally equal results.</summary>
    [Fact]
    public void Same_inputs_in_different_order_produce_structurally_equal_results()
    {
        var e1 = NewEquipment("eq-1");
        var e2 = NewEquipment("eq-2");
        var d1 = NewDriver("drv-1");
        var d2 = NewDriver("drv-2");
        var a1 = NewAlarm("a-1", "eq-1");
        var a2 = NewAlarm("a-2", "eq-2");

        var r1 = Phase7Composer.Compose(
            equipment: new[] { e1, e2 },
            driverInstances: new[] { d1, d2 },
            scriptedAlarms: new[] { a1, a2 });

        var r2 = Phase7Composer.Compose(
            equipment: new[] { e2, e1 },
            driverInstances: new[] { d2, d1 },
            scriptedAlarms: new[] { a2, a1 });

        r1.EquipmentNodes.ShouldBe(r2.EquipmentNodes);
        r1.DriverInstancePlans.ShouldBe(r2.DriverInstancePlans);
        r1.ScriptedAlarmPlans.ShouldBe(r2.ScriptedAlarmPlans);
    }

    /// <summary>Verifies Compose is pure with identical repeated calls.</summary>
    [Fact]
    public void Compose_is_pure_repeated_call_returns_element_identical_output()
    {
        var equipment = new[] { NewEquipment("eq-a"), NewEquipment("eq-b") };
        var drivers = new[] { NewDriver("drv-x") };
        var alarms = new[] { NewAlarm("alarm-1", "eq-a") };

        var r1 = Phase7Composer.Compose(equipment, drivers, alarms);
        var r2 = Phase7Composer.Compose(equipment, drivers, alarms);

        // Record equality won't help here — IReadOnlyList<T> uses reference equality. Compare
        // element-wise to verify the pure-function contract.
        r1.EquipmentNodes.ShouldBe(r2.EquipmentNodes);
        r1.DriverInstancePlans.ShouldBe(r2.DriverInstancePlans);
        r1.ScriptedAlarmPlans.ShouldBe(r2.ScriptedAlarmPlans);
    }

    /// <summary>Verifies output is sorted by natural key.</summary>
    [Fact]
    public void Output_is_sorted_by_natural_key()
    {
        var equipment = new[] { NewEquipment("z"), NewEquipment("a"), NewEquipment("m") };
        var result = Phase7Composer.Compose(equipment, Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>());

        result.EquipmentNodes.Select(e => e.EquipmentId)
            .ShouldBe(new[] { "a", "m", "z" });
    }

    private static Equipment NewEquipment(string id) => new()
    {
        EquipmentId = id,
        DriverInstanceId = "drv-1",
        UnsLineId = "line-1",
        Name = id,
        MachineCode = id.ToUpperInvariant(),
    };

    private static DriverInstance NewDriver(string id) => new()
    {
        DriverInstanceId = id,
        ClusterId = "cluster-1",
        NamespaceId = "ns-1",
        Name = id,
        DriverType = "Stub",
        DriverConfig = "{\"k\":\"v\"}",
    };

    private static ScriptedAlarm NewAlarm(string id, string equipmentId) => new()
    {
        ScriptedAlarmId = id,
        EquipmentId = equipmentId,
        Name = id,
        AlarmType = "AlarmCondition",
        MessageTemplate = "{TagPath} alarm",
        PredicateScriptId = "script-1",
    };
}
