import asyncio
import os
import stat
import tempfile
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, patch
from urllib.parse import parse_qs, urlparse

from openalmanac_mcp import server


class DummyResponse:
    def __init__(self, payload: dict | None = None, status_code: int = 200) -> None:
        self._payload = payload or {}
        self.status_code = status_code

    def raise_for_status(self) -> None:
        return None

    def json(self) -> dict:
        return self._payload


class ServerTests(unittest.IsolatedAsyncioTestCase):
    async def test_request_with_auth_sends_bearer_token(self) -> None:
        captured_headers: dict[str, str] = {}

        class FakeAsyncClient:
            def __init__(self, *args, **kwargs) -> None:
                pass

            async def __aenter__(self):
                return self

            async def __aexit__(self, exc_type, exc, tb) -> None:
                return None

            async def request(self, method, path, headers=None, **kwargs):
                captured_headers.update(headers or {})
                return DummyResponse()

        with patch.dict(
            os.environ,
            {"OPENALMANAC_API_KEY": "oa_test_key"},
            clear=False,
        ):
            with patch.object(server.httpx, "AsyncClient", FakeAsyncClient):
                await server._request("GET", "/api/research/search", auth=True)

        self.assertEqual(
            captured_headers["Authorization"],
            "Bearer oa_test_key",
        )
        self.assertNotIn("X-User-Token", captured_headers)

    async def test_request_without_auth_sends_no_headers(self) -> None:
        captured_headers: dict[str, str] = {}

        class FakeAsyncClient:
            def __init__(self, *args, **kwargs) -> None:
                pass

            async def __aenter__(self):
                return self

            async def __aexit__(self, exc_type, exc, tb) -> None:
                return None

            async def request(self, method, path, headers=None, **kwargs):
                captured_headers.update(headers or {})
                return DummyResponse()

        with patch.dict(os.environ, {}, clear=False):
            with patch.object(server.httpx, "AsyncClient", FakeAsyncClient):
                await server._request("GET", "/api/search", auth=False)

        self.assertNotIn("Authorization", captured_headers)

    async def test_login_saves_api_key_after_valid_callback(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            api_key_path = Path(temp_dir) / "api_key"

            async def send_callback(connect_url: str) -> None:
                parsed = urlparse(connect_url)
                port = int(parse_qs(parsed.query)["callback_port"][0])
                await asyncio.sleep(0.05)
                reader, writer = await asyncio.open_connection("127.0.0.1", port)
                writer.write(
                    b"GET /callback?token=oa_saved_key HTTP/1.1\r\n"
                    b"Host: 127.0.0.1\r\n"
                    b"\r\n"
                )
                await writer.drain()
                await reader.read()
                writer.close()
                await writer.wait_closed()

            def fake_open(connect_url: str) -> bool:
                asyncio.get_running_loop().create_task(send_callback(connect_url))
                return True

            with patch.object(server, "API_KEY_PATH", api_key_path):
                with patch.object(server, "_get_api_key", return_value=None):
                    with patch.object(server.webbrowser, "open", side_effect=fake_open):
                        message = await server.login()
                    self.assertIn("Logged in", message)
                    self.assertEqual(api_key_path.read_text(), "oa_saved_key")
                    self.assertEqual(
                        stat.S_IMODE(api_key_path.stat().st_mode),
                        0o600,
                    )

    async def test_login_ignores_invalid_callback_before_accepting_valid_one(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            api_key_path = Path(temp_dir) / "api_key"

            async def send_request(port: int, target: str) -> None:
                reader, writer = await asyncio.open_connection("127.0.0.1", port)
                writer.write(
                    f"GET {target} HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n".encode("utf-8")
                )
                await writer.drain()
                await reader.read()
                writer.close()
                await writer.wait_closed()

            async def send_callbacks(connect_url: str) -> None:
                parsed = urlparse(connect_url)
                port = int(parse_qs(parsed.query)["callback_port"][0])
                await asyncio.sleep(0.05)
                await send_request(port, "/wrong-path?token=oa_nope")
                await asyncio.sleep(0.05)
                await send_request(port, "/callback?token=oa_final_key")

            def fake_open(connect_url: str) -> bool:
                asyncio.get_running_loop().create_task(send_callbacks(connect_url))
                return True

            with patch.object(server, "API_KEY_PATH", api_key_path):
                with patch.object(server, "_get_api_key", return_value=None):
                    with patch.object(server.webbrowser, "open", side_effect=fake_open):
                        message = await server.login()
                    self.assertIn("Logged in", message)
                    self.assertEqual(api_key_path.read_text(), "oa_final_key")

    async def test_login_timeout_returns_manual_url(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            api_key_path = Path(temp_dir) / "api_key"

            with patch.object(server, "API_KEY_PATH", api_key_path):
                with patch.object(server, "_get_api_key", return_value=None):
                    with patch.object(server.webbrowser, "open", return_value=False):
                        with patch.object(
                            server.asyncio,
                            "wait_for",
                            new=AsyncMock(side_effect=TimeoutError),
                        ):
                            with self.assertRaisesRegex(ValueError, "Open this URL manually"):
                                await server.login()

    async def test_login_returns_early_if_key_already_valid(self) -> None:
        class FakeAsyncClient:
            def __init__(self, *args, **kwargs) -> None:
                pass

            async def __aenter__(self):
                return self

            async def __aexit__(self, exc_type, exc, tb) -> None:
                return None

            async def get(self, path, headers=None, **kwargs):
                return DummyResponse({"name": "my-agent"}, status_code=200)

        with patch.object(server, "_get_api_key", return_value="oa_existing_key"):
            with patch.object(server.httpx, "AsyncClient", FakeAsyncClient):
                message = await server.login()

        self.assertIn("Already logged in", message)
        self.assertIn("my-agent", message)

    async def test_logout_removes_saved_file_and_warns_on_env_override(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            api_key_path = Path(temp_dir) / "api_key"
            api_key_path.write_text("oa_saved_key")

            with patch.object(server, "API_KEY_PATH", api_key_path):
                with patch.dict(
                    os.environ,
                    {"OPENALMANAC_API_KEY": "oa_env_key"},
                    clear=False,
                ):
                    message = await server.logout()
                self.assertFalse(api_key_path.exists())
                self.assertIn("Logged out", message)
                self.assertIn("OPENALMANAC_API_KEY is set", message)


if __name__ == "__main__":
    unittest.main()
