using System.Diagnostics;
using System.Reflection;
using System.Security.Principal;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Ipc;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;

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

/// <summary>
///     The honest cross-FX parity test — spawns the actual <c>OtOpcUa.Driver.Galaxy.Host.exe</c>
///     subprocess (net48 x86), the Proxy connects via real named pipe, exercises Discover
///     against the live Galaxy ZB DB, and asserts gobjects come back. This is the production
///     deployment shape (Tier C: separate process, IPC over named pipe, Proxy in the .NET 10
///     server process). Skipped when the Host EXE isn't built or Galaxy is unreachable.
/// </summary>
[Trait("Category", "ProcessSpawnParity")]
public sealed class HostSubprocessParityTests : IDisposable
{
    private Process? _hostProcess;

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

    private static string? FindHostExe()
    {
        // The test assembly lives at tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/bin/Debug/net10.0/.
        // The Host EXE lives at src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/bin/Debug/net48/.
        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 candidate = Path.Combine(solutionRoot,
            "src", "ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host", "bin", "Debug", "net48",
            "OtOpcUa.Driver.Galaxy.Host.exe");
        return File.Exists(candidate) ? candidate : null;
    }

    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 System.Net.Sockets.TcpClient();
            var task = client.ConnectAsync("localhost", 1433);
            return await Task.WhenAny(task, Task.Delay(1_500)) == task && client.Connected;
        }
        catch { return false; }
    }

    [Fact]
    public async Task Spawned_Host_in_db_mode_lets_Proxy_Discover_real_Galaxy_gobjects()
    {
        if (!OperatingSystem.IsWindows() || IsAdministrator()) return;
        if (!await ZbReachableAsync()) return;

        var hostExe = FindHostExe();
        if (hostExe is null) return; // skip when the Host hasn't been built

        using var identity = WindowsIdentity.GetCurrent();
        var sid = identity.User!;
        var pipeName = $"OtOpcUaGalaxyParity-{Guid.NewGuid():N}";
        const string secret = "parity-secret";

        var psi = new ProcessStartInfo(hostExe)
        {
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            EnvironmentVariables =
            {
                ["OTOPCUA_GALAXY_PIPE"] = pipeName,
                ["OTOPCUA_ALLOWED_SID"] = sid.Value,
                ["OTOPCUA_GALAXY_SECRET"] = secret,
                ["OTOPCUA_GALAXY_BACKEND"] = "db", // SQL-only — doesn't need MXAccess
                ["OTOPCUA_GALAXY_ZB_CONN"] = "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;",
            },
        };

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

        // Wait for the pipe to come up — the Host's PipeServer takes ~100ms to bind.
        await Task.Delay(2_000);

        await using var client = await GalaxyIpcClient.ConnectAsync(
            pipeName, secret, TimeSpan.FromSeconds(5), CancellationToken.None);

        var sessionResp = await client.CallAsync<OpenSessionRequest, OpenSessionResponse>(
            MessageKind.OpenSessionRequest,
            new OpenSessionRequest { DriverInstanceId = "parity", DriverConfigJson = "{}" },
            MessageKind.OpenSessionResponse,
            CancellationToken.None);
        sessionResp.Success.ShouldBeTrue(sessionResp.Error);

        var discoverResp = await client.CallAsync<DiscoverHierarchyRequest, DiscoverHierarchyResponse>(
            MessageKind.DiscoverHierarchyRequest,
            new DiscoverHierarchyRequest { SessionId = sessionResp.SessionId },
            MessageKind.DiscoverHierarchyResponse,
            CancellationToken.None);

        discoverResp.Success.ShouldBeTrue(discoverResp.Error);
        discoverResp.Objects.Length.ShouldBeGreaterThan(0,
            "live Galaxy ZB has at least one deployed gobject");

        await client.SendOneWayAsync(MessageKind.CloseSessionRequest,
            new CloseSessionRequest { SessionId = sessionResp.SessionId }, CancellationToken.None);
    }
}
