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

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

/// <summary>
///     Covers <see cref="AbCipCommandBase"/>: the shared <c>BuildOptions</c> projection
///     (driver-options mapping the four commands depend on), the <c>RejectStructure</c>
///     guard, the <c>Timeout</c> override behaviour, and <c>TimeoutMs</c> validation.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbCipCommandBaseTests
{
    /// <summary>
    ///     Local subclass that surfaces the protected helpers + properties under test.
    /// </summary>
    [CliFx.Attributes.Command("test")]
    private sealed class TestableCommand : AbCipCommandBase
    {
        /// <summary>
        /// Invokes the protected <see cref="AbCipCommandBase.BuildOptions"/> method.
        /// </summary>
        /// <param name="tags">The tags to pass to BuildOptions.</param>
        /// <returns>The configured driver options.</returns>
        public AbCipDriverOptions InvokeBuildOptions(IReadOnlyList<AbCipTagDefinition> tags)
            => BuildOptions(tags);

        /// <summary>
        /// Gets the protected <see cref="AbCipCommandBase.DriverInstanceId"/> property.
        /// </summary>
        public string InvokeDriverInstanceId => DriverInstanceId;

        /// <inheritdoc />
        public override ValueTask ExecuteAsync(CliFx.Infrastructure.IConsole console)
            => ValueTask.CompletedTask;
    }

    private static AbCipTagDefinition SampleTag(string name = "Motor01") => new(
        Name: name,
        DeviceHostAddress: "ab://10.0.0.5/1,0",
        TagPath: "Motor01",
        DataType: AbCipDataType.DInt,
        Writable: false);

    /// <summary>
    /// Verifies that BuildOptions disables probe to prevent racing operator reads.
    /// </summary>
    [Fact]
    public void BuildOptions_disables_probe_so_cli_does_not_race_operator_reads()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
            TimeoutMs = 5000,
        };

        var options = cmd.InvokeBuildOptions([SampleTag()]);

        options.Probe.Enabled.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that BuildOptions disables controller browse.
    /// </summary>
    [Fact]
    public void BuildOptions_disables_controller_browse()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
            TimeoutMs = 5000,
        };

        var options = cmd.InvokeBuildOptions([SampleTag()]);

        options.EnableControllerBrowse.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that BuildOptions disables alarm projection.
    /// </summary>
    [Fact]
    public void BuildOptions_disables_alarm_projection()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
            TimeoutMs = 5000,
        };

        var options = cmd.InvokeBuildOptions([SampleTag()]);

        options.EnableAlarmProjection.ShouldBeFalse();
    }

    /// <summary>
    /// Verifies that BuildOptions produces one device with gateway family and derived name.
    /// </summary>
    [Fact]
    public void BuildOptions_produces_one_device_with_gateway_family_and_derived_name()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.CompactLogix,
            TimeoutMs = 5000,
        };

        var options = cmd.InvokeBuildOptions([SampleTag()]);

        options.Devices.Count.ShouldBe(1);
        var device = options.Devices[0];
        device.HostAddress.ShouldBe("ab://10.0.0.5/1,0");
        device.PlcFamily.ShouldBe(AbCipPlcFamily.CompactLogix);
        device.DeviceName.ShouldBe("cli-CompactLogix");
    }

    /// <summary>
    /// Verifies that BuildOptions passes the supplied tag list verbatim.
    /// </summary>
    [Fact]
    public void BuildOptions_passes_supplied_tag_list_verbatim()
    {
        var tags = new[] { SampleTag("t1"), SampleTag("t2") };
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
            TimeoutMs = 5000,
        };

        var options = cmd.InvokeBuildOptions(tags);

        // v3: BuildOptions projects each typed tag to a RawTagEntry (RawPath = tag name).
        options.RawTags.Count.ShouldBe(2);
        options.RawTags[0].RawPath.ShouldBe("t1");
        options.RawTags[1].RawPath.ShouldBe("t2");
    }

    /// <summary>
    /// Verifies that BuildOptions carries TimeoutMs through to Timeout.
    /// </summary>
    [Fact]
    public void BuildOptions_carries_TimeoutMs_through_to_Timeout()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
            TimeoutMs = 7500,
        };

        var options = cmd.InvokeBuildOptions([SampleTag()]);

        options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7500));
    }

    /// <summary>
    /// Verifies that DriverInstanceId embeds gateway for log disambiguation.
    /// </summary>
    [Fact]
    public void DriverInstanceId_embeds_gateway_for_log_disambiguation()
    {
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Family = AbCipPlcFamily.ControlLogix,
        };

        cmd.InvokeDriverInstanceId.ShouldBe("abcip-cli-ab://10.0.0.5/1,0");
    }

    /// <summary>
    /// Verifies that Timeout setter is inert and does not silently swallow assignments.
    /// </summary>
    [Fact]
    public void Timeout_setter_is_inert_and_does_not_silently_swallow_assignments()
    {
        // Driver.AbCip.Cli-006 — the empty init body would silently discard an
        // object-initializer assignment, hiding a "driven by TimeoutMs" misuse. The fix
        // makes it fail-fast with NotSupportedException so the contract is explicit.
        Should.Throw<NotSupportedException>(() => new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            Timeout = TimeSpan.FromSeconds(99),
        });
    }

    /// <summary>
    /// Verifies that Timeout getter throws CommandException when TimeoutMs is non-positive.
    /// </summary>
    /// <param name="badMs">A non-positive timeout value to test.</param>
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void Timeout_get_throws_CommandException_when_TimeoutMs_is_non_positive(int badMs)
    {
        // Driver.AbCip.Cli-004 — TimeoutMs must be > 0. Validation is exposed via the
        // Timeout getter so any command path that touches Timeout sees the same guard.
        var cmd = new TestableCommand
        {
            Gateway = "ab://10.0.0.5/1,0",
            TimeoutMs = badMs,
        };

        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => _ = cmd.Timeout);
        ex.Message.ShouldContain("--timeout-ms");
    }

    /// <summary>
    /// Verifies that RejectStructure throws for Structure DataType.
    /// </summary>
    [Fact]
    public void RejectStructure_throws_for_Structure_DataType()
    {
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(
            () => CallRejectStructure(AbCipDataType.Structure));
        ex.Message.ShouldContain("Structure");
    }

    /// <summary>
    /// Verifies that RejectStructure passes for atomic data types.
    /// </summary>
    /// <param name="type">The atomic data type to test.</param>
    [Theory]
    [InlineData(AbCipDataType.DInt)]
    [InlineData(AbCipDataType.Bool)]
    [InlineData(AbCipDataType.Real)]
    public void RejectStructure_passes_for_atomic_types(AbCipDataType type)
    {
        // No throw — atomic types are allowed.
        Should.NotThrow(() => CallRejectStructure(type));
    }

    // The static helper is protected; reflect to it once so the test stays at AbCipCommandBase.
    private static void CallRejectStructure(AbCipDataType type)
    {
        var method = typeof(AbCipCommandBase).GetMethod(
            "RejectStructure",
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
            ?? throw new InvalidOperationException("RejectStructure not found");
        try
        {
            method.Invoke(null, [type]);
        }
        catch (System.Reflection.TargetInvocationException tie) when (tie.InnerException is not null)
        {
            throw tie.InnerException;
        }
    }
}
