"""
Module: test_backends
Description: Tests for local backend implementations used in --use-platform mode.

Implements:
    - docs/cli/reference.md#huitzo-dev
"""

from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from huitzo_sdk.errors import HTTPError, LLMError, StorageError

from huitzo_cli.sandbox.backends.files import LocalFileBackend
from huitzo_cli.sandbox.backends.http import LocalHTTPBackend
from huitzo_cli.sandbox.backends.llm import (
    LocalLLMBackend,
    _detect_provider,
    _get_api_key,
)


# --- LLM Backend: Provider Detection ---


def test_detect_provider_openai_gpt() -> None:
    assert _detect_provider("gpt-4o") == "openai"


def test_detect_provider_openai_o1() -> None:
    assert _detect_provider("o1-preview") == "openai"


def test_detect_provider_openai_o3() -> None:
    assert _detect_provider("o3-mini") == "openai"


def test_detect_provider_anthropic() -> None:
    assert _detect_provider("claude-3-opus") == "anthropic"


def test_detect_provider_unknown_raises() -> None:
    with pytest.raises(LLMError, match="Cannot detect provider"):
        _detect_provider("llama-70b")


# --- LLM Backend: API Key ---


def test_get_api_key_openai(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
    assert _get_api_key("openai") == "sk-test"


def test_get_api_key_anthropic(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
    assert _get_api_key("anthropic") == "sk-ant-test"


def test_get_api_key_missing_raises(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    with pytest.raises(LLMError, match="Set OPENAI_API_KEY"):
        _get_api_key("openai")


# --- LLM Backend: Complete ---


@pytest.mark.asyncio
async def test_llm_backend_complete_openai(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
    backend = LocalLLMBackend()

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "choices": [{"message": {"content": "Hello from OpenAI"}}],
    }

    with patch.object(
        httpx.AsyncClient, "post", new_callable=AsyncMock, return_value=mock_response
    ):
        result = await backend.complete(
            prompt="Hi",
            system=None,
            model="gpt-4o",
            temperature=1.0,
            max_tokens=100,
            top_p=None,
            stop=None,
            response_format=None,
            schema=None,
        )
    assert result == "Hello from OpenAI"


@pytest.mark.asyncio
async def test_llm_backend_complete_anthropic(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
    backend = LocalLLMBackend()

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "content": [{"text": "Hello from Anthropic"}],
    }

    with patch.object(
        httpx.AsyncClient, "post", new_callable=AsyncMock, return_value=mock_response
    ):
        result = await backend.complete(
            prompt="Hi",
            system="You are helpful.",
            model="claude-3-opus",
            temperature=0.7,
            max_tokens=None,
            top_p=None,
            stop=None,
            response_format=None,
            schema=None,
        )
    assert result == "Hello from Anthropic"


@pytest.mark.asyncio
async def test_llm_backend_complete_api_error(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
    backend = LocalLLMBackend()

    mock_response = MagicMock()
    mock_response.status_code = 401
    mock_response.text = "Unauthorized"

    with patch.object(
        httpx.AsyncClient, "post", new_callable=AsyncMock, return_value=mock_response
    ):
        with pytest.raises(LLMError, match="OpenAI API error"):
            await backend.complete(
                prompt="Hi",
                system=None,
                model="gpt-4o",
                temperature=1.0,
                max_tokens=None,
                top_p=None,
                stop=None,
                response_format=None,
                schema=None,
            )


@pytest.mark.asyncio
async def test_llm_backend_complete_missing_key() -> None:
    backend = LocalLLMBackend()

    with pytest.raises(LLMError, match="Set OPENAI_API_KEY"):
        await backend.complete(
            prompt="Hi",
            system=None,
            model="gpt-4o",
            temperature=1.0,
            max_tokens=None,
            top_p=None,
            stop=None,
            response_format=None,
            schema=None,
        )


# --- HTTP Backend ---


@pytest.mark.asyncio
async def test_http_backend_get_json() -> None:
    backend = LocalHTTPBackend()

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.headers = {"content-type": "application/json"}
    mock_response.json.return_value = {"key": "value"}
    mock_response.raise_for_status = MagicMock()

    with patch.object(
        httpx.AsyncClient, "request", new_callable=AsyncMock, return_value=mock_response
    ):
        result = await backend.request(
            "GET",
            "https://api.example.com/data",
            headers=None,
            params=None,
            json=None,
            data=None,
            files=None,
            timeout=None,
        )
    assert result == {"key": "value"}


@pytest.mark.asyncio
async def test_http_backend_get_text() -> None:
    backend = LocalHTTPBackend()

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.headers = {"content-type": "text/plain"}
    mock_response.text = "hello"
    mock_response.raise_for_status = MagicMock()

    with patch.object(
        httpx.AsyncClient, "request", new_callable=AsyncMock, return_value=mock_response
    ):
        result = await backend.request(
            "GET",
            "https://example.com",
            headers=None,
            params=None,
            json=None,
            data=None,
            files=None,
            timeout=None,
        )
    assert result == "hello"


@pytest.mark.asyncio
async def test_http_backend_error() -> None:
    backend = LocalHTTPBackend()

    mock_response = httpx.Response(status_code=404, request=httpx.Request("GET", "https://x.com"))

    with patch.object(
        httpx.AsyncClient, "request", new_callable=AsyncMock, return_value=mock_response
    ):
        with pytest.raises(HTTPError, match="HTTP 404"):
            await backend.request(
                "GET",
                "https://x.com/missing",
                headers=None,
                params=None,
                json=None,
                data=None,
                files=None,
                timeout=None,
            )


# --- File Backend ---


@pytest.mark.asyncio
async def test_file_backend_write_read_roundtrip(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    await backend.write("test.txt", b"hello world")
    data = await backend.read("test.txt")
    assert data == b"hello world"


@pytest.mark.asyncio
async def test_file_backend_exists(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    assert not await backend.exists("missing.txt")
    await backend.write("found.txt", b"data")
    assert await backend.exists("found.txt")


@pytest.mark.asyncio
async def test_file_backend_info(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    await backend.write("doc.txt", b"content")
    info = await backend.info("doc.txt")
    assert info["name"] == "doc.txt"
    assert info["size"] == 7


@pytest.mark.asyncio
async def test_file_backend_list(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    await backend.write("a.txt", b"a")
    await backend.write("b.txt", b"bb")
    items = await backend.list("")
    names = [i["name"] for i in items]
    assert "a.txt" in names
    assert "b.txt" in names


@pytest.mark.asyncio
async def test_file_backend_read_missing(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    with pytest.raises(StorageError, match="File not found"):
        await backend.read("nope.txt")


@pytest.mark.asyncio
async def test_file_backend_path_traversal(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    with pytest.raises(StorageError, match="Path traversal denied"):
        await backend.read("../../etc/passwd")


@pytest.mark.asyncio
async def test_file_backend_get_url(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    await backend.write("doc.txt", b"data")
    url = await backend.get_url("doc.txt", expires=3600)
    assert url.startswith("file://")


@pytest.mark.asyncio
async def test_file_backend_nested_write(temp_dir: Path) -> None:
    backend = LocalFileBackend(temp_dir / "files")

    await backend.write("sub/dir/nested.txt", b"deep")
    data = await backend.read("sub/dir/nested.txt")
    assert data == b"deep"
