"""Tests for error handling — ensures errors never crash the MCP and always return useful JSON."""

import json
from unittest.mock import patch, AsyncMock

import pytest

from influ2_mcp.errors import (
    Influ2AuthError,
    Influ2Error,
    Influ2NotFoundError,
    Influ2RateLimitError,
    Influ2ServerError,
    Influ2ValidationError,
    format_error_for_llm,
)
from influ2_mcp.tools.cohorts import influ2_list_cohorts, influ2_get_cohort_details
from influ2_mcp.tools.clients import influ2_list_clients


class TestFormatErrorForLlm:
    """Verify error formatting produces safe, actionable JSON."""

    def test_auth_error(self):
        err = Influ2AuthError()
        result = format_error_for_llm(err)
        assert result["error"] is True
        assert "auth" in result["message"].lower() or "api key" in result["message"].lower()
        assert "suggestion" in result

    def test_not_found_error(self):
        err = Influ2NotFoundError("Cohort", "abc-123")
        result = format_error_for_llm(err)
        assert result["error"] is True

    def test_validation_error(self):
        err = Influ2ValidationError("Invalid cohort_id format")
        result = format_error_for_llm(err)
        assert result["error"] is True
        assert "Invalid" in result["message"]

    def test_rate_limit_error(self):
        err = Influ2RateLimitError(30)
        result = format_error_for_llm(err)
        assert result["error"] is True
        assert "suggestion" in result

    def test_server_error(self):
        err = Influ2ServerError(500)
        result = format_error_for_llm(err)
        assert result["error"] is True

    def test_generic_exception(self):
        err = RuntimeError("Something unexpected")
        result = format_error_for_llm(err)
        assert result["error"] is True
        assert "unexpected" in result["message"].lower() or "Something unexpected" in result["message"]

    def test_all_errors_are_json_serializable(self):
        """Every error format must be JSON-serializable — MCP returns JSON strings."""
        errors = [
            Influ2AuthError(),
            Influ2NotFoundError("Cohort", "id"),
            Influ2ValidationError("bad input"),
            Influ2RateLimitError(60),
            Influ2ServerError(502),
            Influ2Error("generic"),
            RuntimeError("crash"),
            ValueError("bad value"),
        ]
        for err in errors:
            result = format_error_for_llm(err)
            serialized = json.dumps(result)
            assert isinstance(serialized, str), f"Failed to serialize {type(err).__name__}"


class TestToolErrorHandling:
    """Verify that tools never raise — they always return JSON error strings."""

    @pytest.mark.asyncio
    async def test_list_cohorts_auth_error(self):
        """Auth failure must return JSON, not raise."""
        with (
            patch("influ2_mcp.tools.cohorts.resolve_client_id", return_value="test"),
            patch("influ2_mcp.tools.cohorts.get_api_client") as mock_get,
        ):
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(side_effect=Influ2AuthError())
            mock_get.return_value = mock_client

            result = await influ2_list_cohorts(client_id="test")
            data = json.loads(result)
            assert data["error"] is True

    @pytest.mark.asyncio
    async def test_list_cohorts_unexpected_crash(self):
        """Even a totally unexpected error must return JSON."""
        with (
            patch("influ2_mcp.tools.cohorts.resolve_client_id", return_value="test"),
            patch("influ2_mcp.tools.cohorts.get_api_client") as mock_get,
        ):
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(side_effect=RuntimeError("kaboom"))
            mock_get.return_value = mock_client

            result = await influ2_list_cohorts(client_id="test")
            data = json.loads(result)
            assert data["error"] is True

    @pytest.mark.asyncio
    async def test_get_cohort_details_not_found(self):
        with (
            patch("influ2_mcp.tools.cohorts.resolve_client_id", return_value="test"),
            patch("influ2_mcp.tools.cohorts.get_api_client") as mock_get,
        ):
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(side_effect=Influ2NotFoundError("Cohort", "bad-id"))
            mock_get.return_value = mock_client

            result = await influ2_get_cohort_details(
                cohort_id="bad-id", client_id="test"
            )
            data = json.loads(result)
            assert data["error"] is True

    @pytest.mark.asyncio
    async def test_list_clients_server_error(self):
        with patch("influ2_mcp.tools.clients.get_api_client") as mock_get:
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(side_effect=Influ2ServerError(503))
            mock_get.return_value = mock_client

            result = await influ2_list_clients()
            data = json.loads(result)
            assert data["error"] is True

    @pytest.mark.asyncio
    async def test_no_tool_ever_raises(self):
        """Meta-test: import all tool functions and verify they catch exceptions."""
        from influ2_mcp.tools.cohorts import influ2_list_cohorts, influ2_get_cohort_details
        from influ2_mcp.tools.clients import influ2_list_clients
        from influ2_mcp.tools.contents import influ2_list_contents
        from influ2_mcp.tools.jobs import influ2_check_job

        tools_with_args = [
            (influ2_list_clients, {}),
            (influ2_list_cohorts, {"client_id": "test"}),
            (influ2_get_cohort_details, {"cohort_id": "test", "client_id": "test"}),
            (influ2_list_contents, {"scope": "by_cohort", "cohort_id": "test", "client_id": "test"}),
            (influ2_check_job, {"job_id": "test"}),
        ]

        for tool_fn, kwargs in tools_with_args:
            mock_client = AsyncMock()
            mock_client.get = AsyncMock(side_effect=Exception("total failure"))
            mock_client.post = AsyncMock(side_effect=Exception("total failure"))

            with (
                patch("influ2_mcp.tools.cohorts.resolve_client_id", return_value="test"),
                patch("influ2_mcp.tools.cohorts.get_api_client", return_value=mock_client),
                patch("influ2_mcp.tools.clients.get_api_client", return_value=mock_client),
                patch("influ2_mcp.tools.contents.resolve_client_id", return_value="test"),
                patch("influ2_mcp.tools.contents.get_api_client", return_value=mock_client),
                patch("influ2_mcp.tools.jobs.get_api_client", return_value=mock_client),
            ):
                # Should NEVER raise — must return JSON
                result = await tool_fn(**kwargs)
                data = json.loads(result)
                assert data["error"] is True, (
                    f"{tool_fn.__name__} did not return error JSON on failure"
                )
