import importlib

import pytest


http_client = importlib.import_module("gjalla_precommit.protocol.http_client")


class _FakeResponse:
    def __init__(self, lines=None, status_code=200):
        self.status_code = status_code
        self._lines = lines or []

    async def aiter_lines(self):
        for line in self._lines:
            yield line


class _FakeStream:
    def __init__(self, response):
        self._response = response

    async def __aenter__(self):
        return self._response

    async def __aexit__(self, exc_type, exc, tb):
        return False


class _FakeAsyncClient:
    def __init__(self, captured, response):
        self._captured = captured
        self._response = response

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    def stream(self, method, url, json=None, headers=None, timeout=None):
        self._captured["method"] = method
        self._captured["url"] = url
        self._captured["json"] = json
        self._captured["headers"] = headers
        self._captured["timeout"] = timeout
        return _FakeStream(self._response)

    async def aclose(self):
        return None


class _DummySpinner:
    def __init__(self):
        self.updates = []

    def update(self, message: str) -> None:
        self.updates.append(message)


class _OutputRecorder:
    def __init__(self):
        self.warnings = []
        self.errors = []

    def warning(self, message: str) -> None:
        self.warnings.append(message)

    def error(self, message: str) -> None:
        self.errors.append(message)


@pytest.mark.asyncio
async def test_sse_client_connects(monkeypatch):
    captured = {}
    response = _FakeResponse(status_code=200)

    def _fake_client(*args, **kwargs):
        return _FakeAsyncClient(captured, response)

    monkeypatch.setattr(http_client, "httpx", importlib.import_module("httpx"))
    monkeypatch.setattr(http_client.httpx, "AsyncClient", _fake_client)

    client = http_client.SSEClient("https://api.gjalla.io/api/analyze/commit", api_key="gj_test")
    payload = {"diff": "", "context_hash": "abc123", "branch": "main", "message": "Test"}
    result = await client.connect(payload)

    assert result.status_code == 200
    assert captured["method"] == "POST"
    assert captured["url"] == "https://api.gjalla.io/api/analyze/commit"
    assert captured["json"] == payload
    assert captured["headers"]["Accept"] == "text/event-stream"
    assert captured["headers"]["x-api-key"] == "gj_test"


def test_sse_client_parses_events():
    lines = [
        "event: status",
        "data: {\"message\": \"Starting\"}",
        "",
        "event: receipt",
        "data: {\"receipt_id\": \"gj:abc123\"}",
        "",
    ]
    events = list(http_client.parse_sse_events(lines))
    assert events[0].event == "status"
    assert events[0].data["message"] == "Starting"
    assert events[1].event == "receipt"
    assert events[1].data["receipt_id"] == "gj:abc123"


def test_sse_client_handles_status_event():
    spinner = _DummySpinner()
    state = {"violations": [], "receipt": None}
    event = http_client.SSEEvent(event="status", data={"message": "Parsing diff"})

    http_client.handle_sse_event(event, state, spinner=spinner)

    assert spinner.updates[-1] == "Parsing diff"


def test_sse_client_handles_warning_event():
    spinner = _DummySpinner()
    output = _OutputRecorder()
    state = {"violations": [], "receipt": None}
    event = http_client.SSEEvent(event="warning", data={"message": "Consider adding tests"})

    http_client.handle_sse_event(event, state, spinner=spinner, output=output)

    assert output.warnings == ["Consider adding tests"]


def test_sse_client_handles_violation_event():
    spinner = _DummySpinner()
    state = {"violations": [], "receipt": None}
    event = http_client.SSEEvent(event="violation", data={"rule_id": "rule-1", "message": "Missing tests"})

    http_client.handle_sse_event(event, state, spinner=spinner)

    assert state["violations"] == [event.data]


def test_sse_client_handles_receipt_event():
    spinner = _DummySpinner()
    state = {"violations": [], "receipt": None}
    event = http_client.SSEEvent(event="receipt", data={"receipt_id": "gj:abc123"})

    http_client.handle_sse_event(event, state, spinner=spinner)

    assert state["receipt"] == {"receipt_id": "gj:abc123"}


def test_sse_client_handles_context_stale():
    spinner = _DummySpinner()
    state = {"violations": [], "receipt": None}
    refreshed = {"called": False}

    def _refresh():
        refreshed["called"] = True

    event = http_client.SSEEvent(
        event="context_stale",
        data={"current_hash": "old", "expected_hash": "new"},
    )

    http_client.handle_sse_event(event, state, spinner=spinner, refresh_context=_refresh)

    assert refreshed["called"]


def test_sse_client_timeout():
    assert http_client.should_timeout(elapsed_seconds=61, timeout_seconds=60)


def test_sse_client_reconnect_on_disconnect():
    assert http_client.get_reconnect_delay(1) == 0
    assert http_client.get_reconnect_delay(2) == 1
    assert http_client.get_reconnect_delay(3) == 2
    assert http_client.get_reconnect_delay(4) == 5
    with pytest.raises(RuntimeError, match="Cloud unreachable"):
        http_client.get_reconnect_delay(5)
