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

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

/// <summary>
///     Phase 4c #137 array support for the AbLegacy (PCCC/DF1) driver. A PCCC data file
///     (e.g. <c>N7</c>) is inherently an array of up to 256 words; an equipment tag carrying an
///     <c>arrayLength</c> reads <c>count</c> consecutive file elements from the base address
///     (<c>N7:0</c> reading <c>count</c> words) via libplctag's native element-count and surfaces
///     them as a typed CLR array. Proven here against <see cref="FakeAbLegacyTag"/>; there is no
///     live AbLegacy fixture on this machine.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyArrayTests
{
    private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags)
    {
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries(tags),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "ablegacy-array", factory);
        return (drv, factory);
    }

    // ---- Read: typed CLR arrays ----

    /// <summary>An N-file (16-bit integer) array tag reads a <c>short[]</c>.</summary>
    [Fact]
    public async Task Read_N_file_array_returns_short_array()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 100, 101, 102, 103, 104 } };

        var snapshots = await drv.ReadAsync(["Levels"], CancellationToken.None);

        snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        var arr = snapshots.Single().Value.ShouldBeOfType<short[]>();
        arr.ShouldBe(new short[] { 100, 101, 102, 103, 104 });
    }

    /// <summary>An L-file (32-bit long) array tag reads an <c>int[]</c>.</summary>
    [Fact]
    public async Task Read_L_file_array_returns_int_array()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counts", "ab://10.0.0.5/1,0", "L9:0", AbLegacyDataType.Long, ArrayLength: 3));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new[] { 70000, 80000, 90000 } };

        var snapshots = await drv.ReadAsync(["Counts"], CancellationToken.None);

        var arr = snapshots.Single().Value.ShouldBeOfType<int[]>();
        arr.ShouldBe(new[] { 70000, 80000, 90000 });
    }

    /// <summary>An F-file (32-bit float) array tag reads a <c>float[]</c>.</summary>
    [Fact]
    public async Task Read_F_file_array_returns_float_array()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Temps", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float, ArrayLength: 4));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new[] { 1.5f, 2.5f, 3.5f, 4.5f } };

        var snapshots = await drv.ReadAsync(["Temps"], CancellationToken.None);

        var arr = snapshots.Single().Value.ShouldBeOfType<float[]>();
        arr.ShouldBe(new[] { 1.5f, 2.5f, 3.5f, 4.5f });
    }

    /// <summary>A B-file (bit) array tag reads a <c>bool[]</c>.</summary>
    [Fact]
    public async Task Read_B_file_array_returns_bool_array()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Flags", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit, ArrayLength: 6));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new[] { true, false, true, false, true, false } };

        var snapshots = await drv.ReadAsync(["Flags"], CancellationToken.None);

        var arr = snapshots.Single().Value.ShouldBeOfType<bool[]>();
        arr.ShouldBe(new[] { true, false, true, false, true, false });
    }

    /// <summary>The element count flows into the create params so libplctag reads N words.</summary>
    [Fact]
    public async Task Array_tag_threads_element_count_into_create_params()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 1, 2, 3, 4, 5 } };

        await drv.ReadAsync(["Levels"], CancellationToken.None);

        factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(5);
    }

    /// <summary>A read failure on an array tag surfaces the mapped Bad status, not a partial array.</summary>
    [Fact]
    public async Task Array_read_failure_surfaces_bad_status()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)libplctag.Status.ErrorTimeout };

        var snapshots = await drv.ReadAsync(["Levels"], CancellationToken.None);

        snapshots.Single().Value.ShouldBeNull();
        snapshots.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
    }

    // ---- Scalar regression ----

    /// <summary>A scalar tag (no ArrayLength) still reads a single value, not an array.</summary>
    [Fact]
    public async Task Scalar_tag_still_reads_single_value()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counter", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { Value = 42 };

        var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);

        snapshots.Single().Value.ShouldBe(42);
        snapshots.Single().Value.ShouldNotBeOfType<short[]>();
        factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
    }

    /// <summary>
    ///     A 1-element array (ArrayLength: 1) reads as a 1-element typed array, NOT a scalar.
    ///     The foundation materialises a <c>[1]</c> OPC UA array node for <c>isArray:true,
    ///     arrayLength:1</c>, so the driver read must return a single-element array to match
    ///     (review I-3). ElementCount stays 1.
    /// </summary>
    [Fact]
    public async Task ArrayLength_of_one_reads_a_one_element_array()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("Counter", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 1));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 7 } };

        var snapshots = await drv.ReadAsync(["Counter"], CancellationToken.None);

        var arr = snapshots.Single().Value.ShouldBeOfType<short[]>();
        arr.ShouldBe(new short[] { 7 });
        factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
    }

    // ---- Discovery ----

    /// <summary>Discovery flips IsArray and surfaces ArrayDim for an array file tag.</summary>
    [Fact]
    public async Task Discovery_surfaces_IsArray_and_ArrayDim_for_array_tag()
    {
        var captured = new List<DriverAttributeInfo>();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Vector", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 8)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "ablegacy-array", new FakeAbLegacyTagFactory());
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(new RecordingBuilder(captured), CancellationToken.None);

        captured.Count.ShouldBe(1);
        captured[0].IsArray.ShouldBeTrue();
        captured[0].ArrayDim.ShouldBe(8u);
    }

    /// <summary>
    ///     I-3: a 1-element array tag (ArrayLength: 1) discovers as IsArray=true / ArrayDim=1 —
    ///     a <c>[1]</c> OPC UA array node, not a scalar.
    /// </summary>
    [Fact]
    public async Task Discovery_surfaces_one_element_array_as_IsArray_with_dim_one()
    {
        var captured = new List<DriverAttributeInfo>();
        var (drv, _) = NewDriver(
            new AbLegacyTagDefinition("Single", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 1));
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(new RecordingBuilder(captured), CancellationToken.None);

        captured[0].IsArray.ShouldBeTrue();
        captured[0].ArrayDim.ShouldBe(1u);
    }

    /// <summary>Scalar tag discovery keeps IsArray false / ArrayDim null (regression guard).</summary>
    [Fact]
    public async Task Discovery_keeps_scalar_tag_non_array()
    {
        var captured = new List<DriverAttributeInfo>();
        var (drv, _) = NewDriver(
            new AbLegacyTagDefinition("Single", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(new RecordingBuilder(captured), CancellationToken.None);

        captured[0].IsArray.ShouldBeFalse();
        captured[0].ArrayDim.ShouldBeNull();
    }

    /// <summary>Discovery caps a 1024-element tag's ArrayDim at the PCCC 256-word file maximum.</summary>
    [Fact]
    public async Task Discovery_caps_array_dim_at_256()
    {
        var captured = new List<DriverAttributeInfo>();
        var (drv, _) = NewDriver(
            new AbLegacyTagDefinition("Big", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 1024));
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.DiscoverAsync(new RecordingBuilder(captured), CancellationToken.None);

        captured[0].IsArray.ShouldBeTrue();
        captured[0].ArrayDim.ShouldBe(256u);
    }

    // ---- Equipment-tag resolver threads arrayLength ----

    /// <summary>The tag-definition factory threads <c>arrayLength</c> into the definition.</summary>
    [Fact]
    public void TagDefinitionFactory_threads_arrayLength()
    {
        var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":10}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBe(10);
    }

    /// <summary>The factory caps <c>arrayLength</c> at the PCCC 256-word file maximum (isArray:true).</summary>
    [Fact]
    public void TagDefinitionFactory_caps_arrayLength_at_256()
    {
        var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":99999}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBe(256);
    }

    /// <summary>A scalar tag (no arrayLength) leaves ArrayLength null.</summary>
    [Fact]
    public void TagDefinitionFactory_scalar_leaves_arrayLength_null()
    {
        var json = """{"address":"N7:0","dataType":"Int"}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBeNull();
    }

    /// <summary>
    ///     C-2 regression: an <c>arrayLength</c> WITHOUT <c>isArray:true</c> is a SCALAR. The
    ///     canonical contract gates an array on <c>isArray:true</c>, so a stale length behind a
    ///     cleared / absent isArray must leave ArrayLength null and never produce an orphan array.
    /// </summary>
    [Fact]
    public void TagDefinitionFactory_arrayLength_without_isArray_is_scalar()
    {
        var json = """{"address":"N7:0","dataType":"Int","arrayLength":8}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBeNull();
    }

    /// <summary>C-2 regression: <c>isArray:false</c> with a positive arrayLength is a SCALAR.</summary>
    [Fact]
    public void TagDefinitionFactory_isArray_false_with_arrayLength_is_scalar()
    {
        var json = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBeNull();
    }

    /// <summary>
    ///     I-3: <c>isArray:true, arrayLength:1</c> is a valid 1-element array — ArrayLength is 1
    ///     (not null), so the foundation materialises a <c>[1]</c> node and the driver reads it
    ///     as a 1-element array.
    /// </summary>
    [Fact]
    public void TagDefinitionFactory_isArray_true_arrayLength_one_is_one_element_array()
    {
        var json = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":1}""";
        AbLegacyTagDefinitionFactory.FromTagConfig(json, "eq/a", out var def).ShouldBeTrue();
        def!.ArrayLength.ShouldBe(1);
    }

    /// <summary>
    ///     End-to-end: an AbLegacy driver reads a raw tag authored as a <see cref="RawTagEntry"/> whose
    ///     TagConfig carries <c>isArray:true</c> + <c>arrayLength</c> — the mapper threads the count and
    ///     the read surfaces a typed array, capping the libplctag element count at 256.
    /// </summary>
    [Fact]
    public async Task Driver_reads_array_rawtag_as_typed_array()
    {
        const string rawPath = "cell/ablegacy/dev1/Vec";
        var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":true,"arrayLength":3}""";
        var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { ArrayValue = new short[] { 11, 22, 33 } } };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")],
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "ablegacy-eq-array", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        var arr = r[0].Value.ShouldBeOfType<short[]>();
        arr.ShouldBe(new short[] { 11, 22, 33 });
        factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(3);
    }

    /// <summary>
    ///     C-2 end-to-end: a raw tag whose TagConfig carries <c>isArray:false, arrayLength:8</c> reads
    ///     a single SCALAR value (the mapper drops the orphan length), so the read never decodes a
    ///     phantom 8-element array against a scalar node. ElementCount stays 1.
    /// </summary>
    [Fact]
    public async Task Driver_reads_isArray_false_with_arrayLength_as_scalar()
    {
        const string rawPath = "cell/ablegacy/dev1/Scl";
        var tagConfig = """{"address":"N7:0","dataType":"Int","isArray":false,"arrayLength":8}""";
        var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = 99 } };
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = [new RawTagEntry(rawPath, tagConfig, WriteIdempotent: false, DeviceName: "ab://10.0.0.5/1,0")],
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "ablegacy-eq-scalar", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        r[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        r[0].Value.ShouldBe(99);
        r[0].Value.ShouldNotBeOfType<short[]>();
        factory.Tags["N7:0"].CreationParams.ElementCount.ShouldBe(1);
    }

    private sealed class RecordingBuilder(List<DriverAttributeInfo> captured) : IAddressSpaceBuilder
    {
        public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;

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

        public void AddProperty(string name, DriverDataType dataType, object? value) { }

        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) { }
        }
    }
}
