using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack;

/// <summary>
///     Connects a single <see cref="GalaxyProxyDriver"/> to the already-running
///     <c>OtOpcUaGalaxyHost</c> Windows service for the lifetime of a test class. Uses
///     <see cref="AvevaPrerequisites"/> to decide whether to proceed; on failure,
///     <see cref="SkipReason"/> is populated and each test calls <see cref="SkipIfUnavailable"/>
///     to translate that into <c>Assert.Skip</c>.
/// </summary>
/// <remarks>
///     <para>
///         <b>Does NOT spawn the Host process.</b> Production deploys <c>OtOpcUaGalaxyHost</c>
///         as a standalone Windows service — spawning a second instance from a test would
///         bypass the COM-apartment + service-account setup and fail differently than
///         production (see <c>project_galaxy_host_service.md</c> memory).
///     </para>
///     <para>
///         <b>Shared-secret handling</b>: read from <see cref="LiveStackConfig"/> — env vars
///         first, then the service's registry-stored <c>Environment</c> values. Requires
///         the test process to have read access to
///         <c>HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost</c>; on a dev box
///         that typically means running the test host elevated, or exporting
///         <c>OTOPCUA_GALAXY_SECRET</c> out-of-band.
///     </para>
/// </remarks>
public sealed class LiveStackFixture : IAsyncLifetime
{
    public GalaxyProxyDriver? Driver { get; private set; }

    public string? SkipReason { get; private set; }

    public PrerequisiteReport? PrerequisiteReport { get; private set; }

    public LiveStackConfig? Config { get; private set; }

    public async ValueTask InitializeAsync()
    {
        // 0. Elevated-shell short-circuit. The OtOpcUaGalaxyHost pipe ACL allows the configured
        //    SID but explicitly DENIES Administrators (decision #76 — production hardening).
        //    A test process running with a high-integrity token (any elevated shell) carries the
        //    Admins group in its security context, so the deny rule trumps the user's allow and
        //    the pipe connect returns UnauthorizedAccessException — technically correct but
        //    the operationally confusing failure mode that ate most of the PR 37 install
        //    debugging session. Surfacing it explicitly here saves the next operator the same
        //    five-step diagnosis. ParityFixture has the same skip with the same rationale.
        if (IsElevatedAdministratorOnWindows())
        {
            SkipReason =
                "Test host is running with elevated (Administrators) privileges, but the " +
                "OtOpcUaGalaxyHost named-pipe ACL explicitly denies Administrators per the IPC " +
                "security design (decision #76 / PipeAcl.cs). Re-run from a NORMAL (non-admin) " +
                "PowerShell window — even when your user is already in the pipe's allow list, " +
                "the elevated token's Admins group membership trumps the allow rule.";
            return;
        }

        // 1. AVEVA + OtOpcUa service state — actionable diagnostic if anything is missing.
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        PrerequisiteReport = await AvevaPrerequisites.CheckAllAsync(
            new AvevaPrerequisites.Options { CheckGalaxyHostPipe = true, CheckHistorian = false },
            cts.Token);

        if (!PrerequisiteReport.IsLivetestReady)
        {
            SkipReason = PrerequisiteReport.SkipReason;
            return;
        }

        // 2. Secret / pipe-name resolution. If the service is running but we can't discover its
        //    env vars from registry (non-elevated test host), a clear message beats a silent
        //    connect-rejected failure 10 seconds later.
        Config = LiveStackConfig.Resolve();
        if (Config is null)
        {
            SkipReason =
                $"Cannot resolve shared secret. Set {LiveStackConfig.EnvSharedSecret} (and optionally " +
                $"{LiveStackConfig.EnvPipeName}) in the environment, or run the test host elevated so it " +
                $"can read HKLM\\{LiveStackConfig.ServiceRegistryKey}\\Environment.";
            return;
        }

        // 3. Connect. InitializeAsync does the pipe connect + handshake; a 5-second
        //    ConnectTimeout gives enough headroom for a service that just started.
        Driver = new GalaxyProxyDriver(new GalaxyProxyOptions
        {
            DriverInstanceId = "live-stack-smoke",
            PipeName = Config.PipeName,
            SharedSecret = Config.SharedSecret,
            ConnectTimeout = TimeSpan.FromSeconds(5),
        });

        try
        {
            await Driver.InitializeAsync(driverConfigJson: "{}", CancellationToken.None);
        }
        catch (Exception ex)
        {
            SkipReason =
                $"Connected to named pipe '{Config.PipeName}' but GalaxyProxyDriver.InitializeAsync failed: " +
                $"{ex.GetType().Name}: {ex.Message}. Common causes: shared secret mismatch (rotated after last install), " +
                $"service account SID not in pipe ACL (installer sets OTOPCUA_ALLOWED_SID to the service account — " +
                $"test must run as that user), or Host's backend couldn't connect to ZB.";
            Driver.Dispose();
            Driver = null;
            return;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (Driver is not null)
        {
            try { await Driver.ShutdownAsync(CancellationToken.None); } catch { /* best-effort */ }
            Driver.Dispose();
        }
    }

    /// <summary>
    ///     Translate <see cref="SkipReason"/> into <c>Assert.Skip</c>. Tests call this at the
    ///     top of every fact so a fixture init failure shows up as a cleanly-skipped test with
    ///     the full prerequisites report, not a cascading NullReferenceException on
    ///     <see cref="Driver"/>.
    /// </summary>
    public void SkipIfUnavailable()
    {
        if (SkipReason is not null) Assert.Skip(SkipReason);
    }

    private static bool IsElevatedAdministratorOnWindows()
    {
        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false;
        return CheckWindowsAdminToken();
    }

    [SupportedOSPlatform("windows")]
    private static bool CheckWindowsAdminToken()
    {
        try
        {
            using var identity = WindowsIdentity.GetCurrent();
            return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
        }
        catch
        {
            // Probe shouldn't crash the test; if we can't determine elevation, optimistically
            // continue and let the actual pipe connect surface its own error.
            return false;
        }
    }
}

[CollectionDefinition(Name)]
public sealed class LiveStackCollection : ICollectionFixture<LiveStackFixture>
{
    public const string Name = "LiveStack";
}
