using libplctag;
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;

[Trait("Category", "Unit")]
public sealed class AbLegacyReadWriteTests
{
    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),
        }, "drv-1", factory);
        return (drv, factory);
    }

    // ---- Read ----

    /// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown.</summary>
    [Fact]
    public async Task Unknown_reference_maps_to_BadNodeIdUnknown()
    {
        var (drv, _) = NewDriver();
        await drv.InitializeAsync("{}", CancellationToken.None);

        var snapshots = await drv.ReadAsync(["missing"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that a successful N-file read returns a Good status.</summary>
    [Fact]
    public async Task Successful_N_file_read_returns_Good_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().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        snapshots.Single().Value.ShouldBe(42);
        factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
        factory.Tags["N7:0"].ReadCount.ShouldBe(1);
    }

    /// <summary>Verifies that repeated reads reuse the runtime.</summary>
    [Fact]
    public async Task Repeat_read_reuses_runtime()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 };

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

        factory.Tags["N7:0"].InitializeCount.ShouldBe(1);
        factory.Tags["N7:0"].ReadCount.ShouldBe(2);
    }

    /// <summary>Verifies that non-zero libplctag status values map via AbLegacyStatusMapper.</summary>
    [Fact]
    public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
    {
        // Use the real libplctag.Status enum value rather than a raw integer so the test
        // stays correct if the wrapper renumbers its ordinals (Driver.AbLegacy-010).
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)Status.ErrorNotFound };

        var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that read exceptions surface as BadCommunicationError.</summary>
    [Fact]
    public async Task Read_exception_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnRead = true };

        var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
        drv.GetHealth().State.ShouldBe(DriverState.Degraded);
    }

    /// <summary>Verifies that batched reads preserve order.</summary>
    [Fact]
    public async Task Batched_reads_preserve_order()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
            new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "F8:0", AbLegacyDataType.Float),
            new AbLegacyTagDefinition("C", "ab://10.0.0.5/1,0", "ST9:0", AbLegacyDataType.String));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => p.TagName switch
        {
            "N7:0" => new FakeAbLegacyTag(p) { Value = 1 },
            "F8:0" => new FakeAbLegacyTag(p) { Value = 3.14f },
            _ => new FakeAbLegacyTag(p) { Value = "hello" },
        };

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

        snapshots.Count.ShouldBe(3);
        snapshots[0].Value.ShouldBe(1);
        snapshots[1].Value.ShouldBe(3.14f);
        snapshots[2].Value.ShouldBe("hello");
    }

    /// <summary>Verifies that read tag creation parameters are composed from device and profile.</summary>
    [Fact]
    public async Task Read_TagCreateParams_composed_from_device_and_profile()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:5", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        var p = factory.Tags["N7:5"].CreationParams;
        p.Gateway.ShouldBe("10.0.0.5");
        p.Port.ShouldBe(44818);
        p.CipPath.ShouldBe("1,0");
        p.LibplctagPlcAttribute.ShouldBe("slc500");
        p.TagName.ShouldBe("N7:5");
    }

    // ---- Write ----

    /// <summary>Verifies that a non-writable tag rejects with BadNotWritable.</summary>
    [Fact]
    public async Task Non_writable_tag_rejects_with_BadNotWritable()
    {
        var (drv, _) = NewDriver(
            new AbLegacyTagDefinition("RO", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, Writable: false));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("RO", 1)], CancellationToken.None);
        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
    }

    /// <summary>Verifies that a successful N-file write encodes and flushes the data.</summary>
    [Fact]
    public async Task Successful_N_file_write_encodes_and_flushes()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("X", 123)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        factory.Tags["N7:0"].Value.ShouldBe(123);
        factory.Tags["N7:0"].WriteCount.ShouldBe(1);
    }

    /// <summary>Verifies that bit-within-word write now succeeds via RMW.</summary>
    [Fact]
    public async Task Bit_within_word_write_now_succeeds_via_RMW()
    {
        // Task #181 pass 2 lifted this gap — N-file bit writes now go through
        // WriteBitInWordAsync + a parallel parent-word runtime, so the status is Good rather
        // than BadNotSupported. Full RMW semantics covered by AbLegacyBitRmwTests.
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Bit3", "ab://10.0.0.5/1,0", "N7:0/3", AbLegacyDataType.Bit)]),
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("Bit3", true)], CancellationToken.None);

        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
    }

    /// <summary>
    ///     Driver.AbLegacy-014 — a Bit-typed tag with no bit suffix (e.g. B3:0, DataType=Bit)
    ///     takes the EncodeValue(Bit, bitIndex:null, …) path (not RMW). The encode must be
    ///     symmetric with the DecodeValue path, which reads the full 16-bit word via GetInt16.
    ///     Through the fake factory this verifies the driver dispatches through EncodeValue with
    ///     the right arguments; the SetInt16/SetInt8 delta is exercised against a live PLC.
    /// </summary>
    [Fact]
    public async Task Bit_tag_without_suffix_writes_via_EncodeValue_not_RMW()
    {
        // A Bit-typed tag with NO /N bit suffix — bitIndex is null, so write must NOT
        // route through WriteBitInWordAsync (which requires bitIndex). Instead it goes
        // through EncodeValue(Bit, null, value) on the tag's own runtime, not a parent runtime.
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Flag", "ab://10.0.0.5/1,0", "B3:0", AbLegacyDataType.Bit)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
            [new WriteRequest("Flag", true)], CancellationToken.None);

        // Must succeed (Good) and route through the tag's own runtime, NOT a parent runtime.
        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        factory.Tags.ShouldContainKey("B3:0");
        factory.Tags.ShouldNotContainKey("B3");  // no parent-word runtime created
        factory.Tags["B3:0"].WriteCount.ShouldBe(1);
        // FakeAbLegacyTag.EncodeValue stores the raw value; verify it received true.
        factory.Tags["B3:0"].Value.ShouldBe(true);
    }

    /// <summary>Verifies that write exceptions surface as BadCommunicationError.</summary>
    [Fact]
    public async Task Write_exception_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { ThrowOnWrite = true };

        var results = await drv.WriteAsync(
            [new WriteRequest("X", 1)], CancellationToken.None);
        results.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
    }

    /// <summary>Verifies that batch write preserves order across different outcomes.</summary>
    [Fact]
    public async Task Batch_write_preserves_order_across_outcomes()
    {
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries(
            [
                new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int),
                new AbLegacyTagDefinition("B", "ab://10.0.0.5/1,0", "N7:1", AbLegacyDataType.Int, Writable: false),
            ]),
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var results = await drv.WriteAsync(
        [
            new WriteRequest("A", 1),
            new WriteRequest("B", 2),
            new WriteRequest("Unknown", 3),
        ], CancellationToken.None);

        results.Count.ShouldBe(3);
        results[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
        results[1].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotWritable);
        results[2].StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that cancellation propagates through the driver.</summary>
    [Fact]
    public async Task Cancellation_propagates()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p)
        {
            ThrowOnRead = true,
            Exception = new OperationCanceledException(),
        };

        await Should.ThrowAsync<OperationCanceledException>(
            () => drv.ReadAsync(["X"], CancellationToken.None));
    }

    /// <summary>Verifies that ShutdownAsync disposes all runtimes.</summary>
    [Fact]
    public async Task ShutdownAsync_disposes_runtimes()
    {
        var (drv, factory) = NewDriver(
            new AbLegacyTagDefinition("A", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = p => new FakeAbLegacyTag(p) { Value = 1 };

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

        factory.Tags["N7:0"].Disposed.ShouldBeTrue();
    }

    private sealed class RmwThrowingFake(AbLegacyTagCreateParams p) : FakeAbLegacyTag(p)
    {
        /// <summary>Encodes a value for the tag, throwing for unsupported bit-within-word RMW operations.</summary>
        /// <param name="type">The data type of the tag.</param>
        /// <param name="bitIndex">Optional bit index within a word.</param>
        /// <param name="value">The value to encode.</param>
        /// <inheritdoc />
        public override void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value)
        {
            if (type == AbLegacyDataType.Bit && bitIndex is not null)
                throw new NotSupportedException("bit-within-word RMW deferred");
            Value = value;
        }
    }
}
