"""
Module: test_repl_client
Description: Unit tests for REPLClient and _check_response error classification.

Tests cover all HTTP status code branches in _check_response (401, 404, 422,
5xx, and generic errors) and structured sandbox error parsing in
execute_command.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, PropertyMock

import httpx
import pytest

from huitzo_cli.errors import (
    AuthenticationError,
    CommandNotFoundError,
    CommandValidationError,
    HuitzoCliError,
    NetworkError,
    SandboxCommandError,
    ServerError,
)
from huitzo_cli.repl.client import REPLClient, _check_response


def _mock_response(
    status_code: int,
    json_data: dict | None = None,
    *,
    json_error: bool = False,
    text: str = "",
) -> MagicMock:
    """Create a mock httpx response."""
    resp = MagicMock()
    resp.status_code = status_code
    type(resp).is_success = PropertyMock(return_value=(200 <= status_code < 300))
    if json_error:
        resp.json.side_effect = ValueError("not json")
        resp.text = text or "Error"
    elif json_data is not None:
        resp.json.return_value = json_data
    else:
        resp.json.return_value = {}
        resp.text = text
    return resp


class TestCheckResponse:
    """Unit tests for _check_response status code dispatch."""

    def test_success_does_not_raise(self) -> None:
        resp = _mock_response(200)
        _check_response(resp)  # should not raise

    def test_401_raises_authentication_error(self) -> None:
        resp = _mock_response(401, {"detail": "Invalid token"})
        with pytest.raises(AuthenticationError):
            _check_response(resp)

    def test_404_raises_command_not_found_error(self) -> None:
        resp = _mock_response(404, {"detail": "Command 'foo' not found"})
        with pytest.raises(CommandNotFoundError) as exc_info:
            _check_response(resp)
        assert "foo" in str(exc_info.value)

    def test_404_without_detail(self) -> None:
        resp = _mock_response(404, {})
        with pytest.raises(CommandNotFoundError) as exc_info:
            _check_response(resp)
        assert "Not found" in str(exc_info.value)

    def test_422_raises_command_validation_error(self) -> None:
        resp = _mock_response(422, {"detail": "Missing required field: name"})
        with pytest.raises(CommandValidationError) as exc_info:
            _check_response(resp)
        assert "Missing required field" in str(exc_info.value)

    def test_422_without_detail(self) -> None:
        resp = _mock_response(422, {})
        with pytest.raises(CommandValidationError) as exc_info:
            _check_response(resp)
        assert "Validation failed" in str(exc_info.value)

    def test_500_raises_server_error(self) -> None:
        resp = _mock_response(500, {"detail": "Internal crash"})
        with pytest.raises(ServerError) as exc_info:
            _check_response(resp)
        assert exc_info.value.user_message == "Internal crash"

    def test_502_raises_server_error(self) -> None:
        resp = _mock_response(502, {"detail": "Bad gateway"})
        with pytest.raises(ServerError):
            _check_response(resp)

    def test_5xx_without_detail(self) -> None:
        resp = _mock_response(503, {})
        with pytest.raises(ServerError) as exc_info:
            _check_response(resp)
        assert "Server error" in str(exc_info.value.user_message)

    def test_5xx_non_json_body(self) -> None:
        resp = _mock_response(500, json_error=True, text="Internal Server Error")
        with pytest.raises(ServerError) as exc_info:
            _check_response(resp)
        assert "Internal Server Error" in exc_info.value.user_message

    def test_other_status_raises_huitzo_cli_error(self) -> None:
        resp = _mock_response(403, {"detail": "Forbidden"})
        with pytest.raises(HuitzoCliError) as exc_info:
            _check_response(resp)
        assert "Forbidden" in exc_info.value.user_message

    def test_other_status_without_detail(self) -> None:
        resp = _mock_response(418, {})
        with pytest.raises(HuitzoCliError) as exc_info:
            _check_response(resp)
        assert "418" in exc_info.value.user_message


class TestREPLClientExecuteCommand:
    """Tests for execute_command structured error parsing."""

    @pytest.mark.anyio
    async def test_structured_error_response(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.post.return_value = _mock_response(
            500,
            {
                "error": "StorageError",
                "message": "Storage unavailable",
                "command": "save-data",
                "code": "STORAGE_ERROR",
            },
        )
        client._client = mock_http

        with pytest.raises(SandboxCommandError) as exc_info:
            await client.execute_command("save-data", {})
        assert exc_info.value.error_type == "StorageError"
        assert exc_info.value.command == "save-data"

    @pytest.mark.anyio
    async def test_non_structured_error_falls_through(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.post.return_value = _mock_response(500, json_error=True)
        client._client = mock_http

        with pytest.raises(ServerError):
            await client.execute_command("boom", {})

    @pytest.mark.anyio
    async def test_connect_error_raises_network_error(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.post.side_effect = httpx.ConnectError("Connection refused")
        client._client = mock_http

        with pytest.raises(NetworkError) as exc_info:
            await client.execute_command("any", {})
        assert "sandbox" in exc_info.value.user_message.lower()

    @pytest.mark.anyio
    async def test_timeout_raises_network_error(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.post.side_effect = httpx.ReadTimeout("Timed out")
        client._client = mock_http

        with pytest.raises(NetworkError) as exc_info:
            await client.execute_command("any", {})
        assert "timed out" in exc_info.value.user_message.lower()

    @pytest.mark.anyio
    async def test_success_returns_json(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.post.return_value = _mock_response(200, {"data": {"result": "ok"}})
        client._client = mock_http

        result = await client.execute_command("echo", {"msg": "hi"})
        assert result == {"data": {"result": "ok"}}


class TestREPLClientListCommands:
    """Tests for list_commands error handling."""

    @pytest.mark.anyio
    async def test_list_commands_success(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.get.return_value = _mock_response(200, {"commands": [{"name": "echo"}]})
        client._client = mock_http

        result = await client.list_commands()
        assert result == {"commands": [{"name": "echo"}]}

    @pytest.mark.anyio
    async def test_list_commands_connect_error(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.get.side_effect = httpx.ConnectError("Connection refused")
        client._client = mock_http

        with pytest.raises(NetworkError):
            await client.list_commands()

    @pytest.mark.anyio
    async def test_list_commands_401(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.get.return_value = _mock_response(401, {"detail": "Unauthorized"})
        client._client = mock_http

        with pytest.raises(AuthenticationError):
            await client.list_commands()


class TestREPLClientHealth:
    """Tests for health check."""

    @pytest.mark.anyio
    async def test_health_returns_true(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.get.return_value = _mock_response(200)
        client._client = mock_http

        assert await client.health() is True

    @pytest.mark.anyio
    async def test_health_returns_false_on_error(self) -> None:
        client = REPLClient("http://localhost:9999")
        mock_http = AsyncMock()
        mock_http.get.side_effect = httpx.ConnectError("Connection refused")
        client._client = mock_http

        assert await client.health() is False
