"""
Tests for the Brynq Runtime local API server and cloud client.

Tests are structured in three groups:
    1. Configuration — RuntimeConfig defaults and env overrides
    2. Cloud client  — Request/response handling, retries, error mapping
    3. API endpoints — Full endpoint integration via TestClient

All external calls (Ollama, cloud, LLM APIs) are mocked.
No network access, no disk writes outside tmp_path.
"""

from __future__ import annotations

import json
import time
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

from runtime.config import RuntimeConfig, load_config


# ═══════════════════════════════════════════════════════════════════════
# 1. Configuration
# ═══════════════════════════════════════════════════════════════════════


class TestRuntimeConfig:
    """Tests for runtime/config.py."""

    def test_defaults(self) -> None:
        cfg = RuntimeConfig()
        assert cfg.cloud_url == "https://api.brynq.ai"
        assert cfg.local_port == 8003
        assert cfg.ollama_url == "http://localhost:11434"
        assert cfg.data_dir == Path.home() / ".brynq"
        assert cfg.plan_verify_key == ""
        assert cfg.cloud_timeout == 30.0
        assert cfg.ollama_timeout == 120.0
        assert cfg.debug is False

    def test_custom_values(self, tmp_path: Path) -> None:
        cfg = RuntimeConfig(
            cloud_url="https://custom.api.com",
            local_port=9999,
            data_dir=tmp_path,
            debug=True,
        )
        assert cfg.cloud_url == "https://custom.api.com"
        assert cfg.local_port == 9999
        assert cfg.data_dir == tmp_path
        assert cfg.debug is True

    def test_frozen(self) -> None:
        cfg = RuntimeConfig()
        with pytest.raises(AttributeError):
            cfg.local_port = 1234  # type: ignore[misc]

    def test_load_from_env(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        monkeypatch.setenv("BRYNQ_RUNTIME_CLOUD_URL", "https://test.brynq.ai")
        monkeypatch.setenv("BRYNQ_RUNTIME_LOCAL_PORT", "7777")
        monkeypatch.setenv("BRYNQ_RUNTIME_DATA_DIR", str(tmp_path))
        monkeypatch.setenv("BRYNQ_RUNTIME_DEBUG", "1")

        cfg = load_config()
        assert cfg.cloud_url == "https://test.brynq.ai"
        assert cfg.local_port == 7777
        assert cfg.data_dir == tmp_path
        assert cfg.debug is True

    def test_trailing_slash_stripped(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("BRYNQ_RUNTIME_CLOUD_URL", "https://api.brynq.ai/")
        monkeypatch.setenv("BRYNQ_RUNTIME_OLLAMA_URL", "http://localhost:11434/")
        cfg = load_config()
        assert not cfg.cloud_url.endswith("/")
        assert not cfg.ollama_url.endswith("/")


# ═══════════════════════════════════════════════════════════════════════
# 2. Cloud Client
# ═══════════════════════════════════════════════════════════════════════


class TestCloudClient:
    """Tests for runtime/cloud_client.py."""

    @pytest.fixture()
    def client(self, tmp_path: Path) -> "CloudClient":
        from runtime.cloud_client import CloudClient

        cfg = RuntimeConfig(
            cloud_url="https://mock.brynq.ai",
            data_dir=tmp_path,
            cloud_timeout=5.0,
        )
        return CloudClient(cfg)

    def test_not_authenticated_by_default(self, client: Any) -> None:
        assert client.is_authenticated is False

    def test_set_token(self, client: Any) -> None:
        client.set_token("test-jwt-token")
        assert client.is_authenticated is True

    def test_health_check_returns_false_on_unreachable(self, client: Any) -> None:
        # No mock server running — should return False
        assert client.health_check() is False

    def test_health_check_returns_true(self, client: Any) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = '{"status": "ok"}'
        mock_resp.json.return_value = {"status": "ok"}
        client._session.request = MagicMock(return_value=mock_resp)

        assert client.health_check() is True

    def test_get_plan_success(self, client: Any) -> None:
        plan_data = {
            "plan_id": "abc-123",
            "steps": [{"step_id": "s1", "type": "local", "model": "llama3"}],
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = json.dumps(plan_data)
        mock_resp.json.return_value = plan_data
        client._session.request = MagicMock(return_value=mock_resp)

        result = client.get_plan(
            request="test request",
            available_models=["ollama:llama3"],
        )
        assert result.plan_id == "abc-123"
        assert result.plan == plan_data

    def test_report_results(self, client: Any) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = '{"acknowledged": true}'
        mock_resp.json.return_value = {"acknowledged": True}
        client._session.request = MagicMock(return_value=mock_resp)

        result = client.report_results(
            plan_id="abc-123",
            steps=[{"step_id": "s1", "duration_ms": 100, "token_count": 50, "success": True}],
            total_duration_ms=100,
            success=True,
        )
        assert result.get("acknowledged") is True

    def test_get_plan_cloud_unavailable(self, client: Any) -> None:
        from runtime.cloud_client import CloudUnavailable

        # No mock — real connection will fail
        with pytest.raises(CloudUnavailable):
            client.get_plan(request="test", available_models=["llama3"])

    def test_4xx_raises_cloud_rejected(self, client: Any) -> None:
        from runtime.cloud_client import CloudRejected

        mock_resp = MagicMock()
        mock_resp.status_code = 400
        mock_resp.text = '{"error": "bad request"}'
        client._session.request = MagicMock(return_value=mock_resp)

        with pytest.raises(CloudRejected) as exc_info:
            client.get_plan(request="test", available_models=[])
        assert exc_info.value.status_code == 400

    def test_list_marketplace_agents(self, client: Any) -> None:
        agents_data = {
            "agents": [
                {
                    "agent_id": "agent-1",
                    "name": "Test Agent",
                    "description": "A test agent",
                    "category": "testing",
                    "type": "remote",
                    "version": "1.0",
                    "rating": 4.5,
                    "installs": 100,
                },
            ]
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.text = json.dumps(agents_data)
        mock_resp.json.return_value = agents_data
        client._session.request = MagicMock(return_value=mock_resp)

        agents = client.list_marketplace_agents()
        assert len(agents) == 1
        assert agents[0].agent_id == "agent-1"
        assert agents[0].rating == 4.5


# ═══════════════════════════════════════════════════════════════════════
# 3. API Endpoints
# ═══════════════════════════════════════════════════════════════════════


@pytest.fixture()
def runtime_config(tmp_path: Path) -> RuntimeConfig:
    """Create a test config with tmp_path as data directory."""
    return RuntimeConfig(
        cloud_url="https://mock.brynq.ai",
        data_dir=tmp_path,
        ollama_url="http://localhost:11434",
        debug=True,
    )


@pytest.fixture()
def test_client(runtime_config: RuntimeConfig) -> Any:
    """Create a FastAPI TestClient for the runtime app."""
    from fastapi.testclient import TestClient

    from runtime.app import create_app

    app = create_app(runtime_config)
    return TestClient(app)


class TestStatusEndpoint:
    """Tests for GET /status."""

    @patch("runtime.app._ollama_is_running", return_value=False)
    def test_status_ollama_down(self, _mock: MagicMock, test_client: Any) -> None:
        with patch.object(
            test_client.app.state.cloud, "health_check", return_value=False,
        ):
            resp = test_client.get("/status")

        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert data["ollama"]["connected"] is False
        assert data["cloud"]["connected"] is False
        assert data["version"] == "0.1.0"

    @patch("runtime.app._ollama_is_running", return_value=True)
    @patch("runtime.app._ollama_list_models", return_value=[{"name": "llama3", "size": 4_000_000_000}])
    def test_status_ollama_up(self, _m1: MagicMock, _m2: MagicMock, test_client: Any) -> None:
        with patch.object(
            test_client.app.state.cloud, "health_check", return_value=True,
        ):
            resp = test_client.get("/status")

        data = resp.json()
        assert data["ollama"]["connected"] is True
        assert data["ollama"]["model_count"] == 1
        assert data["cloud"]["connected"] is True


class TestModelsEndpoint:
    """Tests for GET /models."""

    @patch("shutil.which", return_value=None)
    @patch("runtime.app._ollama_list_models", return_value=[])
    def test_no_models(self, _mock_ollama: MagicMock, _mock_which: MagicMock, test_client: Any) -> None:
        resp = test_client.get("/models")
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 0
        assert data["models"] == []

    @patch("shutil.which", return_value=None)
    @patch("runtime.app._ollama_list_models")
    def test_local_models(self, mock_list: MagicMock, _mock_which: MagicMock, test_client: Any) -> None:
        mock_list.return_value = [
            {"name": "llama3", "size": 4_000_000_000, "details": {"family": "llama", "quantization_level": "Q4_0"}},
            {"name": "mistral", "size": 3_000_000_000, "details": {}},
        ]
        resp = test_client.get("/models")
        data = resp.json()
        assert data["count"] == 2
        assert data["models"][0]["id"] == "ollama:llama3"
        assert data["models"][0]["type"] == "local"
        assert data["models"][0]["provider"] == "ollama"

    @patch("runtime.app._ollama_list_models", return_value=[])
    def test_cloud_models_with_keys(
        self, _mock: MagicMock, test_client: Any, runtime_config: RuntimeConfig,
    ) -> None:
        # Store an anthropic key
        from runtime.app import _write_keys

        _write_keys(runtime_config.data_dir, {"anthropic": "sk-test-123"})

        resp = test_client.get("/models")
        data = resp.json()
        # Should list Anthropic cloud models
        assert data["count"] > 0
        providers = {m["provider"] for m in data["models"]}
        assert "anthropic" in providers
        assert all(m["type"] == "cloud" for m in data["models"])


class TestKeysEndpoints:
    """Tests for POST/GET/DELETE /keys."""

    def test_store_and_list_key(self, test_client: Any) -> None:
        # Store
        resp = test_client.post("/keys", json={"provider": "anthropic", "api_key": "sk-ant-test1234567890"})
        assert resp.status_code == 200
        data = resp.json()
        assert data["stored"] is True
        assert data["provider"] == "anthropic"
        assert "..." in data["masked_key"]
        assert "sk-a" in data["masked_key"]  # first 4 chars visible

        # List
        resp = test_client.get("/keys")
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] == 1
        assert data["keys"][0]["provider"] == "anthropic"
        # Full key is NOT exposed
        assert "sk-ant-test1234567890" not in json.dumps(data)

    def test_delete_key(self, test_client: Any) -> None:
        # Store first
        test_client.post("/keys", json={"provider": "openai", "api_key": "sk-openai-test"})
        # Delete
        resp = test_client.delete("/keys/openai")
        assert resp.status_code == 200
        assert resp.json()["deleted"] is True

        # Verify gone
        resp = test_client.get("/keys")
        assert resp.json()["count"] == 0

    def test_delete_nonexistent_key(self, test_client: Any) -> None:
        resp = test_client.delete("/keys/openai")
        assert resp.status_code == 200
        assert resp.json()["status"] == "not_found"
        assert resp.json()["provider"] == "openai"

    def test_store_invalid_provider(self, test_client: Any) -> None:
        resp = test_client.post("/keys", json={"provider": "invalid_provider", "api_key": "key"})
        assert resp.status_code == 400
        assert "Unknown provider" in resp.json()["detail"]

    def test_store_missing_fields(self, test_client: Any) -> None:
        resp = test_client.post("/keys", json={"provider": "openai"})
        assert resp.status_code == 400

        resp = test_client.post("/keys", json={"api_key": "key"})
        assert resp.status_code == 400

    def test_multiple_providers(self, test_client: Any) -> None:
        test_client.post("/keys", json={"provider": "anthropic", "api_key": "sk-ant-xxx"})
        test_client.post("/keys", json={"provider": "openai", "api_key": "sk-oai-xxx"})
        test_client.post("/keys", json={"provider": "google", "api_key": "AIza-xxx"})

        resp = test_client.get("/keys")
        assert resp.json()["count"] == 3


class TestChatEndpoint:
    """Tests for POST /chat."""

    def test_empty_message_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": ""})
        assert resp.status_code == 400

    def test_missing_message_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={})
        assert resp.status_code == 400

    def test_non_string_message_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": 12345})
        assert resp.status_code == 400
        assert "message must be a string" in resp.json()["detail"]

    def test_message_over_100k_chars_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": "a" * 100_001})
        assert resp.status_code == 400
        assert "100,000" in resp.json()["detail"]

    def test_temperature_too_high_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": "hello", "temperature": 3.0})
        assert resp.status_code == 400
        assert "temperature" in resp.json()["detail"]

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_temperature_within_range_accepted(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Hi", "model": "llama3", "tokens": 5, "duration_ms": 50,
        }
        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.post("/chat", json={"message": "hello", "temperature": 1.5})
        assert resp.status_code == 200

    @patch("shutil.which", return_value=None)
    @patch("runtime.app._ollama_list_models", return_value=[])
    def test_no_models_available(self, _mock_ollama: MagicMock, _mock_which: MagicMock, test_client: Any) -> None:
        resp = test_client.post("/chat", json={"message": "hello"})
        assert resp.status_code == 503
        assert "No models available" in resp.json()["detail"]

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._ollama_chat")
    def test_local_fallback_on_cloud_failure(
        self, mock_chat: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When cloud is down, /chat falls back to direct Ollama call."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_chat.return_value = {
            "content": "Hello from Llama!",
            "model": "llama3",
            "tokens": 15,
            "duration_ms": 200,
        }

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("Connection refused"),
        ):
            resp = test_client.post("/chat", json={"message": "hello"})

        assert resp.status_code == 200
        data = resp.json()
        assert data["content"] == "Hello from Llama!"
        assert data["source"] == "local_fallback"
        assert data["model"] == "llama3"

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._execute_plan_steps")
    def test_chat_with_plan(
        self, mock_exec: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        """When cloud returns a plan, /chat executes it."""
        from runtime.cloud_client import PlanResponse

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_exec.return_value = {
            "outputs": [{"step_id": "s1", "content": "Plan result", "model": "llama3", "success": True, "error": None}],
            "metadata": [{"step_id": "s1", "duration_ms": 150, "token_count": 20, "success": True}],
        }

        mock_plan = PlanResponse(
            plan={"plan_id": "test-plan", "steps": [{"step_id": "s1"}]},
            plan_id="test-plan",
            raw={},
        )
        with patch.object(test_client.app.state.cloud, "get_plan", return_value=mock_plan):
            with patch.object(test_client.app.state.cloud, "report_results", return_value={}):
                resp = test_client.post("/chat", json={"message": "hello"})

        assert resp.status_code == 200
        data = resp.json()
        assert data["content"] == "Plan result"
        assert data["source"] == "plan"
        assert data["steps"] == 1


class TestChainEndpoint:
    """Tests for POST /chain."""

    def test_empty_request_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chain", json={"request": ""})
        assert resp.status_code == 400

    def test_missing_prompt_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chain", json={})
        assert resp.status_code == 400
        assert "prompt" in resp.json()["detail"]

    def test_empty_prompt_rejected(self, test_client: Any) -> None:
        resp = test_client.post("/chain", json={"prompt": ""})
        assert resp.status_code == 400
        assert "prompt" in resp.json()["detail"]

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._execute_plan_steps")
    def test_valid_prompt_with_models_accepted(
        self, mock_exec: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        from runtime.cloud_client import PlanResponse

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_exec.return_value = {
            "outputs": [
                {"step_id": "s1", "content": "Result", "model": "llama3", "success": True, "error": None},
            ],
            "metadata": [
                {"step_id": "s1", "duration_ms": 100, "token_count": 30, "success": True},
            ],
        }

        mock_plan = PlanResponse(
            plan={"plan_id": "chain-valid", "steps": [{"step_id": "s1"}]},
            plan_id="chain-valid",
            raw={},
        )
        with patch.object(test_client.app.state.cloud, "get_plan", return_value=mock_plan):
            with patch.object(test_client.app.state.cloud, "report_results", return_value={}):
                resp = test_client.post("/chain", json={
                    "prompt": "analyze this data",
                    "models": ["ollama:llama3", "claude-sonnet"],
                })

        assert resp.status_code == 200
        data = resp.json()
        assert data["plan_id"] == "chain-valid"
        assert data["success"] is True

    @patch("runtime.app._ollama_list_models", return_value=[])
    def test_no_models_available(self, _mock: MagicMock, test_client: Any) -> None:
        resp = test_client.post("/chain", json={"request": "analyze this"})
        assert resp.status_code == 503

    @patch("runtime.app._ollama_list_models")
    def test_chain_requires_cloud(self, mock_list: MagicMock, test_client: Any) -> None:
        """Chains always require the cloud — no local fallback."""
        from runtime.cloud_client import CloudUnavailable

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]

        with patch.object(
            test_client.app.state.cloud, "get_plan",
            side_effect=CloudUnavailable("Connection refused"),
        ):
            resp = test_client.post("/chain", json={"request": "analyze"})

        assert resp.status_code == 503
        assert "Cloud unavailable" in resp.json()["detail"]

    @patch("runtime.app._ollama_list_models")
    @patch("runtime.app._execute_plan_steps")
    def test_chain_success(
        self, mock_exec: MagicMock, mock_list: MagicMock, test_client: Any,
    ) -> None:
        from runtime.cloud_client import PlanResponse

        mock_list.return_value = [{"name": "llama3", "size": 4_000_000_000}]
        mock_exec.return_value = {
            "outputs": [
                {"step_id": "s1", "content": "Analysis", "model": "claude", "success": True, "error": None},
                {"step_id": "s2", "content": "Summary", "model": "llama3", "success": True, "error": None},
            ],
            "metadata": [
                {"step_id": "s1", "duration_ms": 300, "token_count": 100, "success": True},
                {"step_id": "s2", "duration_ms": 150, "token_count": 50, "success": True},
            ],
        }

        mock_plan = PlanResponse(
            plan={"plan_id": "chain-1", "steps": [{"step_id": "s1"}, {"step_id": "s2"}]},
            plan_id="chain-1",
            raw={},
        )
        with patch.object(test_client.app.state.cloud, "get_plan", return_value=mock_plan):
            with patch.object(test_client.app.state.cloud, "report_results", return_value={}):
                resp = test_client.post("/chain", json={"request": "analyze and summarize"})

        assert resp.status_code == 200
        data = resp.json()
        assert data["plan_id"] == "chain-1"
        assert len(data["steps"]) == 2
        assert data["total_duration_ms"] == 450
        assert data["success"] is True


class TestAgentsEndpoint:
    """Tests for GET /agents."""

    def test_agents_empty(self, test_client: Any) -> None:
        with patch.object(
            test_client.app.state.cloud, "list_marketplace_agents",
            return_value=[],
        ):
            resp = test_client.get("/agents")

        assert resp.status_code == 200
        data = resp.json()
        assert data["agents"] == []
        assert data["count"] == 0

    def test_agents_cloud_unavailable(self, test_client: Any) -> None:
        """Agents endpoint still works when cloud is down — just no marketplace agents."""
        from runtime.cloud_client import CloudUnavailable

        with patch.object(
            test_client.app.state.cloud, "list_marketplace_agents",
            side_effect=CloudUnavailable("down"),
        ):
            resp = test_client.get("/agents")

        assert resp.status_code == 200
        assert resp.json()["count"] == 0


class TestPlanExecution:
    """Tests for the _execute_plan_steps internal function."""

    @pytest.fixture()
    def config(self, tmp_path: Path) -> RuntimeConfig:
        return RuntimeConfig(data_dir=tmp_path)

    @patch("runtime.app._ollama_chat")
    def test_single_local_step(self, mock_chat: MagicMock, config: RuntimeConfig) -> None:
        from runtime.app import _execute_plan_steps

        mock_chat.return_value = {
            "content": "Hello world",
            "model": "llama3",
            "tokens": 10,
            "duration_ms": 100,
        }

        plan = {
            "original_input": "say hello",
            "steps": [{
                "step_id": "s1",
                "type": "local",
                "backend": "ollama",
                "model": "llama3",
                "prompt": "Say hello",
                "context_refs": [],
                "config": {},
            }],
        }

        result = _execute_plan_steps(plan, config, {})
        assert len(result["outputs"]) == 1
        assert result["outputs"][0]["content"] == "Hello world"
        assert result["outputs"][0]["success"] is True
        assert len(result["metadata"]) == 1
        assert result["metadata"][0]["token_count"] == 10

    @patch("runtime.app._ollama_chat")
    def test_context_resolution(self, mock_chat: MagicMock, config: RuntimeConfig) -> None:
        """Context refs from earlier steps are resolved and prepended."""
        from runtime.app import _execute_plan_steps

        call_count = 0

        def fake_chat(url, model, prompt, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return {"content": "Step 1 output", "model": model, "tokens": 5, "duration_ms": 50}
            else:
                # Second call should include step 1 output in the prompt
                assert "Step 1 output" in prompt
                return {"content": "Step 2 output", "model": model, "tokens": 8, "duration_ms": 80}

        mock_chat.side_effect = fake_chat

        plan = {
            "original_input": "test input",
            "steps": [
                {
                    "step_id": "s1", "type": "local", "backend": "ollama",
                    "model": "llama3", "prompt": "Do step 1",
                    "context_refs": ["original_input"], "config": {},
                },
                {
                    "step_id": "s2", "type": "local", "backend": "ollama",
                    "model": "llama3", "prompt": "Do step 2",
                    "context_refs": ["s1"], "config": {},
                },
            ],
        }

        result = _execute_plan_steps(plan, config, {})
        assert len(result["outputs"]) == 2
        assert result["outputs"][1]["content"] == "Step 2 output"

    def test_unknown_step_type(self, config: RuntimeConfig) -> None:
        from runtime.app import _execute_plan_steps

        plan = {
            "original_input": "",
            "steps": [{
                "step_id": "s1", "type": "quantum", "backend": "qpu",
                "model": "q1", "prompt": "entangle", "context_refs": [], "config": {},
            }],
        }

        result = _execute_plan_steps(plan, config, {})
        assert result["outputs"][0]["success"] is False
        assert "Unknown step type" in (result["outputs"][0]["error"] or "")


class TestKeyMasking:
    """Tests for the _mask_key helper."""

    def test_long_key(self) -> None:
        from runtime.app import _mask_key

        masked = _mask_key("sk-ant-1234567890abcdef")
        assert masked.startswith("sk-a")
        assert masked.endswith("cdef")
        assert "..." in masked
        # Original key NOT in masked output
        assert "1234567890" not in masked

    def test_short_key(self) -> None:
        from runtime.app import _mask_key

        masked = _mask_key("short")
        assert masked.startswith("shor")
        assert "..." in masked


class TestOllamaHelpers:
    """Tests for Ollama integration helpers."""

    @patch("runtime.app.urllib.request.urlopen")
    def test_ollama_list_models_success(self, mock_urlopen: MagicMock) -> None:
        from runtime.app import _ollama_list_models

        mock_resp = MagicMock()
        mock_resp.read.return_value = json.dumps({
            "models": [
                {"name": "llama3", "size": 4_000_000_000},
                {"name": "codellama", "size": 3_000_000_000},
            ]
        }).encode("utf-8")
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        mock_urlopen.return_value = mock_resp

        models = _ollama_list_models("http://localhost:11434")
        assert len(models) == 2
        assert models[0]["name"] == "llama3"

    def test_ollama_list_models_unreachable(self) -> None:
        from runtime.app import _ollama_list_models

        # No mock — will fail to connect and return empty list
        models = _ollama_list_models("http://localhost:99999", timeout=0.1)
        assert models == []

    def test_ollama_is_running_false(self) -> None:
        from runtime.app import _ollama_is_running

        assert _ollama_is_running("http://localhost:99999") is False


class TestAgentsEndpoints:
    def test_install_agent_creates_manifest(self, test_client, tmp_path):
        import runtime.app
        runtime.app._DATA_DIR = tmp_path
        res = test_client.post('/agents/install', json={'agent_id': 'test-agent', 'type': 'mcp'})
        assert res.status_code == 200
        assert (tmp_path / 'agents' / 'test-agent' / 'manifest.json').exists()

    def test_install_agent_missing_id(self, test_client):
        res = test_client.post('/agents/install', json={'type': 'mcp'})
        assert res.status_code == 400

    def test_get_installed_empty(self, test_client, tmp_path):
        import runtime.app
        runtime.app._DATA_DIR = tmp_path
        res = test_client.get('/agents/installed')
        assert res.status_code == 200
        assert res.json() == []

    def test_get_installed_after_install(self, test_client, tmp_path):
        import runtime.app
        runtime.app._DATA_DIR = tmp_path
        test_client.post('/agents/install', json={'agent_id': 'test-agent-2', 'type': 'mcp'})
        res = test_client.get('/agents/installed')
        assert res.status_code == 200
        assert len(res.json()) == 1
        assert res.json()[0]['id'] == 'test-agent-2'

    def test_uninstall_removes_directory(self, test_client, tmp_path):
        import runtime.app
        runtime.app._DATA_DIR = tmp_path
        test_client.post('/agents/install', json={'agent_id': 'test-agent-3', 'type': 'mcp'})
        res = test_client.delete('/agents/test-agent-3')
        assert res.status_code == 200
        assert not (tmp_path / 'agents' / 'test-agent-3').exists()

    def test_uninstall_nonexistent(self, test_client, tmp_path):
        import runtime.app
        runtime.app._DATA_DIR = tmp_path
        res = test_client.delete('/agents/nonexistent')
        assert res.status_code == 404
