from __future__ import annotations

import asyncio
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from typing import Any
from unittest.mock import patch

import httpx

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"
if str(SRC) not in sys.path:
    sys.path.insert(0, str(SRC))

from artasupport import (  # noqa: E402
    APIError,
    ArtaSupportMessage,
    AsyncArtaSupportMessage,
    AuthenticationError,
    ConfigurationError,
    NotFoundError,
    RateLimitError,
    ServerError,
    tse_batch,
    tse_copilot,
    tse_ingest,
)


class SDKConfigTests(unittest.TestCase):
    def test_missing_api_key_raises(self):
        with patch.dict(os.environ, {"ARTASUPPORT_BASE_URL": "https://api.example.com"}, clear=True):
            with self.assertRaises(ConfigurationError):
                ArtaSupportMessage()

    def test_missing_base_url_raises(self):
        with patch.dict(os.environ, {"ARTASUPPORT_API_KEY": "arta_live_a.b"}, clear=True):
            with self.assertRaises(ConfigurationError):
                ArtaSupportMessage()


class SyncSDKTests(unittest.TestCase):
    def test_tse_copilot_success_and_headers(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["path"] = request.url.path
            captured["x_api_key"] = request.headers.get("x-api-key")
            captured["authorization"] = request.headers.get("authorization")
            captured["api_version"] = request.headers.get("artasupport-version")
            captured["payload"] = json.loads(request.content.decode("utf-8"))
            return httpx.Response(
                200,
                json={
                    "id": "msg_1",
                    "content": [{"type": "text", "text": "Copilot reply"}],
                    "session_id": "sess_12345678",
                    "response_type": "answer",
                    "waiting_for_clarification": False,
                    "next_steps": [],
                    "usage": {"input_tokens": 7, "output_tokens": 11},
                },
                request=request,
            )

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        response = sdk.tse_copilot(user_input="internet is slow", session_id="sess_12345678")
        http_client.close()

        self.assertEqual(captured["path"], "/api/tse")
        self.assertEqual(captured["x_api_key"], "arta_live_test.secret")
        self.assertEqual(captured["authorization"], "Bearer arta_live_test.secret")
        self.assertIsNone(captured["api_version"])
        self.assertEqual(captured["payload"]["mode"], "copilot")
        self.assertEqual(captured["payload"]["model"], "artasupport-v1")
        self.assertEqual(captured["payload"]["messages"][0]["role"], "user")
        self.assertEqual(captured["payload"]["session_id"], "sess_12345678")
        self.assertEqual(response.text, "Copilot reply")
        self.assertEqual(response.session_id, "sess_12345678")
        self.assertEqual(response.response_type, "answer")
        self.assertFalse(response.waiting_for_clarification)
        self.assertEqual(response.usage.input_tokens if response.usage else None, 7)

    def test_tse_batch_success_without_session_id(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["payload"] = json.loads(request.content.decode("utf-8"))
            return httpx.Response(
                200,
                json={
                    "id": "msg_2",
                    "content": [{"type": "text", "text": "Batch reply"}],
                },
                request=request,
            )

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        response = sdk.tse_batch(
            messages=[{"role": "user", "content": "check routes"}],
            temperature=None,
        )
        http_client.close()

        self.assertEqual(captured["payload"]["mode"], "batch")
        self.assertNotIn("session_id", captured["payload"])
        self.assertNotIn("temperature", captured["payload"])
        self.assertEqual(response.text, "Batch reply")
        self.assertIsNone(response.session_id)
        self.assertEqual(response.next_steps, [])
        self.assertIsNone(response.usage)

    def test_tse_copilot_accepts_user_input_keyword(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["payload"] = json.loads(request.content.decode("utf-8"))
            return httpx.Response(
                200,
                json={"content": [{"type": "text", "text": "ok"}]},
                request=request,
            )

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        response = sdk.tse_copilot(user_input="internet is slow")
        http_client.close()

        self.assertEqual(captured["payload"]["messages"][0]["content"], "internet is slow")
        self.assertEqual(response.text, "ok")

    def test_tse_batch_accepts_messages_keyword(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["payload"] = json.loads(request.content.decode("utf-8"))
            return httpx.Response(
                200,
                json={"content": [{"type": "text", "text": "ok"}]},
                request=request,
            )

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        response = sdk.tse_batch(messages=[{"role": "user", "content": "check tunnel health"}])
        http_client.close()

        self.assertEqual(captured["payload"]["messages"][0]["content"], "check tunnel health")
        self.assertEqual(response.text, "ok")

    def test_rejects_multiple_message_aliases(self):
        transport = httpx.MockTransport(lambda request: httpx.Response(200, json={}, request=request))
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        with self.assertRaises(ValueError):
            sdk.tse_copilot(
                user_input="hello",
                messages=[{"role": "user", "content": "duplicate"}],
            )
        http_client.close()

    def test_models_and_sessions_endpoints(self):
        calls: list[tuple[str, str]] = []

        def handler(request: httpx.Request) -> httpx.Response:
            calls.append((request.method, request.url.path))
            if request.method == "GET" and request.url.path == "/v1/models":
                return httpx.Response(
                    200,
                    json={
                        "object": "list",
                        "data": [
                            {
                                "id": "artasupport-v1",
                                "object": "model",
                                "created": 12345,
                                "owned_by": "arta",
                            }
                        ],
                    },
                    request=request,
                )
            if request.method == "POST" and request.url.path == "/v1/sessions":
                return httpx.Response(
                    200,
                    json={
                        "session_id": "sess_abc12345",
                        "ttl_seconds": 86400,
                        "created_at": "2026-03-24T00:00:00Z",
                    },
                    request=request,
                )
            if request.method == "DELETE" and request.url.path == "/v1/sessions/sess_abc12345":
                return httpx.Response(
                    200,
                    json={
                        "session_id": "sess_abc12345",
                        "cleared": True,
                        "cleared_at": "2026-03-24T00:05:00Z",
                    },
                    request=request,
                )
            return httpx.Response(500, json={"detail": "Unexpected request"}, request=request)

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )

        models = sdk.list_models()
        session = sdk.create_session()
        deleted = sdk.delete_session(session.session_id)
        http_client.close()

        self.assertEqual(models[0].id, "artasupport-v1")
        self.assertEqual(session.session_id, "sess_abc12345")
        self.assertTrue(deleted.cleared)
        self.assertEqual(
            calls,
            [
                ("GET", "/v1/models"),
                ("POST", "/v1/sessions"),
                ("DELETE", "/v1/sessions/sess_abc12345"),
            ],
        )

    def test_error_mapping(self):
        cases = [
            (400, APIError),
            (401, AuthenticationError),
            (404, NotFoundError),
            (429, RateLimitError),
            (500, ServerError),
        ]
        for status_code, expected_cls in cases:
            with self.subTest(status_code=status_code):
                def handler(request: httpx.Request) -> httpx.Response:
                    return httpx.Response(
                        status_code,
                        json={
                            "detail": {
                                "status_code": status_code,
                                "error": "request_failed",
                                "message": f"Failure {status_code}",
                                "request_id": "req_test_123",
                            }
                        },
                        request=request,
                    )

                transport = httpx.MockTransport(handler)
                http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
                sdk = ArtaSupportMessage(
                    api_key="arta_live_test.secret",
                    base_url="https://api.example.com",
                    client=http_client,
                )
                with self.assertRaises(expected_cls) as cm:
                    sdk.tse_batch(user_input="hello")
                http_client.close()

                exc = cm.exception
                self.assertEqual(exc.status_code, status_code)
                self.assertEqual(exc.request_id, "req_test_123")
                self.assertIn(str(status_code), str(exc))

    def test_top_level_function_exports(self):
        self.assertTrue(callable(tse_copilot))
        self.assertTrue(callable(tse_batch))

    def test_top_level_functions_delegate_to_client(self):
        with patch("artasupport.client.ArtaSupportMessage") as client_cls:
            instance = client_cls.return_value.__enter__.return_value
            instance.tse_copilot.return_value = "copilot-ok"
            instance.tse_batch.return_value = "batch-ok"

            copilot_result = tse_copilot(
                user_input="hello",
                api_key="arta_live_key.secret",
                base_url="https://api.example.com",
            )
            batch_result = tse_batch(
                user_input="hello",
                api_key="arta_live_key.secret",
                base_url="https://api.example.com",
            )

            self.assertEqual(copilot_result, "copilot-ok")
            self.assertEqual(batch_result, "batch-ok")

    def test_top_level_functions_accept_user_input_alias(self):
        with patch("artasupport.client.ArtaSupportMessage") as client_cls:
            instance = client_cls.return_value.__enter__.return_value
            instance.tse_copilot.return_value = "ok"

            result = tse_copilot(
                user_input="hello",
                api_key="arta_live_key.secret",
                base_url="https://api.example.com",
            )

            self.assertEqual(result, "ok")
            _, kwargs = instance.tse_copilot.call_args
            self.assertEqual(kwargs.get("user_input"), "hello")

    def test_alias_classes_construct(self):
        transport = httpx.MockTransport(lambda request: httpx.Response(200, json={}, request=request))
        with ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=httpx.Client(base_url="https://api.example.com", transport=transport),
        ) as sdk:
            self.assertIsInstance(sdk, ArtaSupportMessage)

        async_sdk = AsyncArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=httpx.AsyncClient(base_url="https://api.example.com", transport=transport),
        )
        self.assertTrue(hasattr(async_sdk, "tse_copilot"))
        asyncio.run(async_sdk._client.aclose())

    def test_tse_ingest_success_uses_api_ingest_route(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["path"] = request.url.path
            return httpx.Response(
                200,
                json={"status": "completed"},
                request=request,
            )

        transport = httpx.MockTransport(handler)
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )

        temp_path: str | None = None
        try:
            with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
                tmp.write(b"fake pdf content")
                temp_path = tmp.name

            response = sdk.tse_ingest(temp_path)
            self.assertEqual(response.status, "completed")
            self.assertEqual(captured["method"], "POST")
            self.assertEqual(captured["path"], "/api/ingest")
        finally:
            if temp_path:
                Path(temp_path).unlink(missing_ok=True)
            http_client.close()

    def test_tse_ingest_file_not_found_raises(self):
        transport = httpx.MockTransport(lambda request: httpx.Response(200, json={}, request=request))
        http_client = httpx.Client(base_url="https://api.example.com", transport=transport)
        sdk = ArtaSupportMessage(
            api_key="arta_live_test.secret",
            base_url="https://api.example.com",
            client=http_client,
        )
        with self.assertRaises(ValueError):
            sdk.tse_ingest("C:/does/not/exist/file.pdf")
        http_client.close()

    def test_top_level_tse_ingest_delegates_to_client(self):
        with patch("artasupport.client.ArtaSupportMessage") as client_cls:
            instance = client_cls.return_value.__enter__.return_value
            instance.tse_ingest.return_value = "ingest-ok"

            result = tse_ingest(
                "runbook.pdf",
                api_key="arta_live_key.secret",
                base_url="https://api.example.com",
            )

            self.assertEqual(result, "ingest-ok")
            instance.tse_ingest.assert_called_once_with("runbook.pdf")


class AsyncSDKTests(unittest.IsolatedAsyncioTestCase):
    async def test_async_copilot(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["payload"] = json.loads(request.content.decode("utf-8"))
            return httpx.Response(
                200,
                json={
                    "content": [{"type": "text", "text": "Async reply"}],
                    "session_id": "sess_async_1234",
                    "response_type": "answer",
                },
                request=request,
            )

        transport = httpx.MockTransport(handler)
        async_client = httpx.AsyncClient(base_url="https://api.example.com", transport=transport)
        sdk = AsyncArtaSupportMessage(
            api_key="arta_live_async.secret",
            base_url="https://api.example.com",
            client=async_client,
        )
        response = await sdk.tse_copilot(user_input="async help")
        await async_client.aclose()

        self.assertEqual(captured["payload"]["mode"], "copilot")
        self.assertEqual(response.text, "Async reply")
        self.assertEqual(response.session_id, "sess_async_1234")

    async def test_async_models_and_session_flow(self):
        def handler(request: httpx.Request) -> httpx.Response:
            if request.method == "GET" and request.url.path == "/v1/models":
                return httpx.Response(
                    200,
                    json={"object": "list", "data": [{"id": "artasupport-v1"}]},
                    request=request,
                )
            if request.method == "POST" and request.url.path == "/v1/sessions":
                return httpx.Response(
                    200,
                    json={"session_id": "sess_async_9999", "ttl_seconds": 86400},
                    request=request,
                )
            if request.method == "DELETE" and request.url.path == "/v1/sessions/sess_async_9999":
                return httpx.Response(
                    200,
                    json={"session_id": "sess_async_9999", "cleared": True},
                    request=request,
                )
            return httpx.Response(500, json={"detail": "Unexpected request"}, request=request)

        transport = httpx.MockTransport(handler)
        async_client = httpx.AsyncClient(base_url="https://api.example.com", transport=transport)
        sdk = AsyncArtaSupportMessage(
            api_key="arta_live_async.secret",
            base_url="https://api.example.com",
            client=async_client,
        )

        models = await sdk.list_models()
        session = await sdk.create_session()
        deleted = await sdk.delete_session(session.session_id)
        await async_client.aclose()

        self.assertEqual(models[0].id, "artasupport-v1")
        self.assertEqual(session.session_id, "sess_async_9999")
        self.assertTrue(deleted.cleared)

    async def test_async_tse_ingest_success_uses_api_ingest_route(self):
        captured: dict[str, Any] = {}

        def handler(request: httpx.Request) -> httpx.Response:
            captured["method"] = request.method
            captured["path"] = request.url.path
            return httpx.Response(
                200,
                json={"status": "completed"},
                request=request,
            )

        transport = httpx.MockTransport(handler)
        async_client = httpx.AsyncClient(base_url="https://api.example.com", transport=transport)
        sdk = AsyncArtaSupportMessage(
            api_key="arta_live_async.secret",
            base_url="https://api.example.com",
            client=async_client,
        )

        temp_path: str | None = None
        try:
            with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
                tmp.write(b"fake xlsx content")
                temp_path = tmp.name

            response = await sdk.tse_ingest(temp_path)
            self.assertEqual(response.status, "completed")
            self.assertEqual(captured["method"], "POST")
            self.assertEqual(captured["path"], "/api/ingest")
        finally:
            if temp_path:
                Path(temp_path).unlink(missing_ok=True)
            await async_client.aclose()


class PackagingSmokeTests(unittest.TestCase):
    def test_pyproject_exists(self):
        self.assertTrue((ROOT / "pyproject.toml").exists())

    def test_license_exists(self):
        self.assertTrue((ROOT / "LICENSE").exists())

    def test_pytyped_exists(self):
        self.assertTrue((ROOT / "src" / "artasupport" / "py.typed").exists())


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