using System.Text.Json;
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;

/// <summary>
///     Regression coverage for the five High code-review findings closed 2026-05-22:
///     Driver.TwinCAT-001 (driverConfigJson ignored), -002 (LInt/ULInt narrowed to Int32),
///     -007 (EnsureConnectedAsync not thread-safe), -008 (ADS callbacks on the router thread),
///     -013 (no IRediscoverable / symbol-version-changed handling).
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATHighFindingsRegressionTests
{
    private const string DeviceA = "ads://5.23.91.23.1.1:851";

    // ---- Driver.TwinCAT-001 — Reinitialize applies the new config generation ----

    /// <summary>Verifies ReinitializeAsync applies new device configuration at runtime.</summary>
    [Fact]
    public async Task ReinitializeAsync_applies_changed_device_config()
    {
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(DeviceA)],
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);
        drv.DeviceCount.ShouldBe(1);

        // A config generation that adds a second device must be picked up at runtime.
        var newConfig = JsonSerializer.Serialize(new
        {
            probe = new { enabled = false },
            devices = new[]
            {
                new { hostAddress = DeviceA, deviceName = "Machine1" },
                new { hostAddress = "ads://10.0.0.1.1.1:852", deviceName = "Machine2" },
            },
        });

        await drv.ReinitializeAsync(newConfig, CancellationToken.None);

        drv.DeviceCount.ShouldBe(2);
        drv.GetDeviceState("ads://10.0.0.1.1.1:852")!.Options.DeviceName.ShouldBe("Machine2");
    }

    /// <summary>Verifies InitializeAsync applies supplied JSON config over constructor options.</summary>
    [Fact]
    public async Task InitializeAsync_applies_supplied_config_over_constructor_options()
    {
        // Constructor seeds an empty option set; the JSON document is the authoritative config.
        var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
        var config = JsonSerializer.Serialize(new
        {
            probe = new { enabled = false },
            devices = new[] { new { hostAddress = DeviceA } },
        });

        await drv.InitializeAsync(config, CancellationToken.None);

        drv.DeviceCount.ShouldBe(1);
        drv.GetDeviceState(DeviceA).ShouldNotBeNull();
    }

    // ---- Driver.TwinCAT-002 — 64-bit + unsigned types map without truncation ----

    /// <summary>Verifies TwinCAT data types map to driver types without truncation of width or signedness.</summary>
    [Fact]
    public void DataType_mapping_preserves_width_and_signedness()
    {
        TwinCATDataType.LInt.ToDriverDataType().ShouldBe(DriverDataType.Int64);
        TwinCATDataType.ULInt.ToDriverDataType().ShouldBe(DriverDataType.UInt64);
        TwinCATDataType.UDInt.ToDriverDataType().ShouldBe(DriverDataType.UInt32);
        TwinCATDataType.DInt.ToDriverDataType().ShouldBe(DriverDataType.Int32);
        TwinCATDataType.UInt.ToDriverDataType().ShouldBe(DriverDataType.UInt16);
        TwinCATDataType.Int.ToDriverDataType().ShouldBe(DriverDataType.Int16);
        TwinCATDataType.USInt.ToDriverDataType().ShouldBe(DriverDataType.UInt16);
        TwinCATDataType.SInt.ToDriverDataType().ShouldBe(DriverDataType.Int16);
    }

    /// <summary>Verifies 64-bit LInt reads preserve values larger than int.MaxValue.</summary>
    [Fact]
    public async Task LInt_read_round_trips_value_above_int_MaxValue()
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(DeviceA)],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("Big", DeviceA, "GVL.Big", TwinCATDataType.LInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        long big = (long)int.MaxValue + 1_000;
        factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.Big"] = big } };

        var snapshot = (await drv.ReadAsync(["Big"], CancellationToken.None)).Single();

        snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.Good);
        snapshot.Value.ShouldBe(big); // no truncation into a 32-bit node
    }

    // ---- Driver.TwinCAT-007 — concurrent EnsureConnectedAsync creates exactly one client ----

    /// <summary>Verifies concurrent reads on one device create and share exactly one client.</summary>
    [Fact]
    public async Task Concurrent_reads_on_one_device_create_a_single_client()
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(DeviceA)],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 7 } };

        // 32 readers race the lazy connect. Without the per-device gate this leaks clients.
        var tasks = Enumerable.Range(0, 32)
            .Select(_ => Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None)));
        await Task.WhenAll(tasks);

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

    /// <summary>Verifies concurrent reads and writes on one device share exactly one client.</summary>
    [Fact]
    public async Task Concurrent_reads_and_writes_share_one_client()
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(DeviceA)],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        factory.Customise = () => new FakeTwinCATClient { Values = { ["GVL.X"] = 1 } };

        var work = new List<Task>();
        for (var i = 0; i < 16; i++)
        {
            work.Add(Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None)));
            work.Add(Task.Run(() => drv.WriteAsync(
                [new WriteRequest("X", 5)], CancellationToken.None)));
        }
        await Task.WhenAll(work);

        factory.Clients.Count.ShouldBe(1);
    }

    // ---- Driver.TwinCAT-013 — symbol-version-changed routes to IRediscoverable ----

    /// <summary>Verifies TwinCATDriver implements the IRediscoverable interface.</summary>
    [Fact]
    public void TwinCATDriver_implements_IRediscoverable()
    {
        var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
        drv.ShouldBeAssignableTo<IRediscoverable>();
    }

    /// <summary>Verifies symbol version change events raise the OnRediscoveryNeeded event.</summary>
    [Fact]
    public async Task Symbol_version_changed_raises_OnRediscoveryNeeded()
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions(DeviceA)],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", DeviceA, "GVL.X", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        RediscoveryEventArgs? raised = null;
        drv.OnRediscoveryNeeded += (_, args) => raised = args;

        // Force a connect so the driver wires its handler onto the client.
        await drv.ReadAsync(["X"], CancellationToken.None);
        factory.Clients.ShouldHaveSingleItem();

        // The client observed ADS 0x0702 on the wire and signalled it.
        factory.Clients[0].FireSymbolVersionChanged();

        raised.ShouldNotBeNull();
        raised!.Reason.ShouldContain("0x0711");
    }

    /// <summary>Verifies status mapper recognizes the symbol version changed error code.</summary>
    [Fact]
    public void StatusMapper_recognises_symbol_version_changed_code()
    {
        // DeviceSymbolVersionInvalid is 1809 (0x0711) per Beckhoff.TwinCAT.Ads 7.0.172.
        // Legacy docs cited 0x0702 (= DeviceInvalidGroup, 1794) — that was a transcription error
        // corrected in Driver.TwinCAT-011.
        TwinCATStatusMapper.AdsSymbolVersionChanged.ShouldBe(0x0711u); // = 1809u
        TwinCATStatusMapper.IsSymbolVersionChanged(0x0711u).ShouldBeTrue();
        TwinCATStatusMapper.IsSymbolVersionChanged(0x0702u).ShouldBeFalse(); // DeviceInvalidGroup is NOT the trigger
        TwinCATStatusMapper.IsSymbolVersionChanged(0u).ShouldBeFalse();
    }
}
