"""E2E API Coverage Tests — endpoints not covered by test_basic_api.py.

Covers:
  1. /api/v1/experts          — 专家模板列表/详情
  2. /api/v1/workflows        — 工作流列表/详情
  3. /api/v1/config           — 配置同步 (version/all/skills/agents/workflows)
  4. /api/v1/system/resources — 系统资源监控
  5. /api/v1/evolution        — 进化事件 (store 未配置时 503)
  6. /api/v1/evolution-dashboard — 进化看板 (metrics/experiences/usage/pitfalls)
  7. /api/v1/settings         — 设置 (GET 各类配置；PUT 需 SYSTEM_CONFIG → 403)
  8. /api/v1/auth             — 认证 (login 无效凭据 401；whoami 无 bearer 401)
  9. /api/v1/memory           — 记忆 (retriever 未配置时 503)

每个端点至少 2 个测试：Happy path + Auth/Error path。
api_client fixture 已携带 X-API-Key（operator 角色），所以主要测试 happy path；
对需要更高权限的端点验证 403，对未配置后端的端点验证 503/空数据。
"""

import os
import subprocess
import sys
import time
from typing import Generator

import httpx
import pytest
import yaml

pytestmark = pytest.mark.e2e_basic


# ---------------------------------------------------------------------------
# Local e2e_server fixture — overrides conftest's version for this module.
#
# The conftest has two bugs that prevent the server from starting:
#   1. Uses `python -m agentkit.cli.main` — but main.py has no
#      `if __name__ == "__main__": app()` block, so the module imports
#      silently and exits without invoking the Typer app.
#   2. Even with the correct entry point (`python -m agentkit`), the CLI's
#      `serve` command calls `Confirm.ask` for onboarding when
#      `has_llm_provider()` is False (mock provider has no api_key),
#      blocking forever in a subprocess with no TTY.
#
# This fixture bypasses the CLI entirely: it writes a minimal startup script
# that imports `create_app` and runs uvicorn directly. The config path is
# passed via the `AGENTKIT_CONFIG_PATH` env var that `create_app` reads.
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session")
def e2e_server(tmp_path_factory: pytest.TempPathFactory) -> Generator[str, None, None]:
    """Start a real AgentKit server for this module's tests."""
    from tests.e2e.conftest import (
        E2E_API_KEY,
        E2E_HOST,
        E2E_PORT,
        _build_mock_env,
    )

    tmp_path = tmp_path_factory.mktemp("e2e_cov_server")
    config_dir = tmp_path / "config"
    config_dir.mkdir()
    config_file = config_dir / "agentkit.yaml"
    config_file.write_text(
        yaml.dump(
            {
                "server": {
                    "host": E2E_HOST,
                    "port": E2E_PORT,
                    # server.api_key is what AuthMiddleware reads (NOT auth.api_keys).
                    # Without it, the middleware runs in dev mode and all requests pass.
                    "api_key": E2E_API_KEY,
                },
                "llm": {"default_provider": "mock", "providers": {"mock": {"type": "mock"}}},
            }
        )
    )

    # Minimal startup script: set config path, import create_app, run uvicorn.
    # This avoids the CLI's interactive onboarding prompt entirely.
    startup_script = tmp_path / "start_server.py"
    startup_script.write_text(
        "import os, uvicorn\n"
        "from agentkit.server.app import create_app\n"
        f'uvicorn.run(create_app, factory=True, host="{E2E_HOST}", '
        f"port={E2E_PORT}, log_level='warning')\n"
    )

    env = _build_mock_env(tmp_path)
    env["AGENTKIT_CONFIG_PATH"] = str(config_file)
    # Ensure PYTHONPATH includes the project root so `agentkit` is importable
    env["PYTHONPATH"] = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

    proc = subprocess.Popen(
        [sys.executable, "-u", str(startup_script)],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        cwd=str(tmp_path),
    )

    base_url = f"http://{E2E_HOST}:{E2E_PORT}"
    deadline = time.monotonic() + 45
    ready = False
    while time.monotonic() < deadline:
        try:
            resp = httpx.get(f"{base_url}/api/v1/health", timeout=2)
            if resp.status_code == 200:
                ready = True
                break
        except httpx.RequestError:
            # ConnectError (port not open) OR ReadTimeout (lifespan not ready)
            pass
        time.sleep(0.5)

    if not ready:
        proc.terminate()
        try:
            stdout, stderr = proc.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            proc.kill()
            stdout, stderr = proc.communicate()
        pytest.fail(
            f"E2E server failed to start within 45s.\n"
            f"stdout: {stdout.decode()[:2000]}\n"
            f"stderr: {stderr.decode()[:2000]}"
        )

    yield base_url

    proc.terminate()
    try:
        proc.wait(timeout=10)
    except subprocess.TimeoutExpired:
        proc.kill()


# ═══════════════════════════════════════════════════════════════════════════
# 1. Experts API — /api/v1/experts
# ═══════════════════════════════════════════════════════════════════════════


class TestExpertsAPI:
    def test_list_experts(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/experts")
        assert resp.status_code == 200
        data = resp.json()
        assert "experts" in data
        assert "total" in data
        assert isinstance(data["experts"], list)
        assert data["total"] == len(data["experts"])

    def test_get_expert_not_found(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/experts/nonexistent_expert_xyz")
        assert resp.status_code == 404

    def test_list_experts_no_api_key(self, e2e_server: str):
        """无 API key 时应被中间件拒绝（401）。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.get("/api/v1/experts")
        assert resp.status_code == 401


# ═══════════════════════════════════════════════════════════════════════════
# 2. Workflows API — /api/v1/workflows
# ═══════════════════════════════════════════════════════════════════════════


class TestWorkflowsAPI:
    def test_list_workflows(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/workflows")
        assert resp.status_code == 200
        data = resp.json()
        assert "workflows" in data
        assert "total" in data
        assert isinstance(data["workflows"], list)

    def test_get_workflow_not_found(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/workflows/nonexistent_wf_id")
        assert resp.status_code == 404

    def test_create_workflow_invalid_body(self, api_client: httpx.Client):
        """缺少必填字段应返回 422。"""
        resp = api_client.post("/api/v1/workflows", json={})
        assert resp.status_code == 422


# ═══════════════════════════════════════════════════════════════════════════
# 3. Config Sync API — /api/v1/config
# ═══════════════════════════════════════════════════════════════════════════


class TestConfigSyncAPI:
    def test_get_config_version(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/config/version")
        assert resp.status_code == 200
        data = resp.json()
        assert "version" in data
        assert "skill_count" in data
        assert "workflow_count" in data

    def test_get_all_configs(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/config/all")
        assert resp.status_code == 200
        data = resp.json()
        assert "version" in data
        assert "skills" in data
        assert "workflows" in data
        assert "synced_at" in data

    def test_get_skill_configs(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/config/skills")
        assert resp.status_code == 200
        data = resp.json()
        assert "skills" in data
        assert "count" in data

    def test_get_config_no_api_key(self, e2e_server: str):
        """config 端点需 CHAT 权限；无 API key 时中间件返回 401。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.get("/api/v1/config/version")
        assert resp.status_code == 401


# ═══════════════════════════════════════════════════════════════════════════
# 4. System Resources API — /api/v1/system/resources
# ═══════════════════════════════════════════════════════════════════════════


class TestSystemAPI:
    def test_get_system_resources(self, api_client: httpx.Client):
        """端点明确不挂 SYSTEM_CONFIG（见路由注释），operator 可读。"""
        resp = api_client.get("/api/v1/system/resources")
        assert resp.status_code == 200
        data = resp.json()
        assert "cpu" in data
        assert "memory" in data
        assert "disk" in data
        assert "timestamp" in data

    def test_get_system_resources_no_api_key(self, e2e_server: str):
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.get("/api/v1/system/resources")
        assert resp.status_code == 401


# ═══════════════════════════════════════════════════════════════════════════
# 5. Evolution API — /api/v1/evolution
#    E2E 配置无 evolution 段，store 为 None → 503
# ═══════════════════════════════════════════════════════════════════════════


class TestEvolutionAPI:
    def test_list_evolution_events_store_unconfigured(self, api_client: httpx.Client):
        """E2E server 未配置 evolution store，应返回 503。"""
        resp = api_client.get("/api/v1/evolution/events")
        assert resp.status_code == 503

    def test_list_ab_tests_store_unconfigured(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/evolution/ab-tests")
        assert resp.status_code == 503

    def test_trigger_evolution_no_agent(self, api_client: httpx.Client):
        """trigger 在 store 未配置时也应返回 503（_get_evolution_store 抛 503）。"""
        resp = api_client.post(
            "/api/v1/evolution/trigger",
            json={"agent_name": "no_such_agent"},
        )
        assert resp.status_code == 503


# ═══════════════════════════════════════════════════════════════════════════
# 6. Evolution Dashboard API — /api/v1/evolution-dashboard
#    使用自带 _verify_api_key；store 未配置时回退到内存空数据 → 200
# ═══════════════════════════════════════════════════════════════════════════


class TestEvolutionDashboardAPI:
    def test_get_metrics(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/evolution-dashboard/metrics")
        assert resp.status_code == 200
        data = resp.json()
        # 默认空指标结构
        assert "total_tasks" in data or "metrics" in data

    def test_list_experiences(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/evolution-dashboard/experiences")
        assert resp.status_code == 200
        data = resp.json()
        assert "experiences" in data
        assert "total" in data

    def test_get_metrics_no_api_key(self, e2e_server: str):
        """dashboard 端点自带 _verify_api_key，无 key 时返回 401。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.get("/api/v1/evolution-dashboard/metrics")
        assert resp.status_code == 401

    def test_pitfalls_missing_required_param(self, api_client: httpx.Client):
        """pitfalls 端点 task_type 为必填，缺失时 422。"""
        resp = api_client.get("/api/v1/evolution-dashboard/pitfalls")
        assert resp.status_code == 422


# ═══════════════════════════════════════════════════════════════════════════
# 7. Settings API — /api/v1/settings
#    GET 不需特殊权限；PUT 需 SYSTEM_CONFIG（operator 无 → 403）
# ═══════════════════════════════════════════════════════════════════════════


class TestSettingsAPI:
    def test_get_llm_settings(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/settings/llm")
        assert resp.status_code == 200
        data = resp.json()
        assert "providers" in data

    def test_get_skills_settings(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/settings/skills")
        assert resp.status_code == 200
        data = resp.json()
        assert "paths" in data

    def test_get_general_settings(self, api_client: httpx.Client):
        resp = api_client.get("/api/v1/settings/general")
        assert resp.status_code == 200

    def test_update_llm_settings_forbidden(self, api_client: httpx.Client):
        """PUT /settings/llm 需 SYSTEM_CONFIG；api_client 为 operator 角色 → 403。"""
        resp = api_client.put(
            "/api/v1/settings/llm",
            json={"providers": []},
        )
        # operator 无 SYSTEM_CONFIG；非 dev mode 高风险权限 → 403
        assert resp.status_code in (401, 403)


# ═══════════════════════════════════════════════════════════════════════════
# 8. Auth API — /api/v1/auth
#    login/whoami 在中间件白名单内（不需要 X-API-Key）
# ═══════════════════════════════════════════════════════════════════════════


class TestAuthAPI:
    def test_login_invalid_credentials(self, e2e_server: str):
        """login 端点白名单，无需 API key；无效凭据 → 401。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.post(
            "/api/v1/auth/login",
            json={"username": "no_such_user", "password": "wrong_password"},
        )
        assert resp.status_code == 401

    def test_whoami_without_bearer(self, e2e_server: str):
        """whoami 自行校验 bearer token；缺失 → 401。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.get("/api/v1/auth/whoami")
        assert resp.status_code == 401

    def test_login_missing_fields(self, e2e_server: str):
        """login 请求体缺少必填字段 → 422。"""
        client = httpx.Client(base_url=e2e_server, timeout=10)
        resp = client.post("/api/v1/auth/login", json={})
        assert resp.status_code == 422


# ═══════════════════════════════════════════════════════════════════════════
# 9. Memory API — /api/v1/memory
#    E2E 配置无 memory 段，retriever 未设置 → 503
# ═══════════════════════════════════════════════════════════════════════════


class TestMemoryAPI:
    def test_search_episodic_retriever_unconfigured(self, api_client: httpx.Client):
        """retriever 未配置 → 503。"""
        resp = api_client.get("/api/v1/memory/episodic", params={"query": "test"})
        assert resp.status_code == 503

    def test_search_semantic_retriever_unconfigured(self, api_client: httpx.Client):
        resp = api_client.get(
            "/api/v1/memory/semantic/search", params={"query": "test"}
        )
        assert resp.status_code == 503

    def test_search_episodic_missing_query(self, api_client: httpx.Client):
        """query 为必填 query 参数，缺失 → 422。"""
        resp = api_client.get("/api/v1/memory/episodic")
        assert resp.status_code == 422
