using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;

/// <summary>
///     Phase 4c — 1-D array support for the TwinCAT (ADS) driver. Covers the two discovery
///     hard-wire flips (pre-declared + discovered symbols now report IsArray/ArrayDim from the
///     ADS symbol's array dimension), the array READ path (the equipment tag's arrayLength drives
///     a native array read boxed into a typed CLR array), and the equipment-tag parser threading
///     <c>arrayLength</c> from the TagConfig JSON.
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATArraySupportTests
{
    private const string Host = "ads://5.23.91.23.1.1:851";

    // ---- (1) discovery: pre-declared array tag flips IsArray/ArrayDim ----

    /// <summary>A pre-declared tag carrying an ArrayLength surfaces as a 1-D array node.</summary>
    [Fact]
    public async Task Predeclared_array_tag_reports_IsArray_and_ArrayDim()
    {
        var builder = new RecordingBuilder();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Host)],
            RawTags = TwinCATRawTags.Entries(
            [
                new TwinCATTagDefinition("Speeds", Host, "MAIN.Speeds", TwinCATDataType.DInt,
                    Writable: true, WriteIdempotent: false, ArrayLength: 8),
            ]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = false,
        }, "drv-1", new FakeTwinCATClientFactory());
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        var v = builder.Variables.Single(x => x.BrowseName == "Speeds").Info;
        v.IsArray.ShouldBeTrue();
        v.ArrayDim.ShouldBe(8u);
    }

    /// <summary>A scalar pre-declared tag still reports IsArray=false / ArrayDim=null.</summary>
    [Fact]
    public async Task Predeclared_scalar_tag_stays_scalar()
    {
        var builder = new RecordingBuilder();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Host)],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", new FakeTwinCATClientFactory());
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        var v = builder.Variables.Single(x => x.BrowseName == "Speed").Info;
        v.IsArray.ShouldBeFalse();
        v.ArrayDim.ShouldBeNull();
    }

    // ---- (1) discovery: discovered (browsed) array symbol flips IsArray/ArrayDim ----

    /// <summary>A discovered (browsed) 1-D array symbol surfaces as a 1-D array node.</summary>
    [Fact]
    public async Task Discovered_array_symbol_reports_IsArray_and_ArrayDim()
    {
        var builder = new RecordingBuilder();
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () =>
            {
                var c = new FakeTwinCATClient();
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol(
                    "MAIN.Buffer", TwinCATDataType.Int, ReadOnly: false, ArrayLength: 16));
                c.BrowseResults.Add(new TwinCATDiscoveredSymbol(
                    "GVL.Scalar", TwinCATDataType.Real, ReadOnly: false));
                return c;
            },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Host)],
            Probe = new TwinCATProbeOptions { Enabled = false },
            EnableControllerBrowse = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        var arr = builder.Variables.Single(x => x.Info.FullName == "MAIN.Buffer").Info;
        arr.IsArray.ShouldBeTrue();
        arr.ArrayDim.ShouldBe(16u);

        var scalar = builder.Variables.Single(x => x.Info.FullName == "GVL.Scalar").Info;
        scalar.IsArray.ShouldBeFalse();
        scalar.ArrayDim.ShouldBeNull();
    }

    // ---- (2) read path: array read returns a typed CLR array ----

    private const string ArrayRawPath = "equip/Speeds";
    private const string ScalarRawPath = "equip/Speed";

    /// <summary>An array raw tag reads a typed CLR array (boxed as object) through the driver.</summary>
    [Fact]
    public async Task Driver_reads_an_array_raw_tag_as_typed_clr_array()
    {
        var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":4}""";
        var expected = new[] { 10, 20, 30, 40 };
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speeds"] = expected } },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Host)],
            RawTags = [new RawTagEntry(ArrayRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "twincat-arr", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var r = await drv.ReadAsync([ArrayRawPath], CancellationToken.None);

        r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        r[0].Value.ShouldBeOfType<int[]>().ShouldBe(expected);
        // The driver must request the array read with the authored length.
        factory.Clients[0].LastReadArrayCount.ShouldBe(4);
    }

    /// <summary>A scalar raw tag reads with a null array count (scalar path unchanged).</summary>
    [Fact]
    public async Task Driver_reads_a_scalar_raw_tag_with_null_array_count()
    {
        var blob = """{"symbolPath":"MAIN.Speed","dataType":"DInt"}""";
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 7 } },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(Host)],
            RawTags = [new RawTagEntry(ScalarRawPath, blob, WriteIdempotent: false, DeviceName: Host)],
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "twincat-scalar", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var r = await drv.ReadAsync([ScalarRawPath], CancellationToken.None);

        r[0].StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        r[0].Value.ShouldBe(7);
        factory.Clients[0].LastReadArrayCount.ShouldBeNull();
    }

    // ---- (3) mapper: FromTagConfig threads arrayLength ----

    /// <summary>The tag-definition factory threads <c>arrayLength</c> into the definition.</summary>
    [Fact]
    public void FromTagConfig_threads_arrayLength_into_the_definition()
    {
        var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":12}""";
        TwinCATTagDefinitionFactory.FromTagConfig(blob, ArrayRawPath, out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBe(12);
    }

    /// <summary>A blob without isArray (or with isArray=false) leaves ArrayLength null.</summary>
    [Fact]
    public void FromTagConfig_leaves_ArrayLength_null_for_a_scalar_blob()
    {
        TwinCATTagDefinitionFactory.FromTagConfig(
            """{"symbolPath":"MAIN.X","dataType":"DInt"}""", ScalarRawPath, out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBeNull();
    }

    /// <summary>arrayLength is ignored when isArray is absent/false (no orphan length).</summary>
    [Fact]
    public void FromTagConfig_ignores_arrayLength_when_isArray_is_false()
    {
        TwinCATTagDefinitionFactory.FromTagConfig(
            """{"symbolPath":"MAIN.X","dataType":"DInt","isArray":false,"arrayLength":9}""",
            ScalarRawPath, out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBeNull();
    }

    // ---- (4) Driver.TwinCAT-017 — array raw tag delivered via driver config JSON rawTags ----

    /// <summary>
    ///     An array raw tag in the driver config JSON <c>rawTags</c> array must bind + map to a
    ///     <see cref="TwinCATTagDefinition"/> with the correct <see cref="TwinCATTagDefinition.ArrayLength"/>,
    ///     which in turn drives <c>IsArray</c>/<c>ArrayDim</c> at discovery and a native array read
    ///     at runtime (Driver.TwinCAT-017).
    /// </summary>
    [Fact]
    public void Array_raw_tag_arrayLength_binds_from_driver_config_json()
    {
        var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
        var json = System.Text.Json.JsonSerializer.Serialize(new
        {
            devices = new[] { new { hostAddress = Host } },
            rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
        });
        var parsed = TwinCATDriverFactoryExtensions.ParseOptionsForTests(json, "drv-1");
        var entry = parsed.RawTags.Single();
        TwinCATTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBe(8);
    }

    /// <summary>
    ///     An array raw tag delivered via the driver config JSON must surface as <c>IsArray=true</c> /
    ///     <c>ArrayDim=8</c> in the discovery address space.
    /// </summary>
    [Fact]
    public async Task Array_raw_tag_from_json_reports_IsArray_in_discovery()
    {
        var blob = """{"symbolPath":"MAIN.Speeds","dataType":"DInt","isArray":true,"arrayLength":8}""";
        var configJson = System.Text.Json.JsonSerializer.Serialize(new
        {
            devices = new[] { new { hostAddress = Host } },
            rawTags = new[] { new { rawPath = "Speeds", tagConfig = blob, writeIdempotent = false, deviceName = Host } },
        });
        var builder = new RecordingBuilder();
        var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1", new FakeTwinCATClientFactory());
        await drv.InitializeAsync(configJson, CancellationToken.None);

        await drv.DiscoverAsync(builder, CancellationToken.None);

        var v = builder.Variables.Single(x => x.BrowseName == "Speeds").Info;
        v.IsArray.ShouldBeTrue();
        v.ArrayDim.ShouldBe(8u);
    }

    // ---- helpers ----

    private sealed class RecordingBuilder : IAddressSpaceBuilder
    {
        public List<(string BrowseName, string DisplayName)> Folders { get; } = new();
        public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new();

        public IAddressSpaceBuilder Folder(string browseName, string displayName)
        { Folders.Add((browseName, displayName)); return this; }

        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info)
        { Variables.Add((browseName, info)); return new Handle(info.FullName); }

        public void AddProperty(string _, DriverDataType __, object? ___) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            public string FullReference => fullRef;
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
        }

        private sealed class NullSink : IAlarmConditionSink
        {
            public void OnTransition(AlarmEventArgs args) { }
        }
    }
}
