using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text.Json;
using Mbproxy.Options;
using Mbproxy.Proxy;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Shouldly;
using Xunit;

namespace Mbproxy.Tests.Admin;

/// <summary>
/// End-to-end test of the SignalR live feed: a real <see cref="HubConnection"/> against
/// the live Kestrel admin host. Exercises the whole push path that no other test covers —
/// <see cref="Mbproxy.Admin.StatusHub"/> group joins, the <c>MapHub</c> wiring, the
/// <see cref="Mbproxy.Admin.SignalRStatusPushSink"/>, and the broadcaster loop —
/// confirming that <c>SubscribeFleet</c> yields a <c>"fleet"</c> message and
/// <c>SubscribePlc</c> yields a <c>"plc"</c> message.
/// </summary>
[Trait("Category", "E2E")]
public sealed class HubStatusE2ETests
{
    [Fact(Timeout = 15_000)]
    public async Task SubscribeFleet_ReceivesFleetSnapshot()
    {
        int adminPort = PickFreePort();
        int proxyPort = PickFreePort();

        await using var host = BuildHost(adminPort, proxyPort);
        using var startCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        await host.Host.StartAsync(startCts.Token);
        await WaitForAdminAsync(adminPort);

        await using var connection = new HubConnectionBuilder()
            .WithUrl($"http://127.0.0.1:{adminPort}/hub/status")
            .Build();

        var fleet = new TaskCompletionSource<JsonElement>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        connection.On<JsonElement>("fleet", payload => fleet.TrySetResult(payload));

        await connection.StartAsync(TestContext.Current.CancellationToken);
        await connection.InvokeAsync("SubscribeFleet", TestContext.Current.CancellationToken);

        var snapshot = await fleet.Task.WaitAsync(
            TimeSpan.FromSeconds(8), TestContext.Current.CancellationToken);

        // The fleet payload is a StatusResponse — assert a couple of its known fields.
        snapshot.TryGetProperty("service", out _).ShouldBeTrue("fleet payload must carry 'service'");
        snapshot.TryGetProperty("plcs", out var plcs).ShouldBeTrue("fleet payload must carry 'plcs'");
        plcs.ValueKind.ShouldBe(JsonValueKind.Array);
    }

    [Fact(Timeout = 15_000)]
    public async Task SubscribePlc_ReceivesDetailSnapshot()
    {
        int adminPort = PickFreePort();
        int proxyPort = PickFreePort();

        await using var host = BuildHost(adminPort, proxyPort);
        using var startCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        await host.Host.StartAsync(startCts.Token);
        await WaitForAdminAsync(adminPort);

        await using var connection = new HubConnectionBuilder()
            .WithUrl($"http://127.0.0.1:{adminPort}/hub/status")
            .Build();

        var detail = new TaskCompletionSource<JsonElement>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        connection.On<JsonElement>("plc", payload => detail.TrySetResult(payload));

        await connection.StartAsync(TestContext.Current.CancellationToken);
        // tabId is a stable per-page-load identifier the real client generates.
        await connection.InvokeAsync("SubscribePlc", "TestPLC", "tab-e2e",
            TestContext.Current.CancellationToken);

        var snapshot = await detail.Task.WaitAsync(
            TimeSpan.FromSeconds(8), TestContext.Current.CancellationToken);

        // The detail payload is a PlcDetailResponse { plc, debug }.
        snapshot.TryGetProperty("debug", out var debug).ShouldBeTrue("detail payload must carry 'debug'");
        debug.TryGetProperty("captureArmed", out _).ShouldBeTrue("debug must carry 'captureArmed'");
        debug.TryGetProperty("tags", out var tags).ShouldBeTrue("debug must carry 'tags'");
        tags.ValueKind.ShouldBe(JsonValueKind.Array);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private static HostHandle BuildHost(int adminPort, int proxyPort)
    {
        var config = new Dictionary<string, string?>
        {
            ["Mbproxy:AdminPort"]                          = adminPort.ToString(),
            // Fast push cadence so the subscribed client sees a message promptly.
            ["Mbproxy:AdminPushIntervalMs"]                = "100",
            ["Mbproxy:Plcs:0:Name"]                        = "TestPLC",
            ["Mbproxy:Plcs:0:ListenPort"]                  = proxyPort.ToString(),
            ["Mbproxy:Plcs:0:Host"]                        = "127.0.0.1",
            ["Mbproxy:Plcs:0:Port"]                        = "502",
            ["Mbproxy:Connection:BackendConnectTimeoutMs"] = "500",
            ["Mbproxy:Connection:BackendRequestTimeoutMs"] = "500",
        };

        var builder = Host.CreateApplicationBuilder();
        builder.Configuration.AddInMemoryCollection(config);
        builder.Services.AddSerilog(
            new LoggerConfiguration().MinimumLevel.Fatal().CreateLogger(), dispose: false);
        builder.AddMbproxyOptions();
        builder.Services.AddSingleton<IPduPipeline, NoopPduPipeline>();
        builder.Services.AddSingleton<ProxyWorker>();
        builder.Services.AddHostedService(sp => sp.GetRequiredService<ProxyWorker>());
        builder.AddMbproxyAdmin();

        return new HostHandle(builder.Build());
    }

    private static async Task WaitForAdminAsync(int adminPort)
    {
        using var http = new HttpClient();
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        while (!cts.IsCancellationRequested)
        {
            try
            {
                var r = await http.GetAsync($"http://127.0.0.1:{adminPort}/status.json", cts.Token);
                if (r.StatusCode == HttpStatusCode.OK) return;
            }
            catch { }
            await Task.Delay(100, cts.Token).ConfigureAwait(false);
        }
        throw new TimeoutException($"Admin endpoint on port {adminPort} did not start in time.");
    }

    private static int PickFreePort()
    {
        var l = new TcpListener(IPAddress.Loopback, 0);
        l.Start();
        int port = ((IPEndPoint)l.LocalEndpoint).Port;
        l.Stop();
        return port;
    }

    private sealed class HostHandle(IHost host) : IAsyncDisposable
    {
        public IHost Host { get; } = host;

        public async ValueTask DisposeAsync()
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            try { await Host.StopAsync(cts.Token); } catch { }
            Host.Dispose();
        }
    }
}
