using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using FluentAssertions;
using DragonISO.App.Services;
using DragonISO.App.Tests.Fakes;

namespace DragonISO.App.Tests.Services;

// End-to-end-ish smoke tests for ControlSurfaceServer. Each test boots
// the server on an OS-assigned free port (127.0.0.1 only â€” no urlacl
// required), makes a real HTTP request via HttpClient, and asserts
// against the response. The tests share a StubIsoController and a
// null view-model â€” endpoints that need a UI dispatcher degrade
// gracefully (return empty arrays) which is enough to verify the
// route table.
//
// We don't exercise the WebSocket path here â€” ClientWebSocket adds
// non-trivial timing complexity and the upgrade is verified by the
// 426/101 status arc of `/ws` on a non-WS GET (we hit it and confirm
// the server doesn't 500).
public sealed class ControlSurfaceServerTests
{
    private static int PickFreePort()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        try { return ((IPEndPoint)listener.LocalEndpoint).Port; }
        finally { listener.Stop(); }
    }

    private static async Task<(ControlSurfaceServer Server, HttpClient Client, int Port)> BootAsync()
    {
        var controller = new StubIsoController();
        var server = new ControlSurfaceServer(controller, () => null, logger: null);
        var port = PickFreePort();
        server.Start(port, bindToLan: false);
        // HttpListener accepts on a background task; give it a beat so
        // the first request doesn't race the bind.
        await Task.Delay(50);
        var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}") };
        return (server, client, port);
    }

    [Fact]
    public async Task GetRoot_Returns200_WithServerInfoBody()
    {
        var (server, client, _) = await BootAsync();
        try
        {
            var res = await client.GetAsync("/");

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            var body = await res.Content.ReadAsStringAsync();
            body.Should().Contain("\"product\":\"Dragon-ISO\"");
            body.Should().Contain("\"endpoints\"");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }

    [Fact]
    public async Task GetUnknownPath_Returns200_WithErrorBody()
    {
        // Quirk: the route table's catch-all arm returns NotFound() (an
        // object {error:"not found"}) rather than null, so the response
        // pipeline writes 200 OK with that body instead of branching to
        // 404. The body is the disambiguator, matching the rest of the
        // surface's "200 + {ok:false,error:â€¦}" convention. Pinning this
        // so a deliberate move to a true 404 is a conscious decision,
        // not an accident.
        var (server, client, _) = await BootAsync();
        try
        {
            var res = await client.GetAsync("/this-route-does-not-exist");

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            var body = await res.Content.ReadAsStringAsync();
            body.Should().Contain("\"error\":\"not found\"");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }

    [Fact]
    public async Task GetParticipants_Returns200_WithEmptyListWhenNoViewModel()
    {
        // No dispatcher / no view-model in tests â€” the endpoint should
        // gracefully return participants=[] rather than throwing.
        var (server, client, _) = await BootAsync();
        try
        {
            var res = await client.GetAsync("/participants");

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            var body = await res.Content.ReadAsStringAsync();
            body.Should().Contain("\"participants\":[]");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }

    [Fact]
    public async Task PostPresetsRefreshDiscovery_HitsControllerAndReturnsOk()
    {
        var controller = new StubIsoController();
        var server = new ControlSurfaceServer(controller, () => null, logger: null);
        var port = PickFreePort();
        server.Start(port);
        await Task.Delay(50);
        using var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}") };
        try
        {
            var res = await client.PostAsync("/presets/refresh-discovery", content: null);

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            controller.RefreshDiscoveryCalled.Should().BeTrue();
        }
        finally
        {
            server.Stop();
        }
    }

    [Fact]
    public async Task PostPresetApply_MissingPreset_RespondsWithOkFalseAndPresetNotFound()
    {
        // Preset name that demonstrably doesn't exist on disk â†’ endpoint
        // returns 200 with {"ok":false,"error":"preset not found",...}.
        // We don't 404 on missing presets because the operator may have
        // typed the wrong name; clearer payload is friendlier.
        var (server, client, _) = await BootAsync();
        try
        {
            var res = await client.PostAsync(
                "/presets/__nonexistent_preset_for_test__/apply",
                content: null);

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            var body = await res.Content.ReadAsStringAsync();
            body.Should().Contain("\"ok\":false");
            body.Should().Contain("\"error\":\"preset not found\"");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }

    [Fact]
    public async Task GetUi_Returns200_WithEmbeddedHtml()
    {
        var (server, client, _) = await BootAsync();
        try
        {
            var res = await client.GetAsync("/ui");

            res.StatusCode.Should().Be(HttpStatusCode.OK);
            res.Content.Headers.ContentType?.MediaType.Should().Be("text/html");
            var body = await res.Content.ReadAsStringAsync();
            body.Should().Contain("<html", "the response should be a real HTML document");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }

    [Fact]
    public async Task OptionsRequest_Returns204_WithCorsHeaders()
    {
        // Companion / browser-based controllers preflight POSTs; the
        // server must answer 204 with the allow-origin/allow-methods
        // headers or the actual call gets blocked by CORS.
        var (server, client, _) = await BootAsync();
        try
        {
            var req = new HttpRequestMessage(HttpMethod.Options, "/participants");
            var res = await client.SendAsync(req);

            res.StatusCode.Should().Be(HttpStatusCode.NoContent);
            res.Headers.GetValues("Access-Control-Allow-Origin").Should().Contain("*");
        }
        finally
        {
            server.Stop();
            client.Dispose();
        }
    }
}
