using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;

namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests.Series;

/// <summary>
///     Dual-run companion to <see cref="FixedTreePopulatesTests"/> — exercises the same
///     fixed-tree scenarios through the pure-managed <see cref="WireFocasClient"/>
///     instead of the shim/P-Invoke path. Proves both backends observe identical
///     state against the same focas-mock instance.
/// </summary>
/// <remarks>
///     Scheduled for removal in Wire migration phase 3 (task #104) once the shim is
///     deleted — at that point only this class survives and becomes the canonical
///     fixed-tree integration test.
/// </remarks>
[Collection(FocasSimCollection.Name)]
public sealed class WireBackendTests
{
    private readonly FocasSimFixture _fx;

    /// <summary>
    /// Initializes a new instance of the <see cref="WireBackendTests"/> class.
    /// </summary>
    /// <param name="fx">The FOCAS simulator fixture.</param>
    public WireBackendTests(FocasSimFixture fx) => _fx = fx;

    // Derived from the fixture endpoint (OTOPCUA_FOCAS_SIM_ENDPOINT) so the driver dials the SAME
    // mock the fixture probed — a remote fixture on the Docker host, not a hardcoded 127.0.0.1.
    private string DeviceHost => _fx.DeviceUri;

    /// <summary>Verifies that identity axes and dynamic data populate via the wire backend.</summary>
    [Fact]
    public async Task Identity_axes_and_dynamic_populate_via_wire_backend()
    {
        if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
        var ct = TestContext.Current.CancellationToken;

        await _fx.LoadProfileAsync("FWLIB64", ct);
        await _fx.PatchStateAsync(new
        {
            sysinfo = new
            {
                addinfo = 0, max_axis = 8, cnc_type = "M ", mt_type = "M ",
                series = "30i ", version = "A1.0", axes = "3 ",
            },
            axis_names = new[] { "X", "Y", "Z" },
            rddynamic2 = new
            {
                axis = 1, alarm = 0, prgnum = 1, prgmnum = 1, seqnum = 42,
                actf = 1500, acts = 3200,
                pos = new { absolute = 123456, machine = 123450, relative = 6, distance = 0 },
            },
        }, ct);

        var driver = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions(DeviceHost)],
            RawTags = [],
            Probe = new FocasProbeOptions { Enabled = false },
            FixedTree = new FocasFixedTreeOptions
            {
                Enabled = true,
                PollInterval = TimeSpan.FromMilliseconds(100),
            },
        }, driverInstanceId: "focas-wire-identity", clientFactory: new WireFocasClientFactory());

        await using (driver)
        {
            await driver.InitializeAsync("{}", ct);

            await WaitFor(() =>
                driver.GetDeviceState(DeviceHost) is { FixedTreeCache: not null }, TimeSpan.FromSeconds(5));

            var state = driver.GetDeviceState(DeviceHost);
            state.ShouldNotBeNull();
            state.FixedTreeCache.ShouldNotBeNull();
            state.FixedTreeCache.SysInfo.Series.ShouldStartWith("30i");
            state.FixedTreeCache.Axes.Count.ShouldBe(3);
            state.FixedTreeCache.Axes[0].Display.ShouldBe("X");

            await WaitFor(() =>
                state.LastFixedSnapshots.ContainsKey($"{DeviceHost}/Axes/X/AbsolutePosition"),
                TimeSpan.FromSeconds(3));
            state.LastFixedSnapshots[$"{DeviceHost}/Axes/X/AbsolutePosition"].ShouldBe(123456);
        }
    }

    /// <summary>Verifies that program and operation mode data populate via the wire backend.</summary>
    [Fact]
    public async Task Program_and_operation_mode_populate_via_wire_backend()
    {
        if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
        var ct = TestContext.Current.CancellationToken;

        await _fx.LoadProfileAsync("FWLIB64", ct);
        await _fx.PatchStateAsync(new
        {
            sysinfo = new
            {
                addinfo = 0, max_axis = 8, cnc_type = "M ", mt_type = "M ",
                series = "30i ", version = "A1.0", axes = "1 ",
            },
            axis_names = new[] { "X" },
            rddynamic2 = new
            {
                axis = 1, alarm = 0, prgnum = 42, prgmnum = 42, seqnum = 100,
                actf = 0, acts = 0,
                pos = new { absolute = 0, machine = 0, relative = 0, distance = 0 },
            },
            program = new
            {
                current = 42, main = 42, sequence = 100, block_count = 17,
                executing_path = "O0042.NC",
            },
            operation_mode = new { mode = 3 },
        }, ct);

        var driver = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions(DeviceHost)],
            RawTags = [],
            Probe = new FocasProbeOptions { Enabled = false },
            FixedTree = new FocasFixedTreeOptions
            {
                Enabled = true,
                PollInterval = TimeSpan.FromMilliseconds(100),
                ProgramPollInterval = TimeSpan.FromMilliseconds(200),
            },
        }, driverInstanceId: "focas-wire-program", clientFactory: new WireFocasClientFactory());

        await using (driver)
        {
            await driver.InitializeAsync("{}", ct);

            await WaitFor(() =>
                driver.GetDeviceState(DeviceHost) is { LastProgramInfo: not null },
                TimeSpan.FromSeconds(5));

            var snapshots = await driver.ReadAsync(
                [$"{DeviceHost}/Program/Name",
                 $"{DeviceHost}/Program/ONumber",
                 $"{DeviceHost}/Program/BlockCount",
                 $"{DeviceHost}/OperationMode/Mode"], ct);

            snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
            snapshots[0].Value!.ToString().ShouldStartWith("O0042");
            Convert.ToInt32(snapshots[1].Value).ShouldBe(42);
            Convert.ToInt32(snapshots[2].Value).ShouldBe(17);
            Convert.ToInt32(snapshots[3].Value).ShouldBe(3);
        }
    }

    /// <summary>Verifies that timer data populates via the wire backend.</summary>
    [Fact]
    public async Task Timers_populate_via_wire_backend()
    {
        if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
        var ct = TestContext.Current.CancellationToken;

        await _fx.LoadProfileAsync("FWLIB64", ct);
        await _fx.PatchStateAsync(new
        {
            axis_names = new[] { "X" },
            timers = new
            {
                power_on  = 3600,
                operating = 7200,
                cutting   = 1800,
                cycle     = 120,
            },
        }, ct);

        var driver = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions(DeviceHost)],
            RawTags = [],
            Probe = new FocasProbeOptions { Enabled = false },
            FixedTree = new FocasFixedTreeOptions
            {
                Enabled = true,
                PollInterval = TimeSpan.FromMilliseconds(100),
                TimerPollInterval = TimeSpan.FromMilliseconds(200),
                ProgramPollInterval = TimeSpan.Zero,
            },
        }, driverInstanceId: "focas-wire-timers", clientFactory: new WireFocasClientFactory());

        await using (driver)
        {
            await driver.InitializeAsync("{}", ct);

            await WaitFor(() =>
            {
                var state = driver.GetDeviceState(DeviceHost);
                return state is not null && state.LastTimers.Count == 4;
            }, TimeSpan.FromSeconds(5));

            var snapshots = await driver.ReadAsync(
                [$"{DeviceHost}/Timers/PowerOnSeconds",
                 $"{DeviceHost}/Timers/OperatingSeconds",
                 $"{DeviceHost}/Timers/CuttingSeconds",
                 $"{DeviceHost}/Timers/CycleSeconds"], ct);

            snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
            Convert.ToDouble(snapshots[0].Value).ShouldBe(3600.0, tolerance: 1);
            Convert.ToDouble(snapshots[1].Value).ShouldBe(7200.0, tolerance: 1);
            Convert.ToDouble(snapshots[2].Value).ShouldBe(1800.0, tolerance: 1);
            Convert.ToDouble(snapshots[3].Value).ShouldBe(120.0, tolerance: 1);
        }
    }

    /// <summary>Verifies that spindle load and max RPM data populate via the wire backend.</summary>
    [Fact]
    public async Task Spindle_load_and_max_rpm_populate_via_wire_backend()
    {
        if (_fx.SkipReason is not null) Assert.Skip(_fx.SkipReason);
        var ct = TestContext.Current.CancellationToken;

        await _fx.LoadProfileAsync("FWLIB64", ct);
        await _fx.PatchStateAsync(new
        {
            axis_names = new[] { "X" },
            spindle_names = new[] { "S1", "S2" },
            spindle = new
            {
                load = new object[]
                {
                    new { name = "S1", load = 56, speed = 3200 },
                    new { name = "S2", load = 12, speed = 1800 },
                },
                max_rpm = new[] { 6000, 4500 },
            },
            rddynamic2 = new
            {
                axis = 1, alarm = 0, prgnum = 1, prgmnum = 1, seqnum = 1,
                actf = 0, acts = 0,
                pos = new { absolute = 0, machine = 0, relative = 0, distance = 0 },
            },
        }, ct);

        var driver = new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions(DeviceHost)],
            RawTags = [],
            Probe = new FocasProbeOptions { Enabled = false },
            FixedTree = new FocasFixedTreeOptions
            {
                Enabled = true,
                PollInterval = TimeSpan.FromMilliseconds(100),
                ProgramPollInterval = TimeSpan.Zero,
                TimerPollInterval = TimeSpan.Zero,
            },
        }, driverInstanceId: "focas-wire-spindle", clientFactory: new WireFocasClientFactory());

        await using (driver)
        {
            await driver.InitializeAsync("{}", ct);

            await WaitFor(() =>
            {
                var state = driver.GetDeviceState(DeviceHost);
                return state?.FixedTreeCache is { Capabilities.SpindleLoad: true, Capabilities.SpindleMaxRpm: true }
                    && state.LastSpindleLoads.Count >= 2;
            }, TimeSpan.FromSeconds(5));

            var snapshots = await driver.ReadAsync(
                [$"{DeviceHost}/Spindle/S1/Load",
                 $"{DeviceHost}/Spindle/S1/MaxRpm",
                 $"{DeviceHost}/Spindle/S2/Load",
                 $"{DeviceHost}/Spindle/S2/MaxRpm"], ct);

            snapshots.ShouldAllBe(s => s.StatusCode == FocasStatusMapper.Good);
            Convert.ToInt32(snapshots[0].Value).ShouldBe(56);
            Convert.ToInt32(snapshots[1].Value).ShouldBe(6000);
            Convert.ToInt32(snapshots[2].Value).ShouldBe(12);
            Convert.ToInt32(snapshots[3].Value).ShouldBe(4500);
        }
    }

    private static async Task WaitFor(Func<bool> pred, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (pred()) return;
            await Task.Delay(50);
        }
    }
}
