using System.Diagnostics;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Principal;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E;

/// <summary>
///     Spawns one <c>OtOpcUa.Driver.Galaxy.Host.exe</c> subprocess per test class and exposes
///     a connected <see cref="GalaxyProxyDriver"/> for the tests. Per Phase 2 plan §"Stream E
///     Parity Validation": the Proxy owns a session against a real out-of-process Host running
///     the production-shape <c>MxAccessGalaxyBackend</c> backed by live ZB + MXAccess COM.
///     Skipped when the Host EXE isn't built, when ZB SQL is unreachable, or when the dev box
///     runs as Administrator (the IPC ACL explicitly denies Administrators per decision #76).
/// </summary>
public sealed class ParityFixture : IAsyncLifetime
{
    public GalaxyProxyDriver? Driver { get; private set; }
    public string? SkipReason { get; private set; }

    private Process? _host;
    private const string Secret = "parity-suite-secret";

    public async ValueTask InitializeAsync()
    {
        if (!OperatingSystem.IsWindows()) { SkipReason = "Windows-only"; return; }
        if (IsAdministrator()) { SkipReason = "PipeAcl denies Administrators on dev shells"; return; }
        if (!await ZbReachableAsync()) { SkipReason = "Galaxy ZB SQL not reachable on localhost:1433"; return; }

        var hostExe = FindHostExe();
        if (hostExe is null) { SkipReason = "Galaxy.Host EXE not built — run `dotnet build src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host`"; return; }

        // Use the SQL-only DB backend by default — exercises the full IPC + dispatcher + SQL
        // path without requiring a healthy MXAccess connection. Tests that need MXAccess
        // override via env vars before InitializeAsync is called (use a separate fixture).
        var pipe = $"OtOpcUaGalaxyParity-{Guid.NewGuid():N}";
        using var identity = WindowsIdentity.GetCurrent();
        var sid = identity.User!;

        var psi = new ProcessStartInfo(hostExe)
        {
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            EnvironmentVariables =
            {
                ["OTOPCUA_GALAXY_PIPE"] = pipe,
                ["OTOPCUA_ALLOWED_SID"] = sid.Value,
                ["OTOPCUA_GALAXY_SECRET"] = Secret,
                ["OTOPCUA_GALAXY_BACKEND"] = "db",
                ["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
            },
        };

        _host = Process.Start(psi)
            ?? throw new InvalidOperationException("Failed to spawn Galaxy.Host EXE");

        // Give the PipeServer ~2s to bind. The supervisor's HeartbeatMonitor can do this
        // in production with retry, but the parity tests are best served by a fixed warm-up.
        await Task.Delay(2_000);

        Driver = new GalaxyProxyDriver(new GalaxyProxyOptions
        {
            DriverInstanceId = "parity",
            PipeName = pipe,
            SharedSecret = Secret,
            ConnectTimeout = TimeSpan.FromSeconds(5),
        });

        await Driver.InitializeAsync(driverConfigJson: "{}", CancellationToken.None);
    }

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

        if (_host is not null && !_host.HasExited)
        {
            try { _host.Kill(entireProcessTree: true); } catch { /* ignore */ }
            try { _host.WaitForExit(5_000); } catch { /* ignore */ }
        }
        _host?.Dispose();
    }

    /// <summary>Skip the test if the fixture couldn't initialize. xUnit Skip.If pattern.</summary>
    public void SkipIfUnavailable()
    {
        if (SkipReason is not null)
            Assert.Skip(SkipReason);
    }

    private static bool IsAdministrator()
    {
        if (!OperatingSystem.IsWindows()) return false;
        using var identity = WindowsIdentity.GetCurrent();
        return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
    }

    private static async Task<bool> ZbReachableAsync()
    {
        try
        {
            using var client = new TcpClient();
            var task = client.ConnectAsync("localhost", 1433);
            return await Task.WhenAny(task, Task.Delay(1_500)) == task && client.Connected;
        }
        catch { return false; }
    }

    private static string? FindHostExe()
    {
        var asmDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
        var solutionRoot = asmDir;
        for (var i = 0; i < 8 && solutionRoot is not null; i++)
        {
            if (File.Exists(Path.Combine(solutionRoot, "ZB.MOM.WW.OtOpcUa.slnx"))) break;
            solutionRoot = Path.GetDirectoryName(solutionRoot);
        }
        if (solutionRoot is null) return null;

        var path = Path.Combine(solutionRoot,
            "src", "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host", "bin", "Debug", "net48",
            "OtOpcUa.Driver.Galaxy.Host.exe");
        return File.Exists(path) ? path : null;
    }
}

[CollectionDefinition(nameof(ParityCollection))]
public sealed class ParityCollection : ICollectionFixture<ParityFixture> { }
