import asyncio
import pytest
from unittest.mock import patch
from asgi_autoreload.middleware import AutoreloadMiddleware


# ── helpers ──────────────────────────────────────────────────────────────────

def html_app(content: bytes = b"<html><body>hello</body></html>"):
    async def app(scope, receive, send):
        await send({
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/html; charset=utf-8"],
                        [b"content-length", str(len(content)).encode()]],
        })
        await send({"type": "http.response.body", "body": content, "more_body": False})
    return app


def plain_app(content: bytes = b"hello"):
    async def app(scope, receive, send):
        await send({
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/plain"],
                        [b"content-length", str(len(content)).encode()]],
        })
        await send({"type": "http.response.body", "body": content, "more_body": False})
    return app


async def http_scope(path: str = "/") -> dict:
    return {"type": "http", "path": path, "query_string": b""}


async def fake_receive():
    return {"type": "http.disconnect"}


async def collect_response(middleware, scope):
    messages = []
    async def send(msg):
        messages.append(msg)
    await middleware(scope, fake_receive, send)
    return messages


# ── HTML injection ────────────────────────────────────────────────────────────

async def test_injects_script_before_body_close():
    mw = AutoreloadMiddleware(html_app())
    scope = await http_scope()
    msgs = await collect_response(mw, scope)
    body = msgs[-1]["body"]
    assert b"EventSource" in body
    assert body.endswith(b"</body></html>")


async def test_no_injection_for_plain_text():
    mw = AutoreloadMiddleware(plain_app())
    scope = await http_scope()
    msgs = await collect_response(mw, scope)
    body = msgs[-1]["body"]
    assert b"EventSource" not in body


async def test_content_length_updated_after_injection():
    mw = AutoreloadMiddleware(html_app())
    scope = await http_scope()
    msgs = await collect_response(mw, scope)
    headers = dict(msgs[0]["headers"])
    assert int(headers[b"content-length"]) == len(msgs[-1]["body"])


async def test_no_injection_when_content_encoding_present():
    async def compressed_app(scope, receive, send):
        await send({
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/html"],
                        [b"content-encoding", b"gzip"]],
        })
        await send({"type": "http.response.body", "body": b"compressed", "more_body": False})

    mw = AutoreloadMiddleware(compressed_app)
    scope = await http_scope()
    msgs = await collect_response(mw, scope)
    assert b"EventSource" not in msgs[-1]["body"]


async def test_appends_script_when_no_body_tag():
    content = b"<html>no body tag</html>"
    mw = AutoreloadMiddleware(html_app(content))
    scope = await http_scope()
    msgs = await collect_response(mw, scope)
    body = msgs[-1]["body"]
    assert body.endswith(b"</script>")
    assert b"EventSource" in body


# ── SSE endpoint ──────────────────────────────────────────────────────────────

async def test_sse_endpoint_returns_200():
    mw = AutoreloadMiddleware(plain_app())
    scope = {"type": "http", "path": "/_autoreload", "query_string": b""}

    messages = []
    call_count = 0

    async def recv():
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            await asyncio.sleep(0.01)
        return {"type": "http.disconnect"}

    async def send(msg):
        messages.append(msg)

    await mw(scope, recv, send)
    assert messages[0]["status"] == 200
    assert messages[0]["headers"][0][1] == b"text/event-stream"


async def test_sse_delivers_broadcast():
    mw = AutoreloadMiddleware(plain_app())
    scope = {"type": "http", "path": "/_autoreload", "query_string": b""}

    messages = []
    disconnected = asyncio.Event()

    async def recv():
        await disconnected.wait()
        return {"type": "http.disconnect"}

    async def send(msg):
        messages.append(msg)
        if msg.get("type") == "http.response.body" and msg.get("more_body"):
            disconnected.set()

    task = asyncio.ensure_future(mw(scope, recv, send))
    await asyncio.sleep(0.01)
    await mw._broadcaster.broadcast("reload")
    await task

    bodies = [m["body"] for m in messages if m.get("type") == "http.response.body" and m.get("body")]
    assert any(b"reload" in b for b in bodies)
