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

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

[Trait("Category", "Unit")]
public sealed class FocasReadWriteTests
{
    private static (FocasDriver drv, FakeFocasClientFactory factory) NewDriver(params FocasTagDefinition[] tags)
    {
        var factory = new FakeFocasClientFactory();
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = FocasRawTags.Entries(tags),
            Probe = new FocasProbeOptions { Enabled = false },
        }, "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(FocasStatusMapper.BadNodeIdUnknown);
    }

    /// <summary>Verifies that a successful PMC read returns a Good status value.</summary>
    [Fact]
    public async Task Successful_PMC_read_returns_Good_value()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)5 } };

        var snapshots = await drv.ReadAsync(["Run"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
        snapshots.Single().Value.ShouldBe((sbyte)5);
    }

    /// <summary>Verifies that parameter reads route through the FocasAddress Parameter kind.</summary>
    [Fact]
    public async Task Parameter_read_routes_through_FocasAddress_Parameter_kind()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("Accel", "focas://10.0.0.5:8193", "PARAM:1820", FocasDataType.Int32));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { Values = { ["PARAM:1820"] = 1500 } };

        var snapshots = await drv.ReadAsync(["Accel"], CancellationToken.None);
        snapshots.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
        snapshots.Single().Value.ShouldBe(1500);
    }

    /// <summary>Verifies that macro reads route through the FocasAddress Macro kind.</summary>
    [Fact]
    public async Task Macro_read_routes_through_FocasAddress_Macro_kind()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("CustomVar", "focas://10.0.0.5:8193", "MACRO:500", FocasDataType.Float64));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { Values = { ["MACRO:500"] = 3.14159 } };

        var snapshots = await drv.ReadAsync(["CustomVar"], CancellationToken.None);
        snapshots.Single().Value.ShouldBe(3.14159);
    }

    /// <summary>Verifies that repeated reads reuse the connection.</summary>
    [Fact]
    public async Task Repeat_read_reuses_connection()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)1 } };

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

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

    /// <summary>Verifies that FOCAS error statuses map correctly via the status mapper.</summary>
    [Fact]
    public async Task FOCAS_error_status_maps_via_status_mapper()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("Ghost", "focas://10.0.0.5:8193", "R999", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () =>
        {
            var c = new FakeFocasClient();
            c.ReadStatuses["R999"] = FocasStatusMapper.BadNodeIdUnknown;
            return c;
        };

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

    /// <summary>Verifies that a read exception surfaces BadCommunicationError.</summary>
    [Fact]
    public async Task Read_exception_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { ThrowOnRead = true };

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

    /// <summary>Verifies that a connection failure disposes the client and surfaces BadCommunicationError.</summary>
    [Fact]
    public async Task Connect_failure_disposes_client_and_surfaces_BadCommunicationError()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { ThrowOnConnect = true };

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

    /// <summary>Verifies that batched reads preserve order across different address areas.</summary>
    [Fact]
    public async Task Batched_reads_preserve_order_across_areas()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte),
            new FocasTagDefinition("B", "focas://10.0.0.5:8193", "PARAM:1820", FocasDataType.Int32),
            new FocasTagDefinition("C", "focas://10.0.0.5:8193", "MACRO:500", FocasDataType.Float64));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient
        {
            Values =
            {
                ["R100"] = (sbyte)5,
                ["PARAM:1820"] = 1500,
                ["MACRO:500"] = 2.718,
            },
        };

        var snapshots = await drv.ReadAsync(["A", "B", "C"], CancellationToken.None);
        snapshots[0].Value.ShouldBe((sbyte)5);
        snapshots[1].Value.ShouldBe(1500);
        snapshots[2].Value.ShouldBe(2.718);
    }

    // ---- Write ----

    /// <summary>Verifies that a non-writable tag write is rejected with BadNotWritable.</summary>
    [Fact]
    public async Task Non_writable_tag_rejected_with_BadNotWritable()
    {
        var (drv, _) = NewDriver(
            new FocasTagDefinition("RO", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte, Writable: false));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

    /// <summary>Verifies that a successful write logs the address, type, and value.</summary>
    [Fact]
    public async Task Successful_write_logs_address_type_value()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("Speed", "focas://10.0.0.5:8193", "R100", FocasDataType.Int16));
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        results.Single().StatusCode.ShouldBe(FocasStatusMapper.Good);
        var write = factory.Clients[0].WriteLog.Single();
        write.addr.Canonical.ShouldBe("R100");
        write.type.ShouldBe(FocasDataType.Int16);
        write.value.ShouldBe((short)1800);
    }

    /// <summary>Verifies that write status codes map correctly via the FocasStatusMapper.</summary>
    [Fact]
    public async Task Write_status_code_maps_via_FocasStatusMapper()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("Protected", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () =>
        {
            var c = new FakeFocasClient();
            c.WriteStatuses["R100"] = FocasStatusMapper.BadNotWritable;
            return c;
        };

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

    /// <summary>Verifies that batched writes preserve order across different outcomes.</summary>
    [Fact]
    public async Task Batch_write_preserves_order_across_outcomes()
    {
        var factory = new FakeFocasClientFactory();
        var drv = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")],
            RawTags = FocasRawTags.Entries(
            [
                new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte),
                new FocasTagDefinition("B", "focas://10.0.0.5:8193", "R101", FocasDataType.Byte, Writable: false),
            ]),
            Probe = new FocasProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

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

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

    /// <summary>Verifies that cancellation signals are propagated.</summary>
    [Fact]
    public async Task Cancellation_propagates()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        using var cts = new CancellationTokenSource();
        cts.Cancel();
        factory.Customise = () => new FakeFocasClient
        {
            ThrowOnRead = true,
            Exception = new OperationCanceledException(cts.Token),
        };

        // A CANCELLATION of the caller's token must propagate (abort the read). This is distinct
        // from a per-call timeout — an OCE raised while the caller's token is still live is swallowed
        // to a per-tag BadCommunicationError (see Swallows_a_spurious_read_OCE_when_caller_not_cancelled).
        await Should.ThrowAsync<OperationCanceledException>(
            () => drv.ReadAsync(["X"], cts.Token));
    }

    /// <summary>
    ///     An OperationCanceledException from the wire read while the CALLER'S token is NOT cancelled
    ///     (e.g. a per-call timeout firing) must be turned into a per-tag BadCommunicationError, not
    ///     propagated — otherwise one stalled tag would abort the whole poll batch.
    /// </summary>
    [Fact]
    public async Task Swallows_a_spurious_read_OCE_when_caller_not_cancelled()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient
        {
            ThrowOnRead = true,
            Exception = new OperationCanceledException(),
        };

        var snap = (await drv.ReadAsync(["X"], CancellationToken.None)).Single();
        snap.StatusCode.ShouldBe(FocasStatusMapper.BadCommunicationError);
    }

    /// <summary>Verifies that ShutdownAsync disposes the client.</summary>
    [Fact]
    public async Task ShutdownAsync_disposes_client()
    {
        var (drv, factory) = NewDriver(
            new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte));
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)1 } };

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

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