from __future__ import annotations

import asyncio
import ssl
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from proto_socket.communicator import type_name_of
from proto_socket.packets.message_common_pb2 import TestData
from proto_socket.ws_server import WsServer

HOST = "127.0.0.1"
WS_PORT = 29698
WSS_PORT = 29699
WS_PATH = "/"
PROCESS_TIMEOUT = 60.0
SERVER_OBSERVATION_WINDOW = 0.5
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"


def parser_map():
    return {type_name_of(TestData): TestData.FromString}


async def main() -> None:
    print(f"INFO typeName python={type_name_of(TestData)}")
    try:
        await run_web_suites()
    except Exception as exc:
        print(f"FAIL crosstest error={exc}", file=sys.stderr)
        raise SystemExit(1) from exc
    print("PASS scenario=ws-send-push")
    print("PASS scenario=ws-request-response")
    print("PASS scenario=wss-send-push")
    print("PASS scenario=wss-request-response")
    print("PASS all python-server/dart-web-client crosstests passed")


async def run_web_suites() -> None:
    loop = asyncio.get_running_loop()
    ws_received = loop.create_future()
    wss_received = loop.create_future()
    ws_server = make_web_server(WS_PORT, None, ws_received)
    wss_server = make_web_server(WSS_PORT, make_server_ssl_context(), wss_received)

    await ws_server.start()
    await wss_server.start()
    try:
        await run_dart_browser_test("test/browser_ws_python_test.dart")
        await expect_received("WS", ws_received)
        await expect_received("WSS", wss_received)
    finally:
        await wss_server.stop()
        await ws_server.stop()


def make_web_server(
    port: int,
    ssl_context: ssl.SSLContext | None,
    received: asyncio.Future[bool],
) -> WsServer:
    server = WsServer(
        HOST,
        port,
        WS_PATH,
        interval_sec=0,
        wait_sec=0,
        parser_map=parser_map(),
        ssl_context=ssl_context,
    )

    def on_connected(client):
        def on_request(req):
            print(f"SERVER_RECEIVED index={req.index} message={req.message}")
            if req.index == 101 and req.message == "fire from dart web client":
                if not received.done():
                    received.set_result(True)
                asyncio.create_task(
                    client.communicator.send(
                        TestData(index=200, message="push from python server")
                    )
                )
            return TestData(index=req.index * 2, message="echo: " + req.message)

        client.communicator.add_request_listener(type_name_of(TestData), on_request)

    server.on_client_connected = on_connected
    return server


def make_server_ssl_context() -> ssl.SSLContext:
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(CERT_PATH, KEY_PATH)
    return ctx


async def expect_received(transport: str, received: asyncio.Future[bool]) -> None:
    try:
        ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
    except TimeoutError as exc:
        raise RuntimeError(
            f"{transport} web send-push server did not receive expected data"
        ) from exc
    if not ok:
        raise RuntimeError(f"{transport} web send-push server received unexpected data")


async def run_dart_browser_test(test_path: str) -> None:
    dart_dir = dart_package_dir()
    process = await asyncio.create_subprocess_exec(
        "dart",
        "test",
        "-p",
        "chrome",
        test_path,
        cwd=dart_dir,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    try:
        stdout, stderr = await asyncio.wait_for(process.communicate(), PROCESS_TIMEOUT)
    except TimeoutError as exc:
        process.kill()
        await process.wait()
        raise TimeoutError("dart browser test timed out") from exc
    for line in stdout.decode().splitlines():
        print(line)
    for line in stderr.decode().splitlines():
        print(line, file=sys.stderr)
    if process.returncode != 0:
        raise RuntimeError(f"dart browser test failed exitCode={process.returncode}")


def dart_package_dir() -> Path:
    repo_root = Path(__file__).resolve().parents[2]
    candidate = repo_root / "dart"
    if (candidate / "pubspec.yaml").is_file():
        return candidate
    raise RuntimeError(f"cannot resolve dart package directory from {candidate}")


if __name__ == "__main__":
    asyncio.run(main())
