using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TrajectoryPlanningVisualization;

namespace TrajectoryPlanningVisualizationVerificationHost;

internal static class ServerChecks
{
    public static void Run()
    {
        RejectsUnauthorizedAndMalformedRequests();
        LimitsSseClientsAndKeepsPublishNonBlocking();
        CollectsHistoryWithoutClientsAndStopsCleanly();
    }

    private static void RejectsUnauthorizedAndMalformedRequests()
    {
        int port;
        using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 }))
        {
            session.Publish(DynamicSnapshot(0));
            PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot());
            Verification.Equal(info.Token, session.Start(StaticSnapshot()).Token, "repeated start is idempotent");
            port = info.Uri.Port;
            Verification.Equal("127.0.0.1", info.Uri.Host, "server binds loopback");
            Verification.Equal(64, info.Token.Length, "session token is 256 bits of hex");
            Verification.Equal(403, SendStatus(port, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"),
                "missing token is forbidden");
            Verification.Equal(404, SendStatus(port, Get("/missing?token=" + info.Token)),
                "unknown authorized route is not found");
            Verification.Equal(405, SendStatus(port, "POST /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\n\r\n"),
                "non-GET request is rejected");
            Verification.Equal(400, SendStatus(port, "GET /api/bootstrap HTTP/1.0\r\nHost: localhost\r\n\r\n"),
                "non-HTTP/1.1 request is malformed");
            Verification.Equal(400, SendStatus(port, "GET /api/bootstrap?token=" + info.Token + " HTTP/1.1\r\nHost: localhost\r\nX-Large: " + new string('a', 16 * 1024) + "\r\n\r\n"),
                "oversized request header is rejected");

            HttpResponse bootstrap = SendResponse(port, Get("/api/bootstrap?token=" + info.Token));
            Verification.Equal(200, bootstrap.Status, "authorized bootstrap succeeds");
            Verification.Equal(bootstrap.ContentLength, Encoding.UTF8.GetByteCount(bootstrap.Body),
                "normal response has byte-accurate content length");
            Verification.True(bootstrap.Body.Contains("sessionNameChinese"), "bootstrap contains static snapshot");
            session.Stop();
            session.Stop();
            session.Publish(DynamicSnapshot(99));
        }

        Verification.True(CanBindReleasedPort(port), "stop releases loopback port");
    }

    private static void LimitsSseClientsAndKeepsPublishNonBlocking()
    {
        using var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 100 });
        PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot());
        using TcpClient first = OpenSseClient(info);
        using TcpClient second = OpenSseClient(info);
        Verification.Equal(503, SendStatus(info.Uri.Port, Get("/api/events?token=" + info.Token)),
            "third SSE client is rejected");

        Task publisher = Task.Run(() =>
        {
            for (int i = 1; i <= 10000; i++)
            {
                session.Publish(DynamicSnapshot(i));
            }
        });
        Verification.True(publisher.Wait(TimeSpan.FromSeconds(1)), "slow SSE client does not block Publish");
        session.Stop();
    }

    private static void CollectsHistoryWithoutClientsAndStopsCleanly()
    {
        int port;
        using (var session = new PlanningVisualizationSession(new PlanningVisualizationOptions { Port = 0, RefreshRateHz = 20 }))
        {
            PlanningVisualizationSessionInfo info = session.Start(StaticSnapshot());
            port = info.Uri.Port;
            for (int i = 1; i <= 3; i++)
            {
                session.Publish(DynamicSnapshot(i));
                Thread.Sleep(120);
            }

            using TcpClient client = OpenSseClient(info);
            string frame = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(2));
            Verification.True(frame.Contains("cycleVersion\":1") && frame.Contains("cycleVersion\":2") &&
                frame.Contains("cycleVersion\":3"), "dispatcher collects bounded history without browsers");

            Stopwatch stopWatch = Stopwatch.StartNew();
            session.Stop();
            Verification.True(stopWatch.Elapsed <= TimeSpan.FromSeconds(1), "normal stop is bounded");
            string terminal = ReadUntil(client.GetStream(), "\n\n", TimeSpan.FromSeconds(1));
            Verification.True(terminal.Contains("event: end") && terminal.Contains("会话已结束"),
                "normal stop offers a terminal SSE event");
        }

        Verification.True(CanBindReleasedPort(port), "port can be rebound after normal stop");
    }

    private static TcpClient OpenSseClient(PlanningVisualizationSessionInfo info)
    {
        var client = new TcpClient();
        client.Connect(IPAddress.Loopback, info.Uri.Port);
        byte[] request = Encoding.ASCII.GetBytes(Get("/api/events?token=" + info.Token));
        NetworkStream stream = client.GetStream();
        stream.Write(request, 0, request.Length);
        string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2));
        Verification.True(headers.StartsWith("HTTP/1.1 200", StringComparison.Ordinal), "authorized SSE succeeds");
        return client;
    }

    private static int SendStatus(int port, string request)
    {
        return SendResponse(port, request).Status;
    }

    private static HttpResponse SendResponse(int port, string request)
    {
        using var client = new TcpClient();
        client.Connect(IPAddress.Loopback, port);
        NetworkStream stream = client.GetStream();
        byte[] bytes = Encoding.ASCII.GetBytes(request);
        stream.Write(bytes, 0, bytes.Length);
        string headers = ReadUntil(stream, "\r\n\r\n", TimeSpan.FromSeconds(2));
        string[] lines = headers.Split(new[] { "\r\n" }, StringSplitOptions.None);
        string[] statusParts = lines[0].Split(' ');
        int status = int.Parse(statusParts[1]);
        int contentLength = 0;
        foreach (string line in lines)
        {
            if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase))
            {
                contentLength = int.Parse(line.Substring("Content-Length:".Length).Trim());
            }
        }

        byte[] body = ReadExactly(stream, contentLength, TimeSpan.FromSeconds(2));
        return new HttpResponse(status, contentLength, Encoding.UTF8.GetString(body));
    }

    private static string Get(string target)
    {
        return "GET " + target + " HTTP/1.1\r\nHost: localhost\r\n\r\n";
    }

    private static string ReadUntil(NetworkStream stream, string delimiter, TimeSpan timeout)
    {
        var bytes = new List<byte>();
        byte[] delimiterBytes = Encoding.ASCII.GetBytes(delimiter);
        stream.ReadTimeout = (int)timeout.TotalMilliseconds;
        while (true)
        {
            int next = stream.ReadByte();
            if (next < 0)
            {
                throw new InvalidOperationException("connection ended before expected response delimiter");
            }

            bytes.Add((byte)next);
            if (bytes.Count >= delimiterBytes.Length)
            {
                bool found = true;
                for (int index = 0; index < delimiterBytes.Length; index++)
                {
                    if (bytes[bytes.Count - delimiterBytes.Length + index] != delimiterBytes[index])
                    {
                        found = false;
                        break;
                    }
                }

                if (found)
                {
                    return Encoding.UTF8.GetString(bytes.ToArray());
                }
            }
        }
    }

    private static byte[] ReadExactly(NetworkStream stream, int count, TimeSpan timeout)
    {
        byte[] result = new byte[count];
        stream.ReadTimeout = (int)timeout.TotalMilliseconds;
        int offset = 0;
        while (offset < count)
        {
            int read = stream.Read(result, offset, count - offset);
            if (read == 0)
            {
                throw new InvalidOperationException("connection ended before response body");
            }

            offset += read;
        }

        return result;
    }

    private static bool CanBindReleasedPort(int port)
    {
        try
        {
            using var listener = new TcpListener(IPAddress.Loopback, port);
            listener.Start();
            return true;
        }
        catch (SocketException)
        {
            return false;
        }
    }

    private static PlanningVisualizationStaticSnapshot StaticSnapshot()
    {
        return new PlanningVisualizationStaticSnapshot(
            "验证会话",
            new VisualizationBounds(0d, 1d, 0d, 1d),
            null,
            Array.Empty<VisualizationPolyline>(),
            Array.Empty<VisualizationMarker>(),
            Array.Empty<VisualizationDirectionSegment>(),
            Array.Empty<VisualizationConfigurationGroup>());
    }

    private static PlanningVisualizationDynamicSnapshot DynamicSnapshot(long sequence)
    {
        return new PlanningVisualizationDynamicSnapshot(
            sequence,
            DateTimeOffset.UtcNow,
            "运行中",
            0,
            "forward",
            new VisualizationPose(0d, 0d, 0d),
            Array.Empty<VisualizationPolyline>(),
            Array.Empty<VisualizationMarker>(),
            Array.Empty<VisualizationChart>(),
            Array.Empty<VisualizationValue>(),
            new VisualizationCycleSummary(sequence, DateTimeOffset.UtcNow, "成功", true, 1d, 0, "forward",
                "rolling", "none", null, null, ""));
    }

    private sealed class HttpResponse
    {
        public HttpResponse(int status, int contentLength, string body)
        {
            Status = status;
            ContentLength = contentLength;
            Body = body;
        }

        public int Status { get; }
        public int ContentLength { get; }
        public string Body { get; }
    }
}
