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

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

/// <summary>
///     Phase 4 data-type tier — axis-position scaling. <c>cnc_rddynamic2</c> returns
///     positions as scaled integers; the driver applies a <c>10^PositionDecimalPlaces</c>
///     divide at the <see cref="FocasDriver"/> publish seam so positions surface in
///     engineering units on the Float64 axis nodes. The managed wire backend fetches per-axis
///     figures live via <c>cnc_getfigure</c>; the config knob is the per-axis fallback used
///     when the CNC reports no figure for an axis. These tests cover the config-supplied path.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasPositionScalingTests
{
    private const string Host = "focas://10.0.0.5:8193";

    // A single-axis CNC (X only) so the snapshot we seed is unambiguous.
    private static FakeFocasClient SingleAxisClient(FocasDynamicSnapshot snap)
    {
        var c = new FakeFocasClient();
        c.AxisNames.Clear();
        c.AxisNames.Add(new("X", ""));
        c.DynamicByAxis[1] = snap; // FOCAS axis indexing is 1-based
        return c;
    }

    private static FocasDriver NewDriver(int positionDecimalPlaces, FakeFocasClientFactory factory)
    {
        return new FocasDriver(new FocasDriverOptions
        {
            Devices = [new FocasDeviceOptions(Host, PositionDecimalPlaces: positionDecimalPlaces)],
            Probe = new FocasProbeOptions { Enabled = false },
            FixedTree = new FocasFixedTreeOptions
            {
                Enabled = true,
                PollInterval = TimeSpan.FromMilliseconds(10),
            },
        }, "drv-1", factory);
    }

    // Drive the fixed-tree poll loop and wait (bounded) for the cached value to appear.
    private static async Task<object?> ReadWhenPublishedAsync(FocasDriver drv, string reference)
    {
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            var snaps = await drv.ReadAsync([reference], CancellationToken.None);
            var snap = snaps.Single();
            if (snap.StatusCode == FocasStatusMapper.Good && snap.Value is not null)
                return snap.Value;
            await Task.Delay(15);
        }
        return null;
    }

    /// <summary>
    ///     With <c>PositionDecimalPlaces: 3</c> a raw AbsolutePosition of 12345 scales to
    ///     12.345 on the Float64 node (engineering units).
    /// </summary>
    [Fact]
    public async Task PositionDecimalPlaces_3_scales_AbsolutePosition_by_thousand()
    {
        var snap = new FocasDynamicSnapshot(
            AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
            SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
            AbsolutePosition: 12345, MachinePosition: 67890,
            RelativePosition: 11111, DistanceToGo: 500);
        var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
        var drv = NewDriver(positionDecimalPlaces: 3, factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
        var mach = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/MachinePosition");
        var rel = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/RelativePosition");
        var dtg = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/DistanceToGo");

        abs.ShouldBe(12.345);
        mach.ShouldBe(67.890);
        rel.ShouldBe(11.111);
        dtg.ShouldBe(0.500);

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>
    ///     <c>PositionDecimalPlaces: 0</c> (the default) must be byte-identical to today's
    ///     behaviour: the integer is published unchanged, widened to double on the Float64
    ///     node (12345 → 12345.0).
    /// </summary>
    [Fact]
    public async Task PositionDecimalPlaces_0_leaves_position_unscaled()
    {
        var snap = new FocasDynamicSnapshot(
            AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
            SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
            AbsolutePosition: 12345, MachinePosition: 0,
            RelativePosition: 0, DistanceToGo: 0);
        var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
        var drv = NewDriver(positionDecimalPlaces: 0, factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
        abs.ShouldBe(12345.0);

        await drv.ShutdownAsync(CancellationToken.None);
    }

    /// <summary>
    ///     FeedRate (and other non-position fields) is NOT position-scaled — it surfaces
    ///     unchanged even when PositionDecimalPlaces is set.
    /// </summary>
    [Fact]
    public async Task FeedRate_is_not_position_scaled()
    {
        var snap = new FocasDynamicSnapshot(
            AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
            SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
            AbsolutePosition: 12345, MachinePosition: 0,
            RelativePosition: 0, DistanceToGo: 0);
        var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
        var drv = NewDriver(positionDecimalPlaces: 3, factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Sanity: the position IS scaled on this same device.
        (await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition")).ShouldBe(12.345);
        // FeedRate must remain the raw 250, not 0.250.
        var feed = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/FeedRate/Actual");
        feed.ShouldBe(250.0);

        await drv.ShutdownAsync(CancellationToken.None);
    }
}
