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")],
            Tags = tags,
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        return (drv, factory);
    }

    // ---- Read ----

    [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);
    }

    [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();
    }

    [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);
    }

    [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);
    }

    [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);
    }

    [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);
    }

    [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 ----

    [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);
    }

    [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);
    }

    [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);
    }

    [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);
    }

    [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")],
            Tags =
            [
                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);
    }

    [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));
    }

    [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);
    }
}
