"""CPU backend validation tests for engine behavior (Async API).

These tests validate CPUEngine behavior using a tiny model.
All tests use CPU backend for validation (no GPU required).
"""

from __future__ import annotations

import pytest

from sagellm_core import CPUEngine, CPUEngineConfig
from sagellm_protocol import Request, Response


@pytest.mark.slow
@pytest.mark.asyncio
class TestCPUEngineBehavior:
    """Validate CPUEngine behavior on CPU backend."""

    async def test_cpu_engine_has_required_methods(self, cpu_engine) -> None:
        """CPUEngine must expose all required methods."""
        required_methods = [
            "start",
            "stop",
            "execute",
            "stream",
            "health_check",
            "is_available",
            "backend_type",
            "priority",
        ]

        for method in required_methods:
            assert hasattr(cpu_engine, method), f"CPUEngine missing {method}"
            assert callable(getattr(cpu_engine, method)), f"{method} must be callable"

    async def test_cpu_engine_health_check(self, cpu_engine) -> None:
        """CPUEngine health_check returns bool and is healthy when running."""
        health = await cpu_engine.health_check()
        assert isinstance(health, bool)
        assert health is True

    async def test_cpu_engine_backend_type(self, cpu_engine) -> None:
        """CPUEngine returns correct backend type."""
        assert cpu_engine.backend_type() == "cpu"

    async def test_cpu_engine_is_available(self, cpu_engine) -> None:
        """CPUEngine is_available returns bool."""
        assert isinstance(cpu_engine.is_available(), bool)

    async def test_cpu_engine_priority(self, cpu_engine) -> None:
        """CPUEngine priority returns int."""
        assert isinstance(cpu_engine.priority(), int)

    async def test_cpu_engine_execute_returns_response(self, cpu_engine) -> None:
        """CPUEngine execute returns Response."""
        request = Request(
            request_id="cpu-001",
            trace_id="cpu-trace",
            model="sshleifer/tiny-gpt2",
            prompt="Test prompt",
            max_tokens=16,
            stream=False,
        )

        response = await cpu_engine.execute(request)

        assert isinstance(response, Response)
        assert response.request_id == "cpu-001"
        assert response.output_text is not None

    async def test_cpu_engine_stream_yields_events(self, cpu_engine) -> None:
        """CPUEngine stream yields events."""
        request = Request(
            request_id="cpu-stream-001",
            trace_id="cpu-trace",
            model="sshleifer/tiny-gpt2",
            prompt="Stream test",
            max_tokens=16,
            stream=True,
        )

        events = []
        async for event in cpu_engine.stream(request):
            events.append(event)

        assert len(events) >= 2
        assert events[0].event == "start"
        assert events[-1].event == "end"

    async def test_cpu_execute_before_start_fails(self, tiny_model) -> None:
        """CPUEngine must fail when execute() called before start()."""
        config = CPUEngineConfig(
            engine_id="cpu-error",
            model_path=tiny_model,
            max_new_tokens=16,
        )
        engine = CPUEngine(config)

        request = Request(
            request_id="error-001",
            trace_id="error-trace",
            model=tiny_model,
            prompt="Should fail",
            max_tokens=16,
            stream=False,
        )

        with pytest.raises(RuntimeError, match="not running|not started"):
            await engine.execute(request)

    async def test_cpu_stream_before_start_fails(self, tiny_model) -> None:
        """CPUEngine must fail when stream() called before start()."""
        config = CPUEngineConfig(
            engine_id="cpu-stream-error",
            model_path=tiny_model,
            max_new_tokens=16,
        )
        engine = CPUEngine(config)

        request = Request(
            request_id="error-002",
            trace_id="error-trace",
            model=tiny_model,
            prompt="Should fail",
            max_tokens=16,
            stream=True,
        )

        with pytest.raises(RuntimeError, match="not running|not started"):
            async for _ in engine.stream(request):
                pass

    async def test_cpu_start_stop_cycle(self, tiny_model) -> None:
        """CPUEngine must support start -> stop cycle."""
        config = CPUEngineConfig(
            engine_id="cpu-cycle",
            model_path=tiny_model,
            max_new_tokens=16,
        )
        engine = CPUEngine(config)

        assert not engine.is_running
        await engine.start()
        assert engine.is_running

        await engine.stop()
        assert not engine.is_running


class TestEngineErrorHandlingParity:
    """Test error handling parity between CPU engines."""

    pass
