using CliFx.Attributes;
using CliFx.Infrastructure;
using Shouldly;
using Xunit;

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

/// <summary>
///     Driver.FOCAS.Cli-003: numeric options that are not range-checked at the CLI
///     boundary surface either as opaque downstream exceptions or as tight-spinning
///     poll loops rather than a clear "value must be positive" message. These tests
///     pin the validation contract for <c>--cnc-port</c>, <c>--timeout-ms</c>, and
///     <c>--interval-ms</c>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class FocasCommandBaseValidationTests
{
    // Test-only FocasCommandBase concrete subclass that exposes the protected ValidateOptions
    // helper. The [Command] attribute is required by the CliFx analyzer
    // (CliFx_CommandMustBeAnnotated) — this command is never registered with the CLI app
    // but the analyzer rule fires for every ICommand implementor in the compilation.
    [Command("noop-test", Description = "Test-only probe of FocasCommandBase.ValidateOptions.")]
    private sealed class Probe : FocasCommandBase
    {
        /// <summary>Gets or sets the interval in milliseconds.</summary>
        public int IntervalMs { get; init; }

        /// <inheritdoc />
        public override ValueTask ExecuteAsync(IConsole console) => default;

        /// <summary>Invokes option validation with interval.</summary>
        public void InvokeValidate() => ValidateOptions(IntervalMs);

        /// <summary>Invokes option validation without interval.</summary>
        public void InvokeValidateNoInterval() => ValidateOptions(intervalMs: null);
    }

    /// <summary>Verifies that default options are accepted.</summary>
    [Fact]
    public void Validate_accepts_default_options()
    {
        var sut = new Probe { CncHost = "host", IntervalMs = 1000 };
        Should.NotThrow(() => sut.InvokeValidate());
    }

    /// <summary>Verifies that out-of-range CNC port is rejected.</summary>
    /// <param name="port">The port value to test.</param>
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(65536)]
    public void Validate_rejects_out_of_range_cnc_port(int port)
    {
        var sut = new Probe { CncHost = "host", CncPort = port, IntervalMs = 1000 };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
        ex.Message.ShouldContain("cnc-port", Case.Insensitive);
    }

    /// <summary>Verifies that non-positive timeout is rejected.</summary>
    /// <param name="timeoutMs">The timeout value in milliseconds to test.</param>
    [Theory]
    [InlineData(0)]
    [InlineData(-100)]
    public void Validate_rejects_non_positive_timeout_ms(int timeoutMs)
    {
        var sut = new Probe { CncHost = "host", TimeoutMs = timeoutMs, IntervalMs = 1000 };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
        ex.Message.ShouldContain("timeout-ms", Case.Insensitive);
    }

    /// <summary>Verifies that non-positive interval is rejected.</summary>
    /// <param name="intervalMs">The interval value in milliseconds to test.</param>
    [Theory]
    [InlineData(0)]
    [InlineData(-500)]
    public void Validate_rejects_non_positive_interval_ms(int intervalMs)
    {
        var sut = new Probe { CncHost = "host", IntervalMs = intervalMs };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => sut.InvokeValidate());
        ex.Message.ShouldContain("interval-ms", Case.Insensitive);
    }

    /// <summary>Verifies that interval check is skipped when command omits it.</summary>
    [Fact]
    public void Validate_skips_interval_check_when_command_omits_it()
    {
        // probe / read / write don't take an --interval-ms option; the validator must
        // skip that check when the caller passes null.
        var sut = new Probe { CncHost = "host" };
        Should.NotThrow(() => sut.InvokeValidateNoInterval());
    }
}
