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

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

/// <summary>
///     WP7 Calculation deploy gates in <see cref="DraftValidator"/>: <c>scriptId</c> existence and the
///     calc→calc dependency-cycle gate (self-cycle, 2-cycle, and the cross-driver-terminal negative case).
/// </summary>
[Trait("Category", "Unit")]
public sealed class DraftValidatorCalculationTests
{
    // A calc DriverInstance "calc" + one Device "engine" → calc tags get RawPath "calc/engine/<name>".
    private static DriverInstance CalcDriver() => new()
    {
        DriverInstanceId = "di-calc", ClusterId = "c", Name = "calc",
        DriverType = "Calculation", DriverConfig = "{}",
    };

    private static DriverInstance ModbusDriver() => new()
    {
        DriverInstanceId = "di-mb", ClusterId = "c", Name = "mb",
        DriverType = "Modbus", DriverConfig = "{}",
    };

    private static Device Device(string id, string driverInstanceId, string name) => new()
    {
        DeviceId = id, DriverInstanceId = driverInstanceId, Name = name, DeviceConfig = "{}",
    };

    private static Tag CalcTagRow(string tagId, string deviceId, string name, string tagConfig) => new()
    {
        TagId = tagId, DeviceId = deviceId, Name = name, DataType = "Double",
        AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig,
    };

    private static Script ScriptRow(string id, string source) => new()
    {
        ScriptId = id, Name = id, SourceCode = source, SourceHash = "h-" + id,
    };

    private static string Cfg(string scriptId) => $"{{\"scriptId\":\"{scriptId}\",\"changeTriggered\":true}}";

    [Fact]
    public void Missing_scriptId_is_a_deploy_error()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver()],
            Devices = [Device("dev-1", "di-calc", "engine")],
            Tags = [CalcTagRow("t-1", "dev-1", "a", "{\"changeTriggered\":true}")],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "CalculationScriptMissing" && e.Context == "t-1");
    }

    [Fact]
    public void Unknown_scriptId_is_a_deploy_error()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver()],
            Devices = [Device("dev-1", "di-calc", "engine")],
            Tags = [CalcTagRow("t-1", "dev-1", "a", Cfg("s-nope"))],
            Scripts = [],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "CalculationScriptNotFound" && e.Context == "t-1");
    }

    [Fact]
    public void Existing_scriptId_with_no_cycle_is_accepted()
    {
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver()],
            Devices = [Device("dev-1", "di-calc", "engine")],
            Tags = [CalcTagRow("t-1", "dev-1", "a", Cfg("s1"))],
            Scripts = [ScriptRow("s1", "return 42.0;")],
        };

        DraftValidator.Validate(draft).ShouldNotContain(e => e.Code.StartsWith("Calculation"));
    }

    [Fact]
    public void Self_cycle_is_rejected()
    {
        // calc/engine/a reads itself.
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver()],
            Devices = [Device("dev-1", "di-calc", "engine")],
            Tags = [CalcTagRow("t-a", "dev-1", "a", Cfg("s-a"))],
            Scripts = [ScriptRow("s-a", "return (double)ctx.GetTag(\"calc/engine/a\").Value;")],
        };

        DraftValidator.Validate(draft).ShouldContain(e =>
            e.Code == "CalculationDependencyCycle" && e.Message.Contains("calc/engine/a"));
    }

    [Fact]
    public void Two_node_cycle_names_both_members()
    {
        // a → b and b → a.
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver()],
            Devices = [Device("dev-1", "di-calc", "engine")],
            Tags =
            [
                CalcTagRow("t-a", "dev-1", "a", Cfg("s-a")),
                CalcTagRow("t-b", "dev-1", "b", Cfg("s-b")),
            ],
            Scripts =
            [
                ScriptRow("s-a", "return (double)ctx.GetTag(\"calc/engine/b\").Value;"),
                ScriptRow("s-b", "return (double)ctx.GetTag(\"calc/engine/a\").Value;"),
            ],
        };

        DraftValidator.Validate(draft).ShouldContain(e => e.Code == "CalculationDependencyCycle");
        var msg = DraftValidator.Validate(draft).First(e => e.Code == "CalculationDependencyCycle").Message;
        msg.ShouldContain("calc/engine/a");
        msg.ShouldContain("calc/engine/b");
    }

    [Fact]
    public void Cross_driver_ref_is_terminal_not_an_edge()
    {
        // A calc tag reads a Modbus tag's RawPath ("mb/plc/reg"); that is a terminal node, never an edge,
        // so it can never form a cycle — even if the Modbus tag existed.
        var draft = new DraftSnapshot
        {
            GenerationId = 1, ClusterId = "c",
            DriverInstances = [CalcDriver(), ModbusDriver()],
            Devices = [Device("dev-1", "di-calc", "engine"), Device("dev-2", "di-mb", "plc")],
            Tags =
            [
                CalcTagRow("t-a", "dev-1", "a", Cfg("s-a")),
                new Tag { TagId = "t-reg", DeviceId = "dev-2", Name = "reg", DataType = "Int32",
                          AccessLevel = TagAccessLevel.Read, TagConfig = "{}" },
            ],
            Scripts = [ScriptRow("s-a", "return (double)ctx.GetTag(\"mb/plc/reg\").Value;")],
        };

        DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "CalculationDependencyCycle");
    }
}
