"""Tests for concrete LLM provider implementations (vllm, anthropic, openai).

All HTTP calls are mocked — no network access required.
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch
from urllib.error import URLError

import pytest

from orxaq.orchestration.providers.anthropic import AnthropicProvider
from orxaq.orchestration.providers.base import LLMResponse
from orxaq.orchestration.providers.openai import OpenAIProvider
from orxaq.orchestration.providers.vllm import VLLMProvider

# ── Helpers ───────────────────────────────────────────────────


def _mock_response(data: dict) -> MagicMock:
    """Create a mock urllib response that returns JSON data."""
    mock = MagicMock()
    mock.read.return_value = json.dumps(data).encode("utf-8")
    mock.__enter__ = lambda s: s
    mock.__exit__ = MagicMock(return_value=False)
    return mock


VLLM_RESPONSE = {
    "choices": [{"message": {"content": "hello from vllm"}}],
    "usage": {"prompt_tokens": 10, "completion_tokens": 20},
}

ANTHROPIC_RESPONSE = {
    "content": [{"type": "text", "text": "hello from claude"}],
    "usage": {"input_tokens": 15, "output_tokens": 25},
}

OPENAI_RESPONSE = {
    "choices": [{"message": {"content": "hello from openai"}}],
    "usage": {"prompt_tokens": 12, "completion_tokens": 18},
}

VLLM_MODELS = {
    "data": [{"id": "Qwen2.5-Coder-32B"}, {"id": "llama-3.1-8B"}],
}


# ── VLLMProvider ──────────────────────────────────────────────


class TestVLLMProvider:
    def test_init_defaults(self) -> None:
        p = VLLMProvider()
        assert p.name == "vllm"
        assert p.endpoint == "http://localhost:8000/v1"
        assert p.models == ["default"]

    def test_init_custom_endpoint(self) -> None:
        p = VLLMProvider(endpoint="http://gpu:9000/v1/")
        assert p.endpoint == "http://gpu:9000/v1"  # trailing slash stripped

    @patch("orxaq.orchestration.providers.vllm.urllib.request.urlopen")
    def test_generate_success(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(VLLM_RESPONSE)
        p = VLLMProvider()
        resp = p.generate("test prompt")

        assert isinstance(resp, LLMResponse)
        assert resp.text == "hello from vllm"
        assert resp.provider == "vllm"
        assert resp.input_tokens == 10
        assert resp.output_tokens == 20
        assert resp.latency_ms > 0
        assert "error" not in resp.metadata

    @patch("orxaq.orchestration.providers.vllm.urllib.request.urlopen")
    def test_generate_with_model_override(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(VLLM_RESPONSE)
        p = VLLMProvider()
        resp = p.generate("test", model="custom-model")
        assert resp.model == "custom-model"

    @patch("orxaq.orchestration.providers.vllm.urllib.request.urlopen")
    def test_generate_connection_error(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = URLError("Connection refused")
        p = VLLMProvider()
        resp = p.generate("test")

        assert resp.text == ""
        assert "error" in resp.metadata
        assert "Connection refused" in resp.metadata["error"]

    @patch("orxaq.orchestration.providers.vllm.urllib.request.urlopen")
    def test_health_check_healthy(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(VLLM_MODELS)
        p = VLLMProvider()
        health = p.health_check()

        assert health["healthy"] is True
        assert health["provider"] == "vllm"
        assert "Qwen2.5-Coder-32B" in health["models"]

    @patch("orxaq.orchestration.providers.vllm.urllib.request.urlopen")
    def test_health_check_unhealthy(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = URLError("Connection refused")
        p = VLLMProvider()
        health = p.health_check()

        assert health["healthy"] is False
        assert "error" in health


# ── AnthropicProvider ─────────────────────────────────────────


class TestAnthropicProvider:
    def test_init_with_explicit_key(self) -> None:
        p = AnthropicProvider(auth="sk-test-key")
        assert p.name == "anthropic"
        assert p._auth == "sk-test-key"

    def test_generate_no_api_key(self) -> None:
        p = AnthropicProvider(auth="")
        resp = p.generate("test")
        assert resp.text == ""
        assert "error" in resp.metadata
        assert "ANTHROPIC_API_KEY" in resp.metadata["error"]

    @patch("orxaq.orchestration.providers.anthropic.urllib.request.urlopen")
    def test_generate_success(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(ANTHROPIC_RESPONSE)
        p = AnthropicProvider(auth="sk-test")
        resp = p.generate("test prompt")

        assert resp.text == "hello from claude"
        assert resp.provider == "anthropic"
        assert resp.input_tokens == 15
        assert resp.output_tokens == 25
        assert resp.latency_ms > 0

    @patch("orxaq.orchestration.providers.anthropic.urllib.request.urlopen")
    def test_generate_multi_block_response(self, mock_urlopen: MagicMock) -> None:
        multi_block = {
            "content": [
                {"type": "text", "text": "part1 "},
                {"type": "text", "text": "part2"},
            ],
            "usage": {"input_tokens": 5, "output_tokens": 10},
        }
        mock_urlopen.return_value = _mock_response(multi_block)
        p = AnthropicProvider(auth="sk-test")
        resp = p.generate("test")
        assert resp.text == "part1 part2"

    @patch("orxaq.orchestration.providers.anthropic.urllib.request.urlopen")
    def test_generate_connection_error(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = URLError("Network unreachable")
        p = AnthropicProvider(auth="sk-test")
        resp = p.generate("test")
        assert resp.text == ""
        assert "error" in resp.metadata

    def test_health_check_no_key(self) -> None:
        p = AnthropicProvider(auth="")
        health = p.health_check()
        assert health["healthy"] is False
        assert "error" in health

    def test_health_check_with_key(self) -> None:
        p = AnthropicProvider(auth="sk-test")
        health = p.health_check()
        assert health["healthy"] is True
        assert health["provider"] == "anthropic"


# ── OpenAIProvider ────────────────────────────────────────────


class TestOpenAIProvider:
    def test_init_with_explicit_key(self) -> None:
        p = OpenAIProvider(auth="sk-test-key")
        assert p.name == "openai"
        assert p._auth == "sk-test-key"

    def test_generate_no_api_key(self) -> None:
        p = OpenAIProvider(auth="")
        resp = p.generate("test")
        assert resp.text == ""
        assert "error" in resp.metadata
        assert "OPENAI_API_KEY" in resp.metadata["error"]

    @patch("orxaq.orchestration.providers.openai.urllib.request.urlopen")
    def test_generate_success(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(OPENAI_RESPONSE)
        p = OpenAIProvider(auth="sk-test")
        resp = p.generate("test prompt")

        assert resp.text == "hello from openai"
        assert resp.provider == "openai"
        assert resp.input_tokens == 12
        assert resp.output_tokens == 18
        assert resp.latency_ms > 0

    @patch("orxaq.orchestration.providers.openai.urllib.request.urlopen")
    def test_generate_with_params(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.return_value = _mock_response(OPENAI_RESPONSE)
        p = OpenAIProvider(auth="sk-test")
        resp = p.generate("test", model="gpt-4o", max_tokens=2048, temperature=0.0)
        assert resp.model == "gpt-4o"

    @patch("orxaq.orchestration.providers.openai.urllib.request.urlopen")
    def test_generate_connection_error(self, mock_urlopen: MagicMock) -> None:
        mock_urlopen.side_effect = URLError("Connection timed out")
        p = OpenAIProvider(auth="sk-test")
        resp = p.generate("test")
        assert resp.text == ""
        assert "error" in resp.metadata

    def test_health_check_no_key(self) -> None:
        p = OpenAIProvider(auth="")
        health = p.health_check()
        assert health["healthy"] is False

    def test_health_check_with_key(self) -> None:
        p = OpenAIProvider(auth="sk-test")
        health = p.health_check()
        assert health["healthy"] is True
        assert "openai" == health["provider"]


# ── Base LLMProvider ──────────────────────────────────────────


class TestLLMProviderBase:
    def test_generate_raises_not_implemented(self) -> None:
        from orxaq.orchestration.providers.base import LLMProvider

        p = LLMProvider()
        with pytest.raises(NotImplementedError):
            p.generate("test")

    def test_health_check_default(self) -> None:
        from orxaq.orchestration.providers.base import LLMProvider

        p = LLMProvider()
        health = p.health_check()
        assert health["healthy"] is False

    def test_available_models_default(self) -> None:
        from orxaq.orchestration.providers.base import LLMProvider

        p = LLMProvider()
        assert p.available_models() == []

    def test_llm_response_frozen(self) -> None:
        resp = LLMResponse(text="hi", model="m", provider="p")
        with pytest.raises(AttributeError):
            resp.text = "changed"  # type: ignore[misc]
