using Shouldly;
using Xunit;

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

/// <summary>
///     Task #194 — ReadAsync integration tests for the whole-UDT grouping path. The fake
///     runtime records ReadCount + surfaces member values by byte offset so we can assert
///     both "one read per parent UDT" and "each member decoded at the correct offset."
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipDriverWholeUdtReadTests
{
    private const string Device = "ab://10.0.0.5/1,0";

    private static (AbCipDriver drv, FakeAbCipTagFactory factory) NewDriver(params AbCipTagDefinition[] tags)
    {
        var factory = new FakeAbCipTagFactory();
        var opts = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions(Device)],
            RawTags = AbCipRawTags.From(tags),
            // Whole-UDT grouping is opt-in (Driver.AbCip-003) — these tests exercise the
            // grouping fast path, so they switch it on explicitly.
            EnableDeclarationOnlyUdtGrouping = true,
        };
        return (new AbCipDriver(opts, "drv-1", factory), factory);
    }

    private static AbCipTagDefinition MotorUdt() => new(
        "Motor", Device, "Motor", AbCipDataType.Structure, Members:
        [
            new AbCipStructureMember("Speed", AbCipDataType.DInt),   // offset 0
            new AbCipStructureMember("Torque", AbCipDataType.Real),  // offset 4
        ]);

    /// <summary>Verifies that multiple members of the same UDT trigger only one parent read.</summary>
    [Fact]
    public async Task Two_members_of_same_udt_trigger_one_parent_read()
    {
        var (drv, factory) = NewDriver(MotorUdt());
        await drv.InitializeAsync("{}", CancellationToken.None);

        var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);

        snapshots.Count.ShouldBe(2);
        snapshots[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
        snapshots[1].StatusCode.ShouldBe(AbCipStatusMapper.Good);

        // Factory should have created ONE runtime (for the parent "Motor") + issued ONE read.
        // Without the optimization two runtimes (one per member) + two reads would appear.
        factory.Tags.Count.ShouldBe(1);
        factory.Tags.ShouldContainKey("Motor");
        factory.Tags["Motor"].ReadCount.ShouldBe(1);
    }

    /// <summary>Verifies that each UDT member is decoded at its correct offset.</summary>
    [Fact]
    public async Task Each_member_decodes_at_its_own_offset()
    {
        var (drv, factory) = NewDriver(MotorUdt());
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Arrange the offset-keyed values before the read fires — the planner places
        // Speed at offset 0 (DInt) and Torque at offset 4 (Real).
        // The fake records CreationParams so we fetch it up front by the parent name.
        var snapshotsTask = drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
        // The factory creates the runtime inside ReadAsync; we need to set the offset map
        // AFTER creation. Easier path: create the runtime on demand by reading once then
        // re-arming. Instead: seed via a pre-read by constructing the fake in the factory's
        // customise hook.
        var snapshots = await snapshotsTask;

        // First run establishes the runtime + gives the fake a chance to hold its reference.
        factory.Tags["Motor"].ValuesByOffset[0] = 1234; // Speed
        factory.Tags["Motor"].ValuesByOffset[4] = 9.5f; // Torque

        snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
        snapshots[0].Value.ShouldBe(1234);
        snapshots[1].Value.ShouldBe(9.5f);
    }

    /// <summary>Verifies that parent read failure marks all grouped members as Bad.</summary>
    [Fact]
    public async Task Parent_read_failure_stamps_every_grouped_member_Bad()
    {
        var (drv, factory) = NewDriver(MotorUdt());
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Prime runtime existence via a first (successful) read so we can flip it to error.
        await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
        factory.Tags["Motor"].Status = -3; // libplctag BadTimeout — mapped in AbCipStatusMapper

        var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);

        snapshots.Count.ShouldBe(2);
        snapshots[0].StatusCode.ShouldNotBe(AbCipStatusMapper.Good);
        snapshots[0].Value.ShouldBeNull();
        snapshots[1].StatusCode.ShouldNotBe(AbCipStatusMapper.Good);
        snapshots[1].Value.ShouldBeNull();
    }

    /// <summary>Verifies that mixed batches group UDT members and fall back to atomic reads.</summary>
    [Fact]
    public async Task Mixed_batch_groups_udt_and_falls_back_atomics()
    {
        var plain = new AbCipTagDefinition("PlainDint", Device, "PlainDint", AbCipDataType.DInt);
        var (drv, factory) = NewDriver(MotorUdt(), plain);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var snapshots = await drv.ReadAsync(
            ["Motor.Speed", "PlainDint", "Motor.Torque"], CancellationToken.None);

        snapshots.Count.ShouldBe(3);
        // Motor parent ran one read, PlainDint ran its own read = 2 runtimes, 2 reads total.
        factory.Tags.Count.ShouldBe(2);
        factory.Tags.ShouldContainKey("Motor");
        factory.Tags.ShouldContainKey("PlainDint");
        factory.Tags["Motor"].ReadCount.ShouldBe(1);
        factory.Tags["PlainDint"].ReadCount.ShouldBe(1);
    }

    /// <summary>Verifies that a single UDT member uses the per-tag read path rather than grouping.</summary>
    [Fact]
    public async Task Single_member_of_Udt_uses_per_tag_read_path()
    {
        // One member of a UDT doesn't benefit from grouping — the planner demotes to
        // fallback so the member-level runtime (distinct from the parent runtime) is used,
        // matching pre-#194 behavior.
        var (drv, factory) = NewDriver(MotorUdt());
        await drv.InitializeAsync("{}", CancellationToken.None);

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

        factory.Tags.ShouldContainKey("Motor.Speed");
        factory.Tags.ShouldNotContainKey("Motor");
    }
}
