from __future__ import annotations

import json
import unittest
from types import SimpleNamespace
from unittest.mock import patch

from aniket_tools.exceptions import ApiError, ErrorHandler
from aniket_tools.exception_handlers import api_http_validation, cache_queue_family, database_family, http_auth_cloud_family


class TestExceptionsPublicAPI(unittest.TestCase):
    def setUp(self) -> None:
        self.handler = ErrorHandler()

    def test_api_error_is_preserved(self) -> None:
        exc = ApiError(
            "Report already exists.",
            status_code=409,
            code="duplicate_resource",
            details=[{"field": "name", "message": "already exists"}],
        )

        payload = self.handler.build_payload(exc)

        self.assertEqual(payload["response_code"], 409)
        self.assertEqual(payload["error_message"], "Report already exists.")
        self.assertEqual(payload["error"]["code"], "duplicate_resource")
        self.assertEqual(payload["errors"][0]["field"], "name")

    def test_json_decode_error_has_small_message_and_details(self) -> None:
        exc = json.JSONDecodeError("Expecting value", "{", 0)

        payload = self.handler.build_payload(exc)

        self.assertEqual(payload["response_code"], 400)
        self.assertEqual(payload["error"]["code"], "invalid_json")
        self.assertEqual(payload["error_message"], "The request body is not valid JSON.")
        self.assertEqual(payload["errors"][0]["type"], "invalid_json")
        self.assertIn("lineno", payload["errors"][0])

    def test_long_value_error_falls_back_to_default_message(self) -> None:
        info = self.handler.describe(ValueError("x" * 500))
        self.assertEqual(info.status_code, 400)
        self.assertEqual(info.code, "bad_request")
        self.assertEqual(info.message, "The request data is invalid.")

    def test_http_exception_mapping_hides_500_detail_message(self) -> None:
        class FakeHTTPException(Exception):
            def __init__(self, *, status_code: int, detail: object) -> None:
                self.status_code = status_code
                self.detail = detail

        with patch.object(api_http_validation, "HTTP_EXCEPTION_TYPES", (FakeHTTPException,)):
            info = self.handler.describe(
                FakeHTTPException(
                    status_code=500,
                    detail={"message": "raw internal trace should not leak", "code": "boom"},
                )
            )

        self.assertEqual(info.status_code, 500)
        self.assertEqual(info.code, "boom")
        self.assertEqual(info.message, "An unexpected error occurred.")

    def test_validation_error_keeps_source_in_details(self) -> None:
        class FakeValidationError(Exception):
            def errors(self) -> list[dict[str, object]]:
                return [{"loc": ("query", "email"), "msg": "field required", "type": "missing"}]

        with patch.object(api_http_validation, "VALIDATION_EXCEPTION_TYPES", (FakeValidationError,)):
            payload = self.handler.build_payload(FakeValidationError())

        self.assertEqual(payload["response_code"], 422)
        self.assertEqual(payload["error"]["code"], "validation_error")
        self.assertEqual(payload["errors"][0]["field"], "email")
        self.assertEqual(payload["errors"][0]["source"], "query")

    def test_database_timeout_maps_to_504(self) -> None:
        class FakeDatabaseTimeout(Exception):
            pass

        with patch.object(database_family, "DB_TIMEOUT_EXCEPTION_TYPES", (FakeDatabaseTimeout,)):
            info = self.handler.describe(FakeDatabaseTimeout("lock wait timeout exceeded"))

        self.assertEqual(info.status_code, 504)
        self.assertEqual(info.code, "database_timeout")

    def test_database_duplicate_detection_is_not_triggered_by_plain_unique_word(self) -> None:
        class FakeIntegrityError(Exception):
            pass

        with patch.object(database_family, "INTEGRITY_EXCEPTION_TYPES", (FakeIntegrityError,)):
            info = self.handler.describe(FakeIntegrityError("column names are not unique"))

        self.assertNotEqual(info.code, "duplicate_resource")
        self.assertNotEqual(info.status_code, 409)

    def test_requests_timeout_maps_to_upstream_timeout(self) -> None:
        class FakeRequestsTimeout(Exception):
            pass

        fake_requests_exceptions = SimpleNamespace(
            Timeout=FakeRequestsTimeout,
            ConnectTimeout=None,
            ReadTimeout=None,
            ConnectionError=None,
            SSLError=None,
            ProxyError=None,
            URLRequired=None,
            MissingSchema=None,
            InvalidSchema=None,
            InvalidURL=None,
            InvalidProxyURL=None,
            HTTPError=None,
            TooManyRedirects=None,
            ChunkedEncodingError=None,
            ContentDecodingError=None,
            JSONDecodeError=None,
        )

        with patch.object(http_auth_cloud_family, "RequestsExceptions", fake_requests_exceptions):
            info = self.handler.describe(FakeRequestsTimeout("request timed out"))

        self.assertEqual(info.status_code, 504)
        self.assertEqual(info.code, "upstream_timeout")

    def test_redis_watch_error_maps_to_cache_conflict(self) -> None:
        class FakeWatchError(Exception):
            pass

        fake_redis_exceptions = SimpleNamespace(
            TimeoutError=None,
            AuthenticationError=None,
            AuthorizationError=None,
            WatchError=FakeWatchError,
            ExecAbortError=None,
            LockError=None,
            LockNotOwnedError=None,
            DataError=None,
            ResponseError=None,
            ConnectionError=None,
            BusyLoadingError=None,
            ClusterDownError=None,
            MaxConnectionsError=None,
            TryAgainError=None,
            ReadOnlyError=None,
            RedisError=None,
        )

        with patch.object(cache_queue_family, "RedisExceptions", fake_redis_exceptions):
            info = self.handler.describe(FakeWatchError("watched key changed"))

        self.assertEqual(info.status_code, 409)
        self.assertEqual(info.code, "cache_conflict")


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