"""Tests for the device control routes (``/control/*``).

Verifies:
- Auth is required on all control endpoints
- ``GET /control/status`` returns device details
- ``POST /control/pull`` validates input and streams progress
- ``POST /control/restart`` attempts engine restart
"""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from httpx import ASGITransport, AsyncClient

from llmhosts.proxy.app import create_app
from llmhosts.proxy.auth import ProxyKeyStore
from llmhosts.proxy.dispatcher import BackendDispatcher

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def control_app(tmp_path):
    """App with a key store containing a test key."""
    application = create_app()

    # Set up a key store with a known test key
    key_store = ProxyKeyStore(data_dir=tmp_path)
    _, raw_key = key_store.create_key("test-device", plan="pro")
    application.state.key_store = key_store
    application.state.test_raw_key = raw_key

    return application


@pytest.fixture()
async def control_client(control_app):
    """Async HTTP client wired to the control app."""
    dispatcher = BackendDispatcher()
    await dispatcher.startup()
    control_app.state.dispatcher = dispatcher

    transport = ASGITransport(app=control_app, raise_app_exceptions=False)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        ac._raw_key = control_app.state.test_raw_key  # stash for easy access
        yield ac

    await dispatcher.shutdown()


# ---------------------------------------------------------------------------
# Auth tests
# ---------------------------------------------------------------------------


class TestControlAuth:
    """Verify auth is required on all control endpoints."""

    async def test_status_requires_auth(self, control_client):
        """GET /control/status with no auth returns 401."""
        resp = await control_client.get("/control/status")
        assert resp.status_code == 401

    async def test_pull_requires_auth(self, control_client):
        """POST /control/pull with no auth returns 401."""
        resp = await control_client.post("/control/pull", json={"model": "test:7b"})
        assert resp.status_code == 401

    async def test_restart_requires_auth(self, control_client):
        """POST /control/restart with no auth returns 401."""
        resp = await control_client.post("/control/restart")
        assert resp.status_code == 401

    async def test_status_rejects_invalid_key(self, control_client):
        """GET /control/status with invalid key returns 401."""
        resp = await control_client.get(
            "/control/status",
            headers={"Authorization": "Bearer lmh_invalid_key_12345"},
        )
        assert resp.status_code == 401

    async def test_status_accepts_valid_key(self, control_client):
        """GET /control/status with valid key returns 200."""
        resp = await control_client.get(
            "/control/status",
            headers={"Authorization": f"Bearer {control_client._raw_key}"},
        )
        assert resp.status_code == 200


# ---------------------------------------------------------------------------
# GET /control/status tests
# ---------------------------------------------------------------------------


class TestControlStatus:
    """Verify the /control/status endpoint."""

    async def test_status_returns_device_info(self, control_client):
        """Status returns loaded models and hardware summary."""
        resp = await control_client.get(
            "/control/status",
            headers={"Authorization": f"Bearer {control_client._raw_key}"},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "loaded_models" in data
        assert isinstance(data["loaded_models"], list)
        assert "hardware_summary" in data

    async def test_status_includes_uptime_when_start_time_set(self, control_app, control_client):
        """Status returns uptime when app.state.start_time is set."""
        import time

        control_app.state.start_time = time.time() - 120  # 2 minutes ago

        resp = await control_client.get(
            "/control/status",
            headers={"Authorization": f"Bearer {control_client._raw_key}"},
        )
        data = resp.json()
        # Uptime should be roughly 120 seconds (allow some tolerance)
        assert data["uptime_seconds"] >= 119


# ---------------------------------------------------------------------------
# POST /control/pull tests
# ---------------------------------------------------------------------------


class TestControlPull:
    """Verify the /control/pull endpoint."""

    async def test_pull_rejects_empty_model(self, control_client):
        """POST /control/pull with empty model returns 400."""
        resp = await control_client.post(
            "/control/pull",
            json={"model": "  "},
            headers={"Authorization": f"Bearer {control_client._raw_key}"},
        )
        assert resp.status_code == 400

    async def test_pull_rejects_missing_model(self, control_client):
        """POST /control/pull with no model field returns 422."""
        resp = await control_client.post(
            "/control/pull",
            json={},
            headers={"Authorization": f"Bearer {control_client._raw_key}"},
        )
        assert resp.status_code == 422

    @patch("llmhosts.proxy.control_routes._get_engine_url", return_value="http://127.0.0.1:11434")
    async def test_pull_streams_progress(self, mock_engine_url, control_client):
        """POST /control/pull streams ndjson progress lines."""
        # Mock the httpx client to return fake pull progress
        mock_stream = AsyncMock()
        mock_stream.status_code = 200

        pull_lines = [
            '{"status":"downloading","total":4000000000,"completed":1000000000}',
            '{"status":"downloading","total":4000000000,"completed":4000000000}',
            '{"status":"success"}',
        ]

        async def fake_aiter_lines():
            for line in pull_lines:
                yield line

        mock_stream.aiter_lines = fake_aiter_lines
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=False)

        mock_client_instance = AsyncMock()
        mock_client_instance.stream = MagicMock(return_value=mock_stream)
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client_instance)
        mock_client_instance.__aexit__ = AsyncMock(return_value=False)

        with patch("httpx.AsyncClient", return_value=mock_client_instance):
            resp = await control_client.post(
                "/control/pull",
                json={"model": "llama3.1:8b"},
                headers={"Authorization": f"Bearer {control_client._raw_key}"},
            )

        assert resp.status_code == 200
        assert resp.headers.get("content-type", "").startswith("application/x-ndjson")

        # Parse the streamed response lines
        lines = [line for line in resp.text.strip().split("\n") if line.strip()]
        assert len(lines) >= 1  # At least the streamed progress lines
        for line in lines:
            data = json.loads(line)
            assert "status" in data


# ---------------------------------------------------------------------------
# POST /control/restart tests
# ---------------------------------------------------------------------------


class TestControlRestart:
    """Verify the /control/restart endpoint."""

    @patch("llmhosts.proxy.control_routes._get_engine_url", return_value="http://127.0.0.1:11434")
    @patch("llmhosts.proxy.control_routes._restart_engine_service", return_value=True)
    @patch("llmhosts.proxy.control_routes._wait_for_engine", return_value=True)
    async def test_restart_success(self, mock_wait, mock_restart, mock_url, control_client):
        """POST /control/restart returns ok when engine restarts successfully."""
        # Mock the health check
        mock_health_resp = MagicMock()
        mock_health_resp.status_code = 200

        mock_http = AsyncMock()
        mock_http.get = AsyncMock(return_value=mock_health_resp)
        mock_http.__aenter__ = AsyncMock(return_value=mock_http)
        mock_http.__aexit__ = AsyncMock(return_value=False)

        with patch("httpx.AsyncClient", return_value=mock_http):
            resp = await control_client.post(
                "/control/restart",
                headers={"Authorization": f"Bearer {control_client._raw_key}"},
            )

        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert "restarted successfully" in data["message"]

    @patch("llmhosts.proxy.control_routes._get_engine_url", return_value="http://127.0.0.1:11434")
    @patch("llmhosts.proxy.control_routes._restart_engine_service", return_value=False)
    async def test_restart_failure(self, mock_restart, mock_url, control_client):
        """POST /control/restart returns error when restart fails."""
        mock_health_resp = MagicMock()
        mock_health_resp.status_code = 200

        mock_http = AsyncMock()
        mock_http.get = AsyncMock(return_value=mock_health_resp)
        mock_http.__aenter__ = AsyncMock(return_value=mock_http)
        mock_http.__aexit__ = AsyncMock(return_value=False)

        with patch("httpx.AsyncClient", return_value=mock_http):
            resp = await control_client.post(
                "/control/restart",
                headers={"Authorization": f"Bearer {control_client._raw_key}"},
            )

        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "error"
        assert "Manual restart" in data["message"]
