"""Tests for FableClient."""
import os
import pytest
import respx
import httpx
from unittest.mock import patch

from fable_mcp.client import FableClient, FableAPIError, get_client, init_client, _reset_client


@pytest.fixture(autouse=True)
def reset_singleton():
    """Reset the module-level client singleton between tests."""
    _reset_client()
    yield
    _reset_client()


@pytest.fixture
def base_env(monkeypatch):
    monkeypatch.setenv("FABLE_URL", "http://fable.test")
    monkeypatch.setenv("FABLE_API_KEY", "fmcp_testkey")


class TestFableClientInit:
    def test_missing_fable_url_raises(self, monkeypatch):
        monkeypatch.delenv("FABLE_URL", raising=False)
        monkeypatch.setenv("FABLE_API_KEY", "fmcp_testkey")
        with pytest.raises(ValueError, match="FABLE_URL"):
            FableClient()

    def test_missing_api_key_raises(self, monkeypatch):
        monkeypatch.setenv("FABLE_URL", "http://fable.test")
        monkeypatch.delenv("FABLE_API_KEY", raising=False)
        with pytest.raises(ValueError, match="FABLE_API_KEY"):
            FableClient()

    def test_trailing_slash_stripped(self, base_env):
        with patch.dict(os.environ, {"FABLE_URL": "http://fable.test/"}):
            client = FableClient()
            assert client.base_url == "http://fable.test"

    def test_auth_header_set(self, base_env):
        client = FableClient()
        assert client._headers["Authorization"] == "Bearer fmcp_testkey"


class TestFableClientHTTP:
    @respx.mock
    @pytest.mark.asyncio
    async def test_get_returns_json(self, base_env):
        respx.get("http://fable.test/api/notes").mock(
            return_value=httpx.Response(200, json={"notes": []})
        )
        async with FableClient() as client:
            data = await client.get("/api/notes")
        assert data == {"notes": []}

    @respx.mock
    @pytest.mark.asyncio
    async def test_non_2xx_raises_fable_api_error(self, base_env):
        respx.get("http://fable.test/api/notes").mock(
            return_value=httpx.Response(401, json={"error": "Unauthorized"})
        )
        async with FableClient() as client:
            with pytest.raises(FableAPIError) as exc_info:
                await client.get("/api/notes")
        assert exc_info.value.status_code == 401

    @respx.mock
    @pytest.mark.asyncio
    async def test_post_sends_json(self, base_env):
        respx.post("http://fable.test/api/notes").mock(
            return_value=httpx.Response(201, json={"id": 1, "title": "Test"})
        )
        async with FableClient() as client:
            data = await client.post("/api/notes", json={"title": "Test"})
        assert data["id"] == 1

    @respx.mock
    @pytest.mark.asyncio
    async def test_patch_sends_json(self, base_env):
        respx.patch("http://fable.test/api/notes/1").mock(
            return_value=httpx.Response(200, json={"id": 1, "title": "Updated"})
        )
        async with FableClient() as client:
            data = await client.patch("/api/notes/1", json={"title": "Updated"})
        assert data["title"] == "Updated"

    @respx.mock
    @pytest.mark.asyncio
    async def test_delete_returns_none_on_204(self, base_env):
        respx.delete("http://fable.test/api/notes/1").mock(
            return_value=httpx.Response(204)
        )
        async with FableClient() as client:
            result = await client.delete("/api/notes/1")
        assert result is None

    @respx.mock
    @pytest.mark.asyncio
    async def test_fable_api_error_includes_message(self, base_env):
        respx.get("http://fable.test/api/notes").mock(
            return_value=httpx.Response(404, json={"error": "Not found"})
        )
        async with FableClient() as client:
            with pytest.raises(FableAPIError) as exc_info:
                await client.get("/api/notes")
        assert "Not found" in str(exc_info.value)
        assert exc_info.value.status_code == 404


class TestSingleton:
    def test_get_client_before_init_raises(self):
        with pytest.raises(RuntimeError, match="init_client"):
            get_client()

    def test_init_client_returns_instance(self, base_env):
        client = init_client()
        assert isinstance(client, FableClient)

    def test_get_client_after_init_returns_same(self, base_env):
        init_client()
        c1 = get_client()
        c2 = get_client()
        assert c1 is c2


class TestStreamGet:
    @respx.mock
    @pytest.mark.asyncio
    async def test_stream_get_yields_lines(self, base_env):
        sse_body = b"data: line1\n\ndata: line2\n\n"
        respx.get("http://fable.test/api/stream").mock(
            return_value=httpx.Response(200, content=sse_body)
        )
        lines = []
        async with FableClient() as client:
            async for line in client.stream_get("/api/stream"):
                lines.append(line)
        assert "data: line1" in lines
        assert "data: line2" in lines
