using System.Net.Sockets;
using Xunit;
using Xunit.Sdk;

namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests;

/// <summary>
///     Reachability probe for a TwinCAT 3 XAR runtime on a Hyper-V VM or dedicated
///     Windows box. TCP-probes ADS port 48898 on the operator-supplied host. Tests
///     skip via <see cref="TwinCATFactAttribute"/> / <see cref="TwinCATTheoryAttribute"/>
///     when the runtime isn't reachable, so <c>dotnet test</c> on a fresh clone without
///     a TwinCAT VM stays green. Matches the
///     <see cref="Modbus.IntegrationTests.ModbusSimulatorFixture"/> /
///     <see cref="S7.IntegrationTests.Snap7ServerFixture"/> /
///     <c>OpcPlcFixture</c> / <c>AbServerFixture</c> patterns.
/// </summary>
/// <remarks>
///     <para><b>Why a VM, not a container</b>: TwinCAT XAR bypasses the Windows
///     kernel scheduler to hit real-time PLC cycles. It can't run inside Docker, and
///     on bare metal it conflicts with Hyper-V / WSL 2 — that's why this repo's dev
///     environment puts XAR in a dedicated Hyper-V VM per
///     <c>docs/v2/dev-environment.md</c> §Integration host. The fixture treats the VM
///     as a black-box ADS endpoint reachable over TCP.</para>
///
///     <para><b>License rotation</b>: the free XAR trial license expires every 7 days.
///     When it lapses the runtime goes silent + the fixture's TCP probe fails; tests
///     skip with the reason message until the operator renews via
///     <c>TcActivate.exe /reactivate</c> (or buys a paid runtime). Intentionally surfaces
///     as a skip rather than a hang because "trial expired" is operator action, not a
///     test failure.</para>
///
///     <para><b>Env var overrides</b>:
///     <list type="bullet">
///       <item><c>TWINCAT_TARGET_HOST</c> — IP or hostname of the XAR VM (default
///         <c>localhost</c>, assumed to be unset on the average dev box + result in a
///         clean skip).</item>
///       <item><c>TWINCAT_TARGET_NETID</c> — AMS NetId the tests address (e.g.
///         <c>5.23.91.23.1.1</c>). Seeded on the target VM via TwinCAT System
///         Manager → Routes; the dev box's AmsNetId also needs a bilateral route
///         entry on the VM side. No sensible default — tests skip if unset.</item>
///       <item><c>TWINCAT_TARGET_PORT</c> — ADS target port (default <c>851</c> =
///         TC3 PLC runtime 1). Set to <c>852</c> for runtime 2, etc.</item>
///     </list></para>
/// </remarks>
public sealed class TwinCATXarFixture : IAsyncLifetime
{
    private const string HostEnvVar   = "TWINCAT_TARGET_HOST";
    private const string NetIdEnvVar  = "TWINCAT_TARGET_NETID";
    private const string PortEnvVar   = "TWINCAT_TARGET_PORT";

    /// <summary>ADS-over-TCP port on the XAR host. Not the PLC runtime port (that's
    /// <see cref="AmsPort"/>).</summary>
    public const int AdsTcpPort = 48898;

    /// <summary>TC3 PLC runtime 1. Override via <see cref="PortEnvVar"/>.</summary>
    public const int DefaultAmsPort = 851;

    /// <summary>Gets the target host address.</summary>
    public string TargetHost { get; }
    /// <summary>Gets the target AMS NetId.</summary>
    public string? TargetNetId { get; }
    /// <summary>Gets the AMS target port.</summary>
    public int AmsPort { get; }
    /// <summary>Gets the reason to skip tests if the fixture is unreachable.</summary>
    public string? SkipReason { get; }

    /// <summary>Initializes a new instance of the TwinCATXarFixture class.</summary>
    public TwinCATXarFixture()
    {
        TargetHost  = Environment.GetEnvironmentVariable(HostEnvVar)  ?? "localhost";
        TargetNetId = Environment.GetEnvironmentVariable(NetIdEnvVar);
        AmsPort     = int.TryParse(Environment.GetEnvironmentVariable(PortEnvVar), out var p)
                          ? p : DefaultAmsPort;

        if (string.IsNullOrWhiteSpace(TargetNetId))
        {
            SkipReason = $"TwinCAT XAR unreachable: {NetIdEnvVar} is not set. " +
                         $"Start the XAR VM + set {HostEnvVar}=<vm-ip> and {NetIdEnvVar}=<vm-ams-netid>.";
            return;
        }

        if (!TcpProbe(TargetHost, AdsTcpPort, TimeSpan.FromSeconds(2)))
        {
            SkipReason = $"TwinCAT XAR at {TargetHost}:{AdsTcpPort} not reachable within 2 s. " +
                         $"Verify the XAR VM is running, its trial license hasn't expired " +
                         $"(run TcActivate.exe /reactivate on the VM), and {HostEnvVar}/{NetIdEnvVar} point at it.";
        }
    }

    /// <inheritdoc />
    public ValueTask InitializeAsync() => ValueTask.CompletedTask;
    /// <inheritdoc />
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;

    /// <summary><c>true</c> when the XAR runtime is reachable + the AmsNetId is set.
    /// Used by the skip attributes to avoid spinning up the fixture for every test
    /// class.</summary>
    public static bool IsRuntimeAvailable()
    {
        if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(NetIdEnvVar))) return false;
        var host = Environment.GetEnvironmentVariable(HostEnvVar) ?? "localhost";
        return TcpProbe(host, AdsTcpPort, TimeSpan.FromMilliseconds(500));
    }

    private static bool TcpProbe(string host, int port, TimeSpan timeout)
    {
        try
        {
            using var client = new TcpClient();
            var task = client.ConnectAsync(host, port);
            return task.Wait(timeout) && client.Connected;
        }
        catch { return false; }
    }
}

[Xunit.CollectionDefinition(Name)]
public sealed class TwinCATXarCollection : Xunit.ICollectionFixture<TwinCATXarFixture>
{
    public const string Name = "TwinCATXar";
}

/// <summary><c>[Fact]</c>-equivalent gated on <see cref="TwinCATXarFixture.IsRuntimeAvailable"/>.</summary>
public sealed class TwinCATFactAttribute : FactAttribute
{
    /// <summary>Initializes a new instance of the TwinCATFactAttribute class.</summary>
    public TwinCATFactAttribute()
    {
        if (!TwinCATXarFixture.IsRuntimeAvailable())
            Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " +
                   "for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset.";
    }
}

/// <summary><c>[Theory]</c>-equivalent with the same gate as <see cref="TwinCATFactAttribute"/>.</summary>
public sealed class TwinCATTheoryAttribute : TheoryAttribute
{
    /// <summary>Initializes a new instance of the TwinCATTheoryAttribute class.</summary>
    public TwinCATTheoryAttribute()
    {
        if (!TwinCATXarFixture.IsRuntimeAvailable())
            Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " +
                   "for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset.";
    }
}
