using System.Reflection;
using CliFx.Attributes;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;

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

/// <summary>
///     Covers <see cref="TwinCATCommandBase"/> / <see cref="TwinCATTagCommandBase"/> wiring:
///     the canonical gateway string, the driver instance id, the BuildOptions field projection
///     (Driver.TwinCAT.Cli-006), and the up-front range validation guards
///     (Driver.TwinCAT.Cli-001).
/// </summary>
[Trait("Category", "Unit")]
public sealed class TwinCATCommandBaseTests
{
    /// <summary>Verifies that the gateway string uses canonical ADS scheme with port.</summary>
    [Fact]
    public void Gateway_uses_canonical_ads_scheme_with_port()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "192.168.1.40.1.1",
            AmsPort = 851,
            SymbolPath = "MAIN.bRunning",
        };
        cmd.GatewayForTest.ShouldBe("ads://192.168.1.40.1.1:851");
    }

    /// <summary>Verifies that the gateway string round-trips through TwinCATAmsAddress.TryParse.</summary>
    [Fact]
    public void Gateway_round_trips_through_TwinCATAmsAddress_TryParse()
    {
        // Driver.TwinCAT.Cli-006: a regression in the Gateway string breaks every command
        // because the driver's TwinCATAmsAddress.TryParse refuses anything not shaped
        // ads://{netId}:{port}.
        var cmd = new ProbeCommand
        {
            AmsNetId = "5.23.91.23.1.1",
            AmsPort = 852,
            SymbolPath = "MAIN.x",
        };
        var parsed = TwinCAT.TwinCATAmsAddress.TryParse(cmd.GatewayForTest);
        parsed.ShouldNotBeNull();
        parsed!.NetId.ShouldBe("5.23.91.23.1.1");
        parsed.Port.ShouldBe(852);
    }

    /// <summary>Verifies that the driver instance ID includes the AMS target.</summary>
    [Fact]
    public void DriverInstanceId_includes_ams_target()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            AmsPort = 851,
            SymbolPath = "MAIN.x",
        };
        cmd.DriverInstanceIdForTest.ShouldBe("twincat-cli-127.0.0.1.1.1:851");
    }

    /// <summary>Verifies that timeout is a projection of TimeoutMs and initialization is a no-op.</summary>
    [Fact]
    public void Timeout_is_projection_of_TimeoutMs_and_init_is_noop()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            TimeoutMs = 7777,
            SymbolPath = "MAIN.x",
        };
        cmd.Timeout.ShouldBe(TimeSpan.FromMilliseconds(7777));
    }

    /// <summary>Verifies that BuildOptions wires device tags, timeout, and disables probe.</summary>
    [Fact]
    public void BuildOptions_wires_device_tags_timeout_and_disables_probe()
    {
        // Driver.TwinCAT.Cli-006: cover the property-by-property wiring that the four runtime
        // commands depend on. Probe must be disabled (CLI is one-shot — the probe loop would
        // race the operator's own reads) and controller-browse must stay off.
        var cmd = new ProbeCommand
        {
            AmsNetId = "10.0.0.1.1.1",
            AmsPort = 851,
            TimeoutMs = 4321,
            SymbolPath = "MAIN.x",
        };
        var tag = new TwinCAT.TwinCATTagDefinition(
            Name: "n1",
            DeviceHostAddress: cmd.GatewayForTest,
            SymbolPath: "MAIN.x",
            DataType: TwinCAT.TwinCATDataType.DInt,
            Writable: false);

        var options = cmd.BuildOptionsForTest([tag]);

        options.Devices.Count.ShouldBe(1);
        options.Devices[0].HostAddress.ShouldBe("ads://10.0.0.1.1.1:851");
        options.Devices[0].DeviceName.ShouldBe("cli-10.0.0.1.1.1:851");
        // v3: the CLI serialises its typed def into a RawTagEntry (RawPath = def.Name, DeviceName =
        // def.DeviceHostAddress) that round-trips through the factory back to the same def.
        options.RawTags.Count.ShouldBe(1);
        var entry = options.RawTags[0];
        entry.RawPath.ShouldBe("n1");
        entry.DeviceName.ShouldBe(cmd.GatewayForTest);
        TwinCAT.TwinCATTagDefinitionFactory.FromTagConfig(entry.TagConfig, entry.RawPath, out var roundTripped)
            .ShouldBeTrue();
        roundTripped.SymbolPath.ShouldBe("MAIN.x");
        roundTripped.DataType.ShouldBe(TwinCAT.TwinCATDataType.DInt);
        options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(4321));
        options.Probe.Enabled.ShouldBeFalse();
        options.EnableControllerBrowse.ShouldBeFalse();
        // Default UseNativeNotifications = true (no --poll-only).
        options.UseNativeNotifications.ShouldBeTrue();
    }

    /// <summary>Verifies that the PollOnly flag flips UseNativeNotifications off.</summary>
    [Fact]
    public void BuildOptions_PollOnly_flips_UseNativeNotifications_off()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "10.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            PollOnly = true,
        };
        cmd.BuildOptionsForTest([]).UseNativeNotifications.ShouldBeFalse();
    }

    // ---- Driver.TwinCAT.Cli-001 (range validation) ----

    /// <summary>Verifies that validation rejects zero timeout.</summary>
    [Fact]
    public void Validate_rejects_zero_timeout()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            TimeoutMs = 0,
        };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
        ex.Message.ShouldContain("--timeout-ms");
    }

    /// <summary>Verifies that validation rejects negative timeout.</summary>
    [Fact]
    public void Validate_rejects_negative_timeout()
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            TimeoutMs = -1,
        };
        Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
    }

    /// <summary>Verifies that validation rejects out-of-range AMS port values.</summary>
    /// <param name="port">The out-of-range AMS port value to test.</param>
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(65536)]
    [InlineData(100000)]
    public void Validate_rejects_out_of_range_ams_port(int port)
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            AmsPort = port,
        };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
        ex.Message.ShouldContain("--ams-port");
    }

    /// <summary>Verifies that validation accepts in-range AMS port values.</summary>
    /// <param name="port">The valid AMS port value to test.</param>
    [Theory]
    [InlineData(1)]
    [InlineData(801)]
    [InlineData(851)]
    [InlineData(65535)]
    public void Validate_accepts_in_range_ams_port(int port)
    {
        var cmd = new ProbeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            AmsPort = port,
        };
        Should.NotThrow(() => cmd.ValidateForTest());
    }

    /// <summary>Verifies that SubscribeCommand validation rejects zero interval.</summary>
    [Fact]
    public void SubscribeCommand_validate_rejects_zero_interval()
    {
        var cmd = new SubscribeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            IntervalMs = 0,
        };
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
        ex.Message.ShouldContain("--interval-ms");
    }

    /// <summary>Verifies that SubscribeCommand validation rejects negative interval.</summary>
    [Fact]
    public void SubscribeCommand_validate_rejects_negative_interval()
    {
        var cmd = new SubscribeCommand
        {
            AmsNetId = "127.0.0.1.1.1",
            SymbolPath = "MAIN.x",
            IntervalMs = -100,
        };
        Should.Throw<CliFx.Exceptions.CommandException>(() => cmd.ValidateForTest());
    }

    // ---- Driver.TwinCAT.Cli-004 (PollOnly off BrowseCommand surface) ----

    /// <summary>Verifies that BrowseCommand does not expose the poll-only flag.</summary>
    [Fact]
    public void BrowseCommand_does_not_expose_poll_only_flag()
    {
        // Driver.TwinCAT.Cli-004: the flag has no observable effect on browse — surfacing it
        // misleads users. After the refactor, PollOnly lives on an intermediate base shared
        // only by the commands that actually consume native ADS notifications.
        var props = typeof(BrowseCommand)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance);
        props.ShouldNotContain(p => p.Name == "PollOnly");
    }

    /// <summary>Verifies that ProbeCommand still exposes the poll-only flag.</summary>
    [Fact]
    public void ProbeCommand_still_exposes_poll_only_flag()
    {
        // Probe / Read / Write / Subscribe all build TwinCATDriverOptions and so still take
        // the --poll-only toggle.
        var props = typeof(ProbeCommand)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance);
        props.ShouldContain(p => p.Name == "PollOnly");
    }

    // ---- Driver.TwinCAT.Cli-005 (probe --type short alias) ----

    /// <summary>Verifies that ProbeCommand type option carries the short alias 't'.</summary>
    [Fact]
    public void ProbeCommand_type_option_carries_short_alias_t()
    {
        // Driver.TwinCAT.Cli-005: --type on read/write/subscribe takes the -t short alias;
        // probe must match so muscle memory works the same way across all four verbs.
        var dataTypeProp = typeof(ProbeCommand).GetProperty("DataType");
        dataTypeProp.ShouldNotBeNull();
        var attr = dataTypeProp!.GetCustomAttribute<CommandOptionAttribute>();
        attr.ShouldNotBeNull();
        attr!.ShortName.ShouldBe('t');
    }
}
