"""Tests for MixedInferenceClient (#15), OllamaClient (#16), and TorchEnv (#43).

All tests use offline_mode=True so no actual servers are required.
"""

from __future__ import annotations

import pytest

from sagellm.mixed_client import MixedInferenceClient, MixedRequest, RequestKind
from sagellm.ollama_client import OllamaClient
from sagellm.torch_env import (
    TorchEnvInfo,
    TorchVariant,
    detect_torch_env,
    get_install_command,
    validate_torch_for_backend,
)

# =============================================================================
# Issue #15 — MixedInferenceClient
# =============================================================================


class TestMixedInferenceClient:
    @pytest.fixture
    def client(self) -> MixedInferenceClient:
        return MixedInferenceClient(offline_mode=True)

    def test_complete_returns_text(self, client: MixedInferenceClient) -> None:
        resp = client.complete("Hello!")
        assert "text" in resp
        assert isinstance(resp["text"], str)

    def test_embed_returns_vectors(self, client: MixedInferenceClient) -> None:
        vecs = client.embed("Hello world")
        assert isinstance(vecs, list)
        assert len(vecs) == 1
        assert all(isinstance(v, float) for v in vecs[0])

    def test_embed_batch(self, client: MixedInferenceClient) -> None:
        vecs = client.embed(["Hello", "World", "Foo"])
        assert len(vecs) == 3

    def test_health_returns_dict(self, client: MixedInferenceClient) -> None:
        h = client.health()
        assert "llm" in h
        assert "embedding" in h
        assert h["llm"] is True
        assert h["embedding"] is True

    def test_dispatch_llm_request(self, client: MixedInferenceClient) -> None:
        req = MixedRequest(kind=RequestKind.LLM, content="Tell me a joke")
        results = client.dispatch([req])
        assert len(results) == 1
        assert results[0].kind == RequestKind.LLM
        assert isinstance(results[0].text, str)

    def test_dispatch_embedding_request(self, client: MixedInferenceClient) -> None:
        req = MixedRequest(kind=RequestKind.EMBEDDING, content="hello world")
        results = client.dispatch([req])
        assert len(results) == 1
        assert results[0].kind == RequestKind.EMBEDDING
        assert isinstance(results[0].embedding, list)

    def test_dispatch_mixed_batch(self, client: MixedInferenceClient) -> None:
        reqs = [
            MixedRequest(kind=RequestKind.LLM, content="What is 2+2?", metadata={"id": 1}),
            MixedRequest(kind=RequestKind.EMBEDDING, content="Hello", metadata={"id": 2}),
            MixedRequest(kind=RequestKind.LLM, content="Explain Python", metadata={"id": 3}),
        ]
        results = client.dispatch(reqs)
        assert len(results) == 3
        assert results[0].kind == RequestKind.LLM
        assert results[1].kind == RequestKind.EMBEDDING
        assert results[2].kind == RequestKind.LLM
        # Metadata is preserved
        assert results[0].metadata == {"id": 1}
        assert results[1].metadata == {"id": 2}

    def test_repr(self, client: MixedInferenceClient) -> None:
        r = repr(client)
        assert "MixedInferenceClient" in r
        assert "offline=True" in r

    def test_chat_returns_response(self, client: MixedInferenceClient) -> None:
        resp = client.chat([{"role": "user", "content": "Hello"}])
        assert "text" in resp


# =============================================================================
# Issue #16 — OllamaClient
# =============================================================================


class TestOllamaClient:
    @pytest.fixture
    def client(self) -> OllamaClient:
        return OllamaClient(model="llama3", offline_mode=True)

    def test_health_returns_true(self, client: OllamaClient) -> None:
        assert client.health() is True

    def test_version_offline(self, client: OllamaClient) -> None:
        assert client.version() == "offline"

    def test_list_models_offline(self, client: OllamaClient) -> None:
        models = client.list_models()
        assert isinstance(models, list)
        assert "llama3" in models

    def test_complete_offline(self, client: OllamaClient) -> None:
        resp = client.complete("Hello")
        assert "text" in resp
        assert "ollama-offline" in resp["text"]

    def test_chat_offline(self, client: OllamaClient) -> None:
        resp = client.chat([{"role": "user", "content": "Hello"}])
        assert "text" in resp
        assert "ollama-offline" in resp["text"]

    def test_embed_offline(self, client: OllamaClient) -> None:
        vecs = client.embed("Hello world")
        assert isinstance(vecs, list)
        assert len(vecs) == 1

    def test_embed_batch_offline(self, client: OllamaClient) -> None:
        vecs = client.embed(["Hello", "World"])
        assert len(vecs) == 2

    def test_repr(self, client: OllamaClient) -> None:
        r = repr(client)
        assert "OllamaClient" in r
        assert "llama3" in r

    def test_base_url_property(self, client: OllamaClient) -> None:
        assert "localhost" in client.base_url


# =============================================================================
# Issue #43 — TorchEnv detection and validation
# =============================================================================


class TestTorchEnvDetection:
    def test_detect_returns_torch_env_info(self) -> None:
        info = detect_torch_env()
        assert isinstance(info, TorchEnvInfo)
        assert isinstance(info.variant, TorchVariant)

    def test_variant_is_a_known_value(self) -> None:
        info = detect_torch_env()
        allowed = set(TorchVariant)
        assert info.variant in allowed

    def test_device_count_non_negative(self) -> None:
        info = detect_torch_env()
        assert info.device_count >= 0

    def test_device_names_is_list(self) -> None:
        info = detect_torch_env()
        assert isinstance(info.device_names, list)

    def test_cpu_always_compatible(self) -> None:
        info = TorchEnvInfo(variant=TorchVariant.CPU)
        assert info.is_compatible_with("cpu") is True

    def test_cuda_variant_not_compatible_with_ascend(self) -> None:
        info = TorchEnvInfo(variant=TorchVariant.CUDA)
        assert info.is_compatible_with("ascend") is False

    def test_ascend_variant_compatible_with_ascend(self) -> None:
        info = TorchEnvInfo(variant=TorchVariant.ASCEND)
        assert info.is_compatible_with("ascend") is True

    def test_not_installed_not_gpu_available(self) -> None:
        info = TorchEnvInfo(variant=TorchVariant.NOT_INSTALLED)
        assert info.is_gpu_available() is False

    def test_cuda_with_devices_is_gpu_available(self) -> None:
        info = TorchEnvInfo(variant=TorchVariant.CUDA, device_count=2)
        assert info.is_gpu_available() is True


class TestValidateTorchForBackend:
    def test_cpu_always_passes(self) -> None:
        """CPU backend should never raise regardless of torch state."""
        # No exception expected
        validate_torch_for_backend("cpu")

    def test_cuda_raises_if_not_installed(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Requesting cuda when torch is not installed must fail-fast."""
        import sagellm.torch_env as te

        monkeypatch.setattr(
            te, "detect_torch_env", lambda: TorchEnvInfo(variant=TorchVariant.NOT_INSTALLED)
        )
        with pytest.raises(EnvironmentError, match="not installed"):
            te.validate_torch_for_backend("cuda")

    def test_cuda_raises_if_wrong_variant(self, monkeypatch: pytest.MonkeyPatch) -> None:
        import sagellm.torch_env as te

        monkeypatch.setattr(
            te,
            "detect_torch_env",
            lambda: TorchEnvInfo(variant=TorchVariant.ASCEND, torch_version="2.1.0"),
        )
        with pytest.raises(EnvironmentError, match="variant"):
            te.validate_torch_for_backend("cuda")

    def test_correct_variant_passes(self, monkeypatch: pytest.MonkeyPatch) -> None:
        import sagellm.torch_env as te

        monkeypatch.setattr(
            te,
            "detect_torch_env",
            lambda: TorchEnvInfo(variant=TorchVariant.CUDA, torch_version="2.1.0+cu121"),
        )
        # Should not raise
        te.validate_torch_for_backend("cuda")


class TestGetInstallCommand:
    @pytest.mark.parametrize(
        "backend",
        ["cpu", "cuda", "cuda11", "cuda12", "ascend", "kunlun", "haiguang", "muxi"],
    )
    def test_returns_non_empty_string(self, backend: str) -> None:
        cmd = get_install_command(backend)
        assert isinstance(cmd, str)
        assert len(cmd) > 0

    def test_unknown_backend_raises_value_error(self) -> None:
        with pytest.raises(ValueError, match="Unknown backend"):
            get_install_command("unknown_backend")

    def test_cuda_command_contains_pip(self) -> None:
        cmd = get_install_command("cuda")
        assert "pip install" in cmd
        assert "cu121" in cmd
