using System.Net.Sockets;
using System.Text;
using System.Text.Json;

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

/// <summary>
///     Fixture for the focas-mock simulator. Probes the Docker mock at
///     collection init; if reachable, exposes helpers that drive the mock's
///     admin surface (<c>mock_load_profile</c>, <c>mock_patch</c>,
///     <c>mock_reset</c>, <c>mock_schedule_alarms</c>) so tests can seed
///     deterministic state before exercising the managed driver.
/// </summary>
/// <remarks>
///     Single skip gate: <see cref="SkipReason"/> is non-null when the
///     <c>localhost:8193</c> TCP probe fails. Tests call
///     <c>Assert.Skip</c>.
/// </remarks>
public sealed class FocasSimFixture : IAsyncDisposable
{
    private const string EndpointEnvVar = "OTOPCUA_FOCAS_SIM_ENDPOINT";
    private const string ProfileEnvVar  = "OTOPCUA_FOCAS_SIM_PROFILE";
    private const string DefaultHost = "localhost";
    private const int DefaultPort = 8193;

    /// <summary>Gets the hostname or IP address of the focas-mock simulator.</summary>
    public string Host { get; }

    /// <summary>Gets the TCP port of the focas-mock simulator.</summary>
    public int Port { get; }

    /// <summary>Gets the <c>focas://host:port</c> device URI the driver connects to — derived from
    /// <see cref="Host"/>/<see cref="Port"/> so tests honour <c>OTOPCUA_FOCAS_SIM_ENDPOINT</c> (a remote
    /// fixture on the Docker host) instead of assuming a localhost-colocated mock.</summary>
    public string DeviceUri => $"focas://{Host}:{Port}";

    /// <summary>focas-mock profile stem the fixture should load (e.g. <c>fwlib30i64</c>,
    /// <c>ThirtyOne_i</c> — both resolve via the mock's alias table). Null when unset.</summary>
    public string? ExpectedProfile { get; }

    /// <summary>When the <see cref="ExpectedProfile"/> maps to a concrete
    /// <see cref="FocasCncSeries"/>, this is it. Null otherwise.</summary>
    public FocasCncSeries? ExpectedSeries { get; }

    /// <summary>Non-null when the mock probe failed — tests skip with this reason.</summary>
    public string? SkipReason { get; }

    /// <summary>
    ///     Initializes a new instance of the <see cref="FocasSimFixture"/> class by probing the focas-mock
    ///     simulator endpoint. Reads the endpoint and profile from environment variables or uses defaults.
    /// </summary>
    public FocasSimFixture()
    {
        var endpoint = Environment.GetEnvironmentVariable(EndpointEnvVar) ?? $"{DefaultHost}:{DefaultPort}";
        (Host, Port) = ParseEndpoint(endpoint);

        ExpectedProfile = Environment.GetEnvironmentVariable(ProfileEnvVar);
        ExpectedSeries = ParseSeries(ExpectedProfile);

        try
        {
            using var client = new TcpClient(AddressFamily.InterNetwork);
            var addresses = System.Net.Dns.GetHostAddresses(Host);
            var ip = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork)
                     ?? System.Net.IPAddress.Loopback;
            var task = client.ConnectAsync(ip, Port);
            if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
            {
                SkipReason = $"focas-mock at {Host}:{Port} did not accept a TCP connection within 2s. " +
                             $"Start it (`docker compose -f Docker/docker-compose.yml up -d`) " +
                             $"or override {EndpointEnvVar}.";
            }
        }
        catch (Exception ex)
        {
            SkipReason = $"focas-mock at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
                         $"Start it or override {EndpointEnvVar}.";
        }
    }

    /// <summary>Disposes the fixture and releases any held resources.</summary>
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;

    // ---- Admin API helpers ----

    /// <summary>
    ///     Load a focas-mock profile. Accepts either the raw DLL-stem name
    ///     (<c>fwlib30i64</c>) or the OtOpcUa-style alias (<c>ThirtyOne_i</c>);
    ///     focas-mock's <c>PROFILE_ALIASES</c> resolves both.
    /// </summary>
    /// <param name="profileName">The DLL-stem name or OtOpcUa-style alias of the profile to load.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public Task<JsonElement> LoadProfileAsync(string profileName, CancellationToken ct = default) =>
        SendAdminAsync("mock_load_profile", new { profile = profileName }, ct);

    /// <summary>Deep-merge <paramref name="state"/> into the mock's current state.</summary>
    /// <param name="state">The state object to deep-merge into the mock's current state.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public Task<JsonElement> PatchStateAsync(object state, CancellationToken ct = default) =>
        SendAdminAsync("mock_patch", new { state }, ct);

    /// <summary>Reset the mock to the selected profile's default state.</summary>
    /// <param name="ct">Cancellation token for the operation.</param>
    public Task<JsonElement> ResetAsync(CancellationToken ct = default) =>
        SendAdminAsync("mock_reset", new { }, ct);

    /// <summary>Install a time-scheduled alarm raise / clear sequence.</summary>
    /// <param name="sequence">The alarm sequence events to schedule.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public Task<JsonElement> ScheduleAlarmsAsync(IEnumerable<object> sequence, CancellationToken ct = default) =>
        SendAdminAsync("mock_schedule_alarms", new { sequence }, ct);

    /// <summary>Low-level JSON round-trip. One TCP connection per call — matches
    /// how the shim talks to the mock; simpler than pooling.</summary>
    /// <param name="method">The admin method name to invoke.</param>
    /// <param name="params">The parameters object to send with the request.</param>
    /// <param name="ct">Cancellation token for the operation.</param>
    public async Task<JsonElement> SendAdminAsync(string method, object @params, CancellationToken ct = default)
    {
        using var client = new TcpClient();
        await client.ConnectAsync(Host, Port, ct).ConfigureAwait(false);
        using var stream = client.GetStream();

        var request = JsonSerializer.SerializeToUtf8Bytes(new
        {
            id = Interlocked.Increment(ref _nextId),
            method,
            @params,
        });
        await stream.WriteAsync(request, ct).ConfigureAwait(false);
        await stream.WriteAsync(new byte[] { (byte)'\n' }, ct).ConfigureAwait(false);

        var buffer = new byte[65536];
        var len = 0;
        while (len < buffer.Length)
        {
            var read = await stream.ReadAsync(buffer.AsMemory(len), ct).ConfigureAwait(false);
            if (read == 0) break;
            len += read;
            // focas-mock replies with a single newline-terminated JSON object.
            if (Array.IndexOf(buffer, (byte)'\n', 0, len) >= 0) break;
        }
        var newline = Array.IndexOf(buffer, (byte)'\n', 0, len);
        var jsonLen = newline >= 0 ? newline : len;
        var text = Encoding.UTF8.GetString(buffer, 0, jsonLen);

        using var doc = JsonDocument.Parse(text);
        var rc = doc.RootElement.GetProperty("rc").GetInt32();
        if (rc != 0)
        {
            var message = doc.RootElement.TryGetProperty("message", out var m) ? m.GetString() : "?";
            throw new InvalidOperationException($"focas-mock {method} returned rc={rc} ({message}).");
        }
        // Return the "result" subtree cloned — document is disposed on exit.
        return doc.RootElement.GetProperty("result").Clone();
    }

    private static int _nextId;

    // ---- Parsing ----

    private static (string Host, int Port) ParseEndpoint(string endpoint)
    {
        const string focasScheme = "focas://";
        var body = endpoint.StartsWith(focasScheme, StringComparison.OrdinalIgnoreCase)
            ? endpoint[focasScheme.Length..]
            : endpoint;
        var slash = body.IndexOf('/');
        if (slash >= 0) body = body[..slash];
        var colon = body.LastIndexOf(':');
        if (colon < 0) return (body, DefaultPort);
        var host = body[..colon];
        return int.TryParse(body[(colon + 1)..], out var p) ? (host, p) : (host, DefaultPort);
    }

    /// <summary>
    ///     Map either a focas-mock DLL-stem profile (<c>fwlib30i64</c>) or a
    ///     OtOpcUa-style alias (<c>ThirtyOne_i</c>) to the matching
    ///     <see cref="FocasCncSeries"/>. Keeps tests able to assert
    ///     series-gated behaviour regardless of how the profile was pinned.
    /// </summary>
    private static FocasCncSeries? ParseSeries(string? profile)
    {
        if (string.IsNullOrWhiteSpace(profile)) return null;
        var trimmed = profile.Trim();

        // Try the OtOpcUa alias set first — it's a superset of human-readable names.
        // The docker-compose profile names (thirtyone / zerod / ...) are accepted too so
        // run-focas.ps1's -Profile argument threads straight through.
        var aliasMapped = trimmed switch
        {
            "ThirtyOne_i" or "Thirty_i" or "ThirtyTwo_i"
                or "thirtyone_i" or "thirty_i" or "thirtytwo_i"
                or "thirtyone" or "thirty" or "thirtytwo"
                or "fwlib30i64" => "ThirtyOne_i",
            "Sixteen_i" or "sixteen_i" or "sixteen" or "FWLIB64" => "Sixteen_i",
            "Zero_i_D" or "Zero_i_F" or "Zero_i_MF" or "Zero_i_TF"
                or "zero_i_d" or "zero_i_f" or "zero_i_mf" or "zero_i_tf"
                or "zerod" or "zerof" or "zeromf" or "zerotf"
                or "fwlib0iD64" => "Zero_i_D",
            "PowerMotion_i" or "powermotion_i" or "powermotion"
                or "fwlib0DN64" => "PowerMotion_i",
            _ => null,
        };

        return aliasMapped is not null && Enum.TryParse<FocasCncSeries>(aliasMapped, out var parsed)
            ? parsed : null;
    }
}

[Xunit.CollectionDefinition(Name)]
public sealed class FocasSimCollection : Xunit.ICollectionFixture<FocasSimFixture>
{
    public const string Name = "FocasSim";
}
