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

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;

/// <summary>
///     Live-gate smoke for backlog #6: controller-discovered nested-struct UDT expansion
///     via Template Object (CIP class 0x6C) against Rockwell Studio 5000 Logix Emulate.
///     Closes the gap left by the offline unit suite
///     (<c>AbCipDriverDiscoveryTests.Controller_discovered_UDT_nested_struct_expands_via_nested_template_id_fetch_no_seam</c>
///     and <c>CipTemplateObjectDecoderTests</c>) — those tests proved correctness with
///     golden-box byte buffers, but the Template Object service (class 0x6C) is NOT
///     implemented by the default <c>ab_server</c> Docker simulator. Emulate's firmware
///     speaks production CIP responses so the nested-template-id threading (commits
///     3d8ce4e8 / d203f31c) gets end-to-end wire-level coverage here.
/// </summary>
/// <remarks>
///     <para><b>Required Emulate project state</b>:</para>
///     <list type="bullet">
///       <item>UDT <c>StatusNested_UDT</c> with members <c>Code : DINT</c>,
///         <c>Running : BOOL</c>.</item>
///       <item>UDT <c>MotorNested_UDT</c> with members <c>Speed : DINT</c>,
///         <c>Status : StatusNested_UDT</c> — <c>Status</c> is the nested struct member
///         whose template instance id the driver must re-fetch via <c>@udt/{id}</c>.</item>
///       <item>Controller-scope tag <c>MotorNested1 : MotorNested_UDT</c>.</item>
///     </list>
///     <para>Runs only when <c>AB_SERVER_PROFILE=emulate</c>. With <c>ab_server</c>
///     (the default tier), skips cleanly — <c>ab_server</c> lacks CIP Template Object
///     (class 0x6C) emulation so the nested-id fetch would time-out regardless.</para>
///     <para>The Emulate tier is hardware-gated (Rockwell per-seat license, Windows-only,
///     conflicts with Docker Desktop's WSL 2 backend) so a permanent skip in CI is expected
///     — see <c>docs/drivers/AbServer-Test-Fixture.md</c> §"Logix Emulate golden-box tier"
///     for the full rationale + the gap matrix this test closes.</para>
/// </remarks>
[Collection("AbServerEmulate")]
[Trait("Category", "Integration")]
[Trait("Tier", "Emulate")]
public sealed class AbCipEmulateNestedUdtTests
{
    /// <summary>
    ///     Verifies that a controller-discovered nested-struct UDT expands into atomic
    ///     leaves addressable as dot-joined paths, proving the nested template-id threading
    ///     runs the production <c>@udt/{id}</c> CIP fetch against Emulate's firmware.
    /// </summary>
    [AbServerFact]
    public async Task Nested_struct_member_discovered_and_expanded_via_template_id_fetch()
    {
        AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);

        var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT")
            ?? throw new InvalidOperationException(
                "AB_SERVER_ENDPOINT must be set to the Logix Emulate instance " +
                "(e.g. '10.0.0.42:44818') when AB_SERVER_PROFILE=emulate.");

        var options = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
            EnableControllerBrowse = true,
            Timeout = TimeSpan.FromSeconds(10),
        };

        await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-nested-udt-smoke");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        // Drive controller discovery — the driver walks @tags, encounters MotorNested1
        // (a Structure symbol), reads its template via @udt/{motorTemplateId}, finds
        // the nested Status member (struct flag set, nested template id in low 12 bits),
        // then re-fetches @udt/{statusTemplateId} to expand the nested leaves.
        var discovered = new DiscoveryRecorder();
        await drv.DiscoverAsync(discovered, TestContext.Current.CancellationToken);

        // The MotorNested1 UDT sub-folder is materialised under Discovered/.
        discovered.FolderNames.ShouldContain("MotorNested1",
            "expected a MotorNested1 sub-folder — check that the Emulate project has a " +
            "MotorNested1 : MotorNested_UDT controller-scope tag and EnableControllerBrowse is true");

        var byName = discovered.Variables.ToDictionary(v => v.FullName, v => v);

        // Top-level atomic member — Speed : DINT.
        byName.ShouldContainKey("MotorNested1.Speed",
            "MotorNested1.Speed not discovered — top-level atomic member expansion failed");

        // Nested struct atomic leaves — Status.Code : DINT, Status.Running : BOOL.
        // These prove the nested template-id fetch actually ran (ab_server can't reach here).
        byName.ShouldContainKey("MotorNested1.Status.Code",
            "MotorNested1.Status.Code not discovered — nested template-id fetch + expansion did not run");
        byName.ShouldContainKey("MotorNested1.Status.Running",
            "MotorNested1.Status.Running not discovered — nested template-id fetch + expansion did not run");

        // The nested struct's sub-folder is materialised (lazy, only when a leaf is emitted).
        discovered.FolderNames.ShouldContain("MotorNested1.Status",
            "expected a MotorNested1.Status sub-folder for the nested StatusNested_UDT expansion");

        // The bare MotorNested1 Structure placeholder Variable must NOT be emitted — the
        // container is replaced by the member fan-out, not emitted alongside it.
        byName.ShouldNotContainKey("MotorNested1",
            "MotorNested1 bare Structure placeholder should NOT be emitted when member fan-out succeeds");
    }

    // ---- minimal recorder wired to IAddressSpaceBuilder ----

    private sealed class DiscoveryRecorder : ZB.MOM.WW.OtOpcUa.Core.Abstractions.IAddressSpaceBuilder
    {
        public List<string> FolderNames { get; } = new();
        public List<(string FullName, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType DataType)> Variables { get; } = new();

        public ZB.MOM.WW.OtOpcUa.Core.Abstractions.IAddressSpaceBuilder Folder(string browseName, string displayName)
        { FolderNames.Add(browseName); return this; }

        public ZB.MOM.WW.OtOpcUa.Core.Abstractions.IVariableHandle Variable(
            string browseName, string displayName, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverAttributeInfo info)
        {
            Variables.Add((info.FullName, info.DriverDataType));
            return new Nop(info.FullName);
        }

        public void AddProperty(string _, ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType __, object? ___) { }

        private sealed class Nop(string fullRef) : ZB.MOM.WW.OtOpcUa.Core.Abstractions.IVariableHandle
        {
            public string FullReference => fullRef;
            public ZB.MOM.WW.OtOpcUa.Core.Abstractions.IAlarmConditionSink MarkAsAlarmCondition(
                ZB.MOM.WW.OtOpcUa.Core.Abstractions.AlarmConditionInfo info) => new NullSink();
        }

        private sealed class NullSink : ZB.MOM.WW.OtOpcUa.Core.Abstractions.IAlarmConditionSink
        {
            public void OnTransition(ZB.MOM.WW.OtOpcUa.Core.Abstractions.AlarmEventArgs args) { }
        }
    }
}
