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;

[Trait("Category", "Unit")]
public sealed class TwinCATReadWriteTests
{
    private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewDriver(params TwinCATTagDefinition[] tags)
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries(tags),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        return (drv, factory);
    }

    // ---- Read ----

    /// <summary>Verifies that an unknown reference maps to BadNodeIdUnknown status.</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(TwinCATStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that a successful DInt read returns Good status and the correct value.</summary>
    [Fact]
    public async Task Successful_DInt_read_returns_Good_value()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Speed"] = 4200 } };

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

        snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        snapshots.Single().Value.ShouldBe(4200);
        factory.Clients[0].ConnectCount.ShouldBe(1);
        factory.Clients[0].IsConnected.ShouldBeTrue();
    }

    /// <summary>Verifies that repeated read operations reuse the same connection.</summary>
    [Fact]
    public async Task Repeat_read_reuses_connection()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "GVL.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 1 } };

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

        // One client, one connect — subsequent calls reuse the connected client.
        factory.Clients.Count.ShouldBe(1);
        factory.Clients[0].ConnectCount.ShouldBe(1);
    }

    /// <summary>Verifies that ADS read errors are mapped via the status mapper.</summary>
    [Fact]
    public async Task Read_with_ADS_error_maps_via_status_mapper()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Ghost", "ads://5.23.91.23.1.1:851", "MAIN.Missing", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () =>
        {
            var c = new FakeTwinCATClient();
            c.ReadStatuses["MAIN.Missing"] = TwinCATStatusMapper.BadNodeIdUnknown;
            return c;
        };

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

    /// <summary>Verifies that read exceptions surface as BadCommunicationError status.</summary>
    [Fact]
    public async Task Read_exception_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { ThrowOnRead = true };

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

    /// <summary>Verifies that connect failures surface BadCommunicationError and dispose the client.</summary>
    [Fact]
    public async Task Connect_failure_surfaces_BadCommunicationError_and_disposes_client()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { ThrowOnConnect = true };

        var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
        factory.Clients[0].DisposeCount.ShouldBe(1);
    }

    /// <summary>Verifies that batched read operations preserve the order of results.</summary>
    [Fact]
    public async Task Batched_reads_preserve_order()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
            new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.Real),
            new TwinCATTagDefinition("C", "ads://5.23.91.23.1.1:851", "MAIN.C", TwinCATDataType.String));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient
        {
            Values =
            {
                ["MAIN.A"] = 1,
                ["MAIN.B"] = 3.14f,
                ["MAIN.C"] = "hello",
            },
        };

        var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);
        snapshots[0].Value.ShouldBe(1);
        snapshots[1].Value.ShouldBe(3.14f);
        snapshots[2].Value.ShouldBe("hello");
    }

    // ---- Write ----

    /// <summary>Verifies that non-writable tags are rejected with BadNotWritable status.</summary>
    [Fact]
    public async Task Non_writable_tag_rejected_with_BadNotWritable()
    {
        var (drv, _) = NewDriver(
            new TwinCATTagDefinition("RO", "ads://5.23.91.23.1.1:851", "MAIN.RO", TwinCATDataType.DInt, Writable: false));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

    /// <summary>Verifies that successful writes log the symbol, type, and value correctly.</summary>
    [Fact]
    public async Task Successful_write_logs_symbol_type_value()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        var write = factory.Clients[0].WriteLog.Single();
        write.symbol.ShouldBe("MAIN.Speed");
        write.type.ShouldBe(TwinCATDataType.DInt);
        write.value.ShouldBe(4200);
    }

    /// <summary>Verifies that ADS write errors are mapped and surfaced correctly.</summary>
    [Fact]
    public async Task Write_with_ADS_error_surfaces_mapped_status()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () =>
        {
            var c = new FakeTwinCATClient();
            c.WriteStatuses["MAIN.X"] = TwinCATStatusMapper.BadNotWritable;
            return c;
        };

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

    /// <summary>Verifies that write exceptions surface as BadCommunicationError status.</summary>
    [Fact]
    public async Task Write_exception_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { ThrowOnWrite = true };

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

    /// <summary>Verifies that batched write operations preserve order across mixed outcomes.</summary>
    [Fact]
    public async Task Batch_write_preserves_order_across_outcomes()
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries(
            [
                new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
                new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt, Writable: false),
            ]),
            Probe = new TwinCATProbeOptions { Enabled = 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(TwinCATStatusMapper.Good);
        results[1].StatusCode.ShouldBe(TwinCATStatusMapper.BadNotWritable);
        results[2].StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that cancellation tokens propagate correctly during read operations.</summary>
    [Fact]
    public async Task Cancellation_propagates()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient
        {
            ThrowOnRead = true,
            Exception = new OperationCanceledException(),
        };

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

    /// <summary>Verifies that shutdown disposes the client correctly.</summary>
    [Fact]
    public async Task ShutdownAsync_disposes_client()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.X"] = 1 } };

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

        factory.Clients[0].DisposeCount.ShouldBe(1);
    }

    // ---- BOOL-within-word RMW writes ----

    /// <summary>A BOOL-within-word set reads the parent word, ORs the bit, writes it back as UDInt.</summary>
    [Fact]
    public async Task Bit_set_RMWs_parent_word_as_UDInt()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Flag", "ads://5.23.91.23.1.1:851", "MAIN.Flags.3", TwinCATDataType.Bool));
        factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Flags"] = 0b0001u } };
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        factory.Clients[0].Values["MAIN.Flags"].ShouldBe(0b1001u);
        factory.Clients[0].WriteLog.ShouldContain(e =>
            e.symbol == "MAIN.Flags" && e.type == TwinCATDataType.UDInt && e.bit == null);
    }

    /// <summary>A BOOL-within-word clear preserves the other bits in the parent word.</summary>
    [Fact]
    public async Task Bit_clear_preserves_other_bits()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Flag", "ads://5.23.91.23.1.1:851", "MAIN.Flags.3", TwinCATDataType.Bool));
        factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Flags"] = 0xFFFFu } };
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.WriteAsync([new WriteRequest("Flag", false)], CancellationToken.None);

        factory.Clients[0].Values["MAIN.Flags"].ShouldBe(0xFFF7u);
    }

    /// <summary>RMW works on a DWORD parent (bit 20 set above the 16-bit boundary).</summary>
    [Fact]
    public async Task Bit_set_on_DWORD_parent_sets_high_bit()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Hi", "ads://5.23.91.23.1.1:851", "GVL.Status.20", TwinCATDataType.Bool));
        factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.Status"] = 0u } };
        await drv.InitializeAsync("{}", CancellationToken.None);

        await drv.WriteAsync([new WriteRequest("Hi", true)], CancellationToken.None);

        factory.Clients[0].Values["GVL.Status"].ShouldBe(1u << 20);
    }

    /// <summary>A failed parent read short-circuits the RMW and surfaces the read status.</summary>
    [Fact]
    public async Task Bit_write_surfaces_parent_read_failure()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Flag", "ads://5.23.91.23.1.1:851", "MAIN.Flags.3", TwinCATDataType.Bool));
        factory.Customise = () => new FakeTwinCATClient
        {
            ReadStatuses = { ["MAIN.Flags"] = TwinCATStatusMapper.BadNodeIdUnknown },
        };
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
        factory.Clients[0].WriteLog.ShouldBeEmpty();
    }

    /// <summary>A Good parent read that yields a null value must NOT zero the word — it surfaces
    ///     a Bad status and writes nothing (treating null as 0 would clear every set bit).</summary>
    [Fact]
    public async Task Bit_write_with_null_parent_value_does_not_zero_the_word()
    {
        var (drv, factory) = NewDriver(
            new TwinCATTagDefinition("Flag", "ads://5.23.91.23.1.1:851", "MAIN.Flags.3", TwinCATDataType.Bool));
        // Do NOT seed Values["MAIN.Flags"] — the fake returns (null, Good) for the parent read.
        factory.Customise = () => new FakeTwinCATClient();
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
        // No parent write attempted — the word is left untouched on the device.
        factory.Clients[0].WriteLog.ShouldBeEmpty();
    }

    /// <summary>Concurrent bit writes to the same word compose correctly (per-parent lock).</summary>
    [Fact]
    public async Task Concurrent_bit_writes_to_same_word_compose_correctly()
    {
        var tags = Enumerable.Range(0, 8)
            .Select(b => new TwinCATTagDefinition($"Bit{b}", "ads://5.23.91.23.1.1:851", $"MAIN.Flags.{b}", TwinCATDataType.Bool))
            .ToArray();
        var (drv, factory) = NewDriver(tags);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["MAIN.Flags"] = 0u } };
        await drv.InitializeAsync("{}", CancellationToken.None);

        await Task.WhenAll(Enumerable.Range(0, 8).Select(b =>
            drv.WriteAsync([new WriteRequest($"Bit{b}", true)], CancellationToken.None)));

        Convert.ToUInt32(factory.Clients[0].Values["MAIN.Flags"]).ShouldBe(0xFFu);
    }
}
