"""Tests for NormaAI sync and async clients."""

import asyncio
import json
import unittest
from unittest.mock import patch, MagicMock
from io import BytesIO
from http.client import HTTPResponse

from normaai.client import NormaAI, NormaAIError, DEFAULT_API_URL
from normaai.async_client import AsyncNormaAI


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _mock_response(data: dict, status: int = 200) -> MagicMock:
    """Create a mock urllib response with JSON body."""
    body = json.dumps(data).encode("utf-8")
    resp = MagicMock()
    resp.read.return_value = body
    resp.status = status
    resp.__enter__ = lambda s: s
    resp.__exit__ = MagicMock(return_value=False)
    return resp


def _mock_http_error(data: dict, status: int = 400) -> "urllib.error.HTTPError":
    """Create a mock HTTPError."""
    import urllib.error
    body = json.dumps(data).encode("utf-8")
    err = urllib.error.HTTPError(
        url="https://example.com",
        code=status,
        msg="Bad Request",
        hdrs={},  # type: ignore[arg-type]
        fp=BytesIO(body),
    )
    return err


# ---------------------------------------------------------------------------
# Sync client tests
# ---------------------------------------------------------------------------

class TestNormaAIInit(unittest.TestCase):
    """Test client instantiation and configuration."""

    def test_requires_api_key(self) -> None:
        with self.assertRaises(ValueError):
            NormaAI(api_key="")

    def test_default_base_url(self) -> None:
        client = NormaAI(api_key="nai_test")
        self.assertEqual(client._base_url, DEFAULT_API_URL)

    def test_custom_base_url(self) -> None:
        client = NormaAI(api_key="nai_test", base_url="https://custom.api/v1")
        self.assertEqual(client._base_url, "https://custom.api/v1")

    def test_stores_api_key(self) -> None:
        client = NormaAI(api_key="nai_abc123")
        self.assertEqual(client._api_key, "nai_abc123")


class TestNormaAIUrlBuilding(unittest.TestCase):
    """Test that methods build correct URLs."""

    @patch("urllib.request.urlopen")
    def test_query_url(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response({"answer": "ok"})
        client = NormaAI(api_key="nai_test", base_url="https://api.test")
        client.query("Test?", "lavoro")

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.full_url, "https://api.test/query")
        self.assertEqual(req.method, "POST")

    @patch("urllib.request.urlopen")
    def test_verticals_url(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response([])
        client = NormaAI(api_key="nai_test", base_url="https://api.test")
        client.verticals()

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.full_url, "https://api.test/verticals")
        self.assertEqual(req.method, "GET")

    @patch("urllib.request.urlopen")
    def test_sources_url(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response([])
        client = NormaAI(api_key="nai_test", base_url="https://api.test")
        client.sources("avvocato")

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.full_url, "https://api.test/sources?vertical_id=avvocato")

    @patch("urllib.request.urlopen")
    def test_usage_url(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response({"queries_used": 5})
        client = NormaAI(api_key="nai_test", base_url="https://api.test")
        client.usage()

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.full_url, "https://api.test/usage")

    @patch("urllib.request.urlopen")
    def test_headers_include_api_key(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response({})
        client = NormaAI(api_key="nai_secret", base_url="https://api.test")
        client.verticals()

        req = mock_urlopen.call_args[0][0]
        self.assertEqual(req.get_header("X-api-key"), "nai_secret")
        self.assertEqual(req.get_header("Content-type"), "application/json")


class TestNormaAIResponses(unittest.TestCase):
    """Test response parsing."""

    @patch("urllib.request.urlopen")
    def test_query_returns_dict(self, mock_urlopen: MagicMock) -> None:
        payload = {"answer": "Il TFR si calcola...", "sources": [], "tokens_used": 150}
        mock_urlopen.return_value = _mock_response(payload)
        client = NormaAI(api_key="nai_test")
        result = client.query("Come si calcola il TFR?", "commercialista")
        self.assertEqual(result["answer"], "Il TFR si calcola...")
        self.assertEqual(result["tokens_used"], 150)

    @patch("urllib.request.urlopen")
    def test_query_sends_correct_body(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response({"answer": ""})
        client = NormaAI(api_key="nai_test")
        client.query("Domanda?", "lavoro")

        req = mock_urlopen.call_args[0][0]
        body = json.loads(req.data.decode("utf-8"))
        self.assertEqual(body["question"], "Domanda?")
        self.assertEqual(body["vertical_id"], "lavoro")


class TestNormaAIErrors(unittest.TestCase):
    """Test error handling."""

    @patch("urllib.request.urlopen")
    def test_http_error_with_json_body(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = _mock_http_error(
            {"error": "rate_limit_exceeded", "message": "Limite superato"}, 429
        )
        client = NormaAI(api_key="nai_test")

        with self.assertRaises(NormaAIError) as ctx:
            client.query("test", "lavoro")

        self.assertEqual(ctx.exception.status, 429)
        self.assertIn("Limite superato", str(ctx.exception))

    @patch("urllib.request.urlopen")
    def test_http_error_with_plain_body(self, mock_urlopen: MagicMock) -> None:
        import urllib.error
        err = urllib.error.HTTPError(
            url="https://example.com",
            code=500,
            msg="Internal Server Error",
            hdrs={},  # type: ignore[arg-type]
            fp=BytesIO(b"not json"),
        )
        mock_urlopen.side_effect = err
        client = NormaAI(api_key="nai_test")

        with self.assertRaises(NormaAIError) as ctx:
            client.verticals()

        self.assertEqual(ctx.exception.status, 500)

    def test_normaai_error_attributes(self) -> None:
        err = NormaAIError("test message", status=403, code="forbidden")
        self.assertEqual(str(err), "test message")
        self.assertEqual(err.status, 403)
        self.assertEqual(err.code, "forbidden")


# ---------------------------------------------------------------------------
# Async client tests
# ---------------------------------------------------------------------------

class TestAsyncNormaAIInit(unittest.TestCase):
    """Test async client instantiation."""

    def test_requires_api_key(self) -> None:
        with self.assertRaises(ValueError):
            AsyncNormaAI(api_key="")

    def test_default_base_url(self) -> None:
        client = AsyncNormaAI(api_key="nai_test")
        self.assertEqual(client._base_url, DEFAULT_API_URL)

    def test_custom_base_url(self) -> None:
        client = AsyncNormaAI(api_key="nai_test", base_url="https://custom.api/v1")
        self.assertEqual(client._base_url, "https://custom.api/v1")


class TestAsyncNormaAIMethods(unittest.TestCase):
    """Test async client methods via mocked sync requests."""

    def _run(self, coro):  # type: ignore[no-untyped-def]
        """Helper to run a coroutine in a fresh event loop."""
        loop = asyncio.new_event_loop()
        try:
            return loop.run_until_complete(coro)
        finally:
            loop.close()

    @patch("normaai.async_client.urllib.request.urlopen")
    def test_async_query(self, mock_urlopen: MagicMock) -> None:
        payload = {"answer": "Risposta async", "sources": []}
        mock_urlopen.return_value = _mock_response(payload)

        client = AsyncNormaAI(api_key="nai_test", base_url="https://api.test")
        result = self._run(client.query("Domanda?", "lavoro"))
        self.assertEqual(result["answer"], "Risposta async")

    @patch("normaai.async_client.urllib.request.urlopen")
    def test_async_verticals(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response([{"id": "lavoro"}])
        client = AsyncNormaAI(api_key="nai_test")
        result = self._run(client.verticals())
        self.assertIsInstance(result, list)

    @patch("normaai.async_client.urllib.request.urlopen")
    def test_async_sources(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response([{"title": "DL 81/2008"}])
        client = AsyncNormaAI(api_key="nai_test")
        result = self._run(client.sources("ingegnere"))
        self.assertEqual(result[0]["title"], "DL 81/2008")

    @patch("normaai.async_client.urllib.request.urlopen")
    def test_async_usage(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response({"queries_used": 42})
        client = AsyncNormaAI(api_key="nai_test")
        result = self._run(client.usage())
        self.assertEqual(result["queries_used"], 42)

    @patch("normaai.async_client.urllib.request.urlopen")
    def test_async_error_handling(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = _mock_http_error({"error": "unauthorized"}, 401)
        client = AsyncNormaAI(api_key="nai_bad")

        with self.assertRaises(NormaAIError) as ctx:
            self._run(client.query("test", "lavoro"))

        self.assertEqual(ctx.exception.status, 401)


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