"""Unit tests for handler/interceptor.py — transparent HTTP/WebSocket/SSE proxy"""

import asyncio
import json

import aiohttp
import pytest
import pytest_asyncio
from aiohttp import web
from aiohttp.test_utils import TestClient, TestServer
from unittest.mock import MagicMock, patch

_mock_log = MagicMock()
_mock_log.get_logger = MagicMock(return_value=MagicMock())
_mock_env = MagicMock()
_mock_env.app.name = "test-app"
_mock_env.model.concurrency = 4
_mock_calc = MagicMock()

with (
    patch("gmi_ieops.handler.interceptor.log", _mock_log),
    patch("gmi_ieops.handler.interceptor.env", _mock_env),
    patch("gmi_ieops.handler.interceptor.get_socket_path", return_value="/tmp/test.sock"),
    patch("gmi_ieops.handler.interceptor.get_load_calculator", return_value=_mock_calc),
):
    from gmi_ieops.handler.interceptor import Interceptor

import gmi_ieops.handler.interceptor as _mod
_mod.log = _mock_log
_mod.get_load_calculator = lambda: _mock_calc


# --- Mock upstream ---
def build_upstream_app() -> web.Application:
    async def echo(req: web.Request) -> web.Response:
        body = await req.read()
        return web.json_response({
            "method": req.method, "path": req.path,
            "query_string": req.query_string,
            "headers": dict(req.headers),
            "body": body.decode("utf-8", errors="replace"),
        })

    async def status(req): return web.Response(text="ok", status=int(req.match_info["code"]))
    async def sse(req):
        r = web.StreamResponse(status=200); r.content_type = "text/event-stream"; r.charset = "utf-8"
        await r.prepare(req)
        for i in range(3): await r.write(f"data: event-{i}\n\n".encode())
        await r.write_eof(); return r
    async def chunked(req):
        r = web.StreamResponse(status=200); r.content_type = "application/octet-stream"
        r.enable_chunked_encoding(); await r.prepare(req)
        for i in range(3): await r.write(f"chunk-{i}".encode())
        await r.write_eof(); return r
    async def charset_utf8(req): return web.Response(text="你好世界", content_type="text/plain", charset="utf-8")
    async def charset_complex(req):
        r = web.Response(body=b"<html></html>", status=200)
        r.headers["Content-Type"] = "text/html; charset=utf-8; boundary=something"; return r
    async def large(req): return web.Response(text="x" * 100_000)
    async def slow(req): await asyncio.sleep(5); return web.Response(text="done")
    async def ws_echo(req):
        ws = web.WebSocketResponse(); await ws.prepare(req)
        async for msg in ws:
            if msg.type == aiohttp.WSMsgType.TEXT: await ws.send_str(f"echo: {msg.data}")
            elif msg.type == aiohttp.WSMsgType.BINARY: await ws.send_bytes(msg.data)
            elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR): break
        return ws

    app = web.Application()
    app.router.add_route("*", "/echo", echo)
    app.router.add_route("*", "/echo/{tail:.*}", echo)
    for path, h in [("/status/{code}", status), ("/sse", sse), ("/chunked", chunked),
                     ("/charset/utf8", charset_utf8), ("/charset/complex", charset_complex),
                     ("/large", large), ("/slow", slow), ("/ws", ws_echo)]:
        app.router.add_get(path, h)
    return app


async def make_proxy(upstream_url: str, timeout: int = 10) -> TestClient:
    ic = Interceptor(upstream=upstream_url, timeout=timeout, app_name="test", socket_path="/tmp/t.sock")
    app = web.Application()
    app.on_startup.append(ic._on_startup); app.on_shutdown.append(ic._on_shutdown)
    app.router.add_route("*", "/{path:.*}", ic._handle_request)
    client = TestClient(TestServer(app)); await client.start_server()
    return client


@pytest_asyncio.fixture
async def upstream_server():
    s = TestServer(build_upstream_app()); await s.start_server(); yield s; await s.close()

@pytest_asyncio.fixture
async def proxy(upstream_server):
    c = await make_proxy(f"http://127.0.0.1:{upstream_server.port}"); yield c; await c.close()


class TestHTTPProxy:

    @pytest.mark.asyncio
    async def test_methods(self, proxy):
        for method, path, kw in [
            ("get", "/echo", {}),
            ("post", "/echo", {"data": '{"k":"v"}', "headers": {"Content-Type": "application/json"}}),
            ("put", "/echo", {"data": b"update"}),
            ("delete", "/echo", {}),
        ]:
            resp = await getattr(proxy, method)(path, **kw)
            data = await resp.json()
            assert resp.status == 200 and data["method"] == method.upper()

    @pytest.mark.asyncio
    async def test_query_and_path(self, proxy):
        resp = await proxy.get("/echo/a/b?foo=bar&x=1")
        data = await resp.json()
        assert data["path"] == "/echo/a/b" and data["query_string"] == "foo=bar&x=1"

    @pytest.mark.asyncio
    async def test_status_codes(self, proxy):
        for code in [201, 400, 404, 500]:
            assert (await proxy.get(f"/status/{code}")).status == code


class TestHeaders:

    @pytest.mark.asyncio
    async def test_filtering(self, proxy):
        resp = await proxy.get("/echo", headers={"X-Custom": "keep", "Origin": "http://evil.com"})
        h = {k.lower(): v for k, v in (await resp.json())["headers"].items()}
        assert h.get("x-custom") == "keep" and "origin" not in h


class TestContentAndStreaming:

    @pytest.mark.asyncio
    async def test_charset(self, proxy):
        resp = await proxy.get("/charset/utf8")
        assert resp.charset == "utf-8" and await resp.text() == "你好世界"

    @pytest.mark.asyncio
    async def test_complex_content_type(self, proxy):
        resp = await proxy.get("/charset/complex")
        assert resp.status == 200 and await resp.read() == b"<html></html>"

    @pytest.mark.asyncio
    async def test_sse(self, proxy):
        body = (await (await proxy.get("/sse")).read()).decode()
        assert all(f"data: event-{i}" in body for i in range(3))

    @pytest.mark.asyncio
    async def test_chunked(self, proxy):
        body = (await (await proxy.get("/chunked")).read()).decode()
        assert all(f"chunk-{i}" in body for i in range(3))

    @pytest.mark.asyncio
    async def test_large_body(self, proxy):
        assert len(await (await proxy.get("/large")).text()) == 100_000
        big = "y" * 50_000
        assert (await (await proxy.post("/echo", data=big)).json())["body"] == big


class TestErrorHandling:

    @pytest.mark.asyncio
    async def test_502_and_504(self, upstream_server):
        c1 = await make_proxy("http://127.0.0.1:1", timeout=2)
        try:
            r = await c1.get("/any"); assert r.status == 502 and "Bad Gateway" in await r.text()
        finally:
            await c1.close()

        c2 = await make_proxy(f"http://127.0.0.1:{upstream_server.port}", timeout=1)
        try:
            r = await c2.get("/slow"); assert r.status == 504 and "Gateway Timeout" in await r.text()
        finally:
            await c2.close()


class TestWebSocket:

    @pytest.mark.asyncio
    async def test_text_and_binary(self, proxy):
        async with proxy.ws_connect("/ws") as ws:
            await ws.send_str("hello")
            assert (await ws.receive()).data == "echo: hello"
            for i in range(3):
                await ws.send_str(f"m{i}")
                assert (await ws.receive()).data == f"echo: m{i}"

        async with proxy.ws_connect("/ws") as ws:
            await ws.send_bytes(b"\x00\x01")
            msg = await ws.receive()
            assert msg.type == aiohttp.WSMsgType.BINARY and msg.data == b"\x00\x01"


class TestLoadTracking:

    @pytest.mark.asyncio
    async def test_called_on_success_and_error(self, proxy):
        _mock_calc.reset_mock()
        await proxy.get("/echo")
        _mock_calc.on_request_start.assert_called(); _mock_calc.on_request_end.assert_called()

        c = await make_proxy("http://127.0.0.1:1", timeout=1)
        try:
            _mock_calc.reset_mock(); await c.get("/fail")
            _mock_calc.on_request_start.assert_called_once(); _mock_calc.on_request_end.assert_called_once()
        finally:
            await c.close()
