"""
Phase I dogfood — AgentPack end-to-end integration tests (no LLM).

Exercises the full HTTP path that a desktop user takes:

  install built-in pack ──► list ──► get detail
        │
        └──► create-agent ──► GET agent (verify config)
                                    │
                                    └──► from_config() ──► agent compiles ✓
        │
        └──► uninstall pack ──► agent STILL exists (no cascade delete)

NOTE on install: POST /agentpacks/install is loopback-only (127.0.0.1).
TestClient sends requests as host "testclient", so the loopback check
returns 404. We test the loopback gate separately (test_install_loopback_blocked)
and use install_from_zip() directly for the pack-setup fixture — consistent
with test_agentpack.py::test_create_agent_from_pack_endpoint. All subsequent
API calls (list / get / create-agent / delete) go through HTTP.

Real LLM execution (review_report.md verification) lives in
scripts/dogfood_agentpack.py and is marked skip here.
"""
from __future__ import annotations

import json
import os
import zipfile

import pytest
import yaml

os.environ.setdefault("AGENTPAAS_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AGENTPAAS_TESTING", "1")

# ── Helpers ───────────────────────────────────────────────────────────────────

def _pack_zip_from_disk(tmp_path, pack_id: str) -> str:
    """Zip the on-disk agentexample pack so install_from_zip() can consume it."""
    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    pack_dir = os.path.join(repo_root, "agentexample", "agentpacks", pack_id)
    if not os.path.isdir(pack_dir):
        pytest.skip(f"built-in pack not found on disk: {pack_dir}")

    zip_path = str(tmp_path / f"{pack_id}.zip")
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for dirpath, _, filenames in os.walk(pack_dir):
            for fname in filenames:
                full = os.path.join(dirpath, fname)
                arcname = os.path.relpath(full, pack_dir)
                zf.write(full, arcname)
    return zip_path


def _install_pack(packs_dir: str, zip_path: str):
    """Install a pack directly (bypasses loopback gate — tested separately)."""
    from agentpaas.engine.agentpack_store import install_from_zip
    return install_from_zip(zip_path, packs_dir)


@pytest.fixture()
def http_client(tmp_path, monkeypatch):
    """TestClient + fresh in-memory DB + one tenant/user/key + reviewer pack pre-installed."""
    import secrets
    from fastapi.testclient import TestClient
    from agentpaas.api.app import app
    from agentpaas.api.middleware.auth import hash_key
    from agentpaas.config import settings
    from agentpaas.db.models import Database, gen_id, now_utc
    import agentpaas.db.session as _session_mod

    packs_dir = tmp_path / "agentpacks"
    packs_dir.mkdir()
    monkeypatch.setattr(settings, "agentpacks_dir", str(packs_dir))
    # 自动目录开辟隔离到 tmp（否则 create-agent 测试会写真实 Workspace/）
    monkeypatch.setattr(settings, "workspace_base", str(tmp_path / "Workspace"))

    prev_db = _session_mod._db
    _session_mod._db = Database("sqlite:///:memory:")
    db = _session_mod._db

    tid = gen_id("tn_")
    uid = gen_id("usr_")
    raw_key = f"ap_{secrets.token_hex(16)}"
    now = now_utc()
    db.execute(
        "INSERT INTO tenants (id, name, plan, status, created_at) "
        "VALUES (?, 'test', 'free', 'active', ?)", (tid, now),
    )
    db.execute(
        "INSERT INTO users (id, tenant_id, email, role, created_at) "
        "VALUES (?, ?, '', 'admin', ?)", (uid, tid, now),
    )
    db.execute(
        "INSERT INTO api_keys "
        "(id, tenant_id, user_id, key_hash, key_prefix, name, scopes, "
        " rate_limit, status, created_at) "
        "VALUES (?, ?, ?, ?, ?, 'test', ?, 600, 'active', ?)",
        (gen_id("key_"), tid, uid, hash_key(raw_key), raw_key[:8],
         json.dumps(["agents:*", "keys:*"]), now),
    )
    db.commit()

    # Pre-install the built-in reviewer pack directly (bypass loopback gate)
    zip_path = _pack_zip_from_disk(tmp_path, REVIEWER_ID)
    pack = _install_pack(str(packs_dir), zip_path)

    with TestClient(app) as client:
        yield client, raw_key, str(packs_dir), pack

    _session_mod._db = prev_db


# ── Auth helper ───────────────────────────────────────────────────────────────

def _auth(key: str) -> dict:
    return {"Authorization": f"Bearer {key}"}


# ── Tests ─────────────────────────────────────────────────────────────────────

REVIEWER_ID = "research.top-journal-reviewer"


def test_install_loopback_blocked(tmp_path, monkeypatch):
    """POST /agentpacks/install from non-loopback → 404 (loopback gate).

    TestClient host = 'testclient', not 127.0.0.1 — this is the gate we want.
    """
    import secrets
    from fastapi.testclient import TestClient
    from agentpaas.api.app import app
    from agentpaas.config import settings

    packs_dir = tmp_path / "agentpacks"
    packs_dir.mkdir()
    monkeypatch.setattr(settings, "agentpacks_dir", str(packs_dir))

    zip_path = _pack_zip_from_disk(tmp_path, REVIEWER_ID)
    with TestClient(app) as client:
        r = client.post(
            "/api/v1/agentpacks/install",
            json={"zip_path": zip_path},
        )
    assert r.status_code == 404, r.text[:200]


def test_install_builtin_pack_via_api(tmp_path, http_client):
    """Pre-installed reviewer pack is visible via GET /agentpacks after direct install."""
    client, key, packs_dir, pack = http_client
    # pack was installed in fixture — verify it via the HTTP list
    r = client.get("/api/v1/agentpacks", headers=_auth(key))
    assert r.status_code == 200, r.text[:300]
    body = r.json()
    assert body["count"] >= 1
    ids = [p["id"] for p in body["agentpacks"]]
    assert REVIEWER_ID in ids
    # spot-check detail fields
    detail = next(p for p in body["agentpacks"] if p["id"] == REVIEWER_ID)
    assert detail["version"] == "0.1.0"
    assert detail["domain"] == "research"
    assert detail["permissions"]["shell"] is False
    assert detail["permissions"]["network"] is False


def test_installed_pack_appears_in_list(tmp_path, http_client):
    """GET /agentpacks → pre-installed pack shows up."""
    client, key, packs_dir, pack = http_client
    r = client.get("/api/v1/agentpacks", headers=_auth(key))
    assert r.status_code == 200, r.text
    body = r.json()
    assert body["count"] >= 1
    ids = [p["id"] for p in body["agentpacks"]]
    assert REVIEWER_ID in ids


def test_get_installed_pack_detail(tmp_path, http_client):
    """GET /agentpacks/{id} → correct detail including permission_summary."""
    client, key, packs_dir, pack = http_client
    r = client.get(f"/api/v1/agentpacks/{REVIEWER_ID}", headers=_auth(key))
    assert r.status_code == 200, r.text
    p = r.json()
    assert p["id"] == REVIEWER_ID
    assert len(p["permission_summary"]) > 0
    assert "professor" in p["audience"] or "phd_student" in p["audience"]


def test_create_agent_from_installed_pack(tmp_path, http_client):
    """POST create-agent → 201, agent_id returned."""
    client, key, packs_dir, pack = http_client
    r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "我的审稿助手", "description": "dogfood test"},
        headers=_auth(key),
    )
    assert r.status_code == 201, r.text[:300]
    body = r.json()
    assert body["ok"] is True
    assert body["agent_id"].startswith("ag_")
    assert body["pack"]["id"] == REVIEWER_ID


def test_created_agent_has_correct_config(tmp_path, http_client):
    """GET /agents/{id} → agent_dir=pack.path, _config_dir correct."""
    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "审稿助手"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]

    r = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key))
    assert r.status_code == 200, r.text
    agent = r.json()

    assert agent["agent_dir"] == pack.path
    assert agent["agent_template"] == REVIEWER_ID

    config = agent["config"]
    # _config_dir must equal pack.path so sub-agent refs resolve correctly
    assert config.get("_config_dir") == pack.path


def test_agent_config_compiles_with_from_config(tmp_path, http_client):
    """Critical seam: config stored in DB → from_config() → agent term.

    Mirrors what _execute_agent does (dump to tmp YAML, compile).
    If this passes, the agent can actually run (LLM aside).
    """
    import tempfile
    from lambdagent.fromconfig import from_config as lambdagent_from_config

    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "审稿助手"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]
    config = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key)).json()["config"]

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".yml", delete=False, encoding="utf-8"
    ) as f:
        yaml.dump(config, f, allow_unicode=True)
        tmp_yml = f.name
    try:
        term = lambdagent_from_config(tmp_yml)
        assert term is not None
    finally:
        os.unlink(tmp_yml)


def test_agent_appears_in_agents_list(tmp_path, http_client):
    """GET /agents after create-agent → new agent visible."""
    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "审稿助手"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]

    r = client.get("/api/v1/agents", headers=_auth(key))
    assert r.status_code == 200
    agent_ids = [a["id"] for a in r.json()["agents"]]
    assert agent_id in agent_ids


def test_uninstall_pack_does_not_delete_agents(tmp_path, http_client):
    """DELETE /agentpacks/{id} removes the pack; derived agent persists.

    Agents are data, not derived assets. Deleting the template pack MUST NOT
    cascade-delete agents (they may have run history and workspace artifacts).
    """
    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "审稿助手"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]

    del_r = client.delete(f"/api/v1/agentpacks/{REVIEWER_ID}", headers=_auth(key))
    assert del_r.status_code == 200
    assert del_r.json()["ok"] is True

    # Pack is gone
    list_r = client.get("/api/v1/agentpacks", headers=_auth(key))
    assert list_r.json()["count"] == 0

    # Agent STILL exists
    agent_r = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key))
    assert agent_r.status_code == 200
    assert agent_r.json()["name"] == "审稿助手"


def test_get_unknown_pack_returns_404(tmp_path, http_client):
    """GET /agentpacks/does.not.exist → 404."""
    client, key, packs_dir, pack = http_client
    r = client.get("/api/v1/agentpacks/does.not.exist", headers=_auth(key))
    assert r.status_code == 404


def test_create_agent_from_uninstalled_pack_returns_404(tmp_path, http_client):
    """POST create-agent for a pack that was never installed → 404."""
    client, key, packs_dir, pack = http_client
    r = client.post(
        "/api/v1/agentpacks/does.not.exist/create-agent",
        json={"name": "x"},
        headers=_auth(key),
    )
    assert r.status_code == 404


@pytest.mark.skip(reason="requires real LLM (ANTHROPIC_API_KEY). Run: python scripts/dogfood_agentpack.py")
def test_run_agent_produces_review_report(tmp_path, http_client):
    """Smoke test: run reviewer pack agent with a short abstract.

    Skipped in CI — needs a live LLM. Use scripts/dogfood_agentpack.py for the
    full end-to-end dogfood (workspace/review_report.md verification).
    """
    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "审稿助手"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]

    abstract = (
        "Title: Quantum Speedup for Matrix Multiplication.\n"
        "Abstract: We present a quantum algorithm achieving O(n^{1.5}) "
        "matrix multiplication, improving on the classical Strassen bound O(n^{2.37}).\n"
        "Verify this claim and produce a review report."
    )
    run_r = client.post(
        f"/api/v1/agents/{agent_id}/run",
        json={"input": abstract},
        headers=_auth(key),
        timeout=300,
    )
    assert run_r.status_code == 200, run_r.text[:500]
    body = run_r.json()
    workspace = body.get("workspace_path", "")
    assert workspace, "workspace_path missing from response"
    assert os.path.isfile(os.path.join(workspace, "review_report.md")), \
        f"review_report.md not found in {workspace}"
    assert os.path.isfile(os.path.join(workspace, "review_result.json")), \
        f"review_result.json not found in {workspace}"


# ── Pack update (zip upload → agents on latest config) ───────────────────────
# AUDIT 后新增功能: POST /agentpacks/{id}/update-upload。HTTP 层的 loopback
# 门单测 + 核心传播逻辑直测（与 install 的测试策略一致）。

def _pack_zip_with_prompt(tmp_path, pack_id: str, new_prompt: str,
                          new_version: str = "") -> str:
    """复制内置包源码，改 entrypoint 的 systemPrompt（和可选版本号），打成 zip。"""
    import shutil
    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    src = os.path.join(repo_root, "agentexample", "agentpacks", pack_id)
    if not os.path.isdir(src):
        pytest.skip(f"built-in pack not found on disk: {src}")
    work = str(tmp_path / "pack_v2_src")
    shutil.copytree(src, work)

    with open(os.path.join(work, "manifest.yml"), encoding="utf-8") as f:
        manifest = yaml.safe_load(f)
    if new_version:
        manifest["version"] = new_version
        with open(os.path.join(work, "manifest.yml"), "w", encoding="utf-8") as f:
            yaml.safe_dump(manifest, f, allow_unicode=True)

    entry = os.path.join(work, manifest["entrypoint"])
    with open(entry, encoding="utf-8") as f:
        cfg = yaml.safe_load(f)
    cfg["systemPrompt"] = new_prompt
    with open(entry, "w", encoding="utf-8") as f:
        yaml.safe_dump(cfg, f, allow_unicode=True)

    zip_path = str(tmp_path / "pack_v2.zip")
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for dirpath, _, filenames in os.walk(work):
            for fname in filenames:
                full = os.path.join(dirpath, fname)
                zf.write(full, os.path.relpath(full, work))
    return zip_path


def test_update_upload_loopback_blocked(http_client, tmp_path):
    """非 loopback 调 update-upload → 404（同 /install 的 oracle 防御）。"""
    client, key, packs_dir, pack = http_client
    zip_path = _pack_zip_with_prompt(tmp_path, REVIEWER_ID, "x")
    with open(zip_path, "rb") as f:
        r = client.post(
            f"/api/v1/agentpacks/{REVIEWER_ID}/update-upload",
            files={"file": ("pack.zip", f, "application/zip")},
            headers=_auth(key),
        )
    assert r.status_code == 404


def test_peek_manifest_id_check(tmp_path, http_client):
    """peek_manifest_from_zip 返回正确 id（更新端点据此拒绝错包）。"""
    from agentpaas.engine.agentpack_store import peek_manifest_from_zip
    zip_path = _pack_zip_with_prompt(tmp_path, REVIEWER_ID, "NEW PROMPT")
    m = peek_manifest_from_zip(zip_path)
    assert m.id == REVIEWER_ID
    # zip 不会被安装
    assert not os.path.exists(
        os.path.join(str(tmp_path), "agentpacks", REVIEWER_ID, "0.9.9"))


def test_pack_update_refreshes_agents(http_client, tmp_path):
    """更新包 → 由它创建的智能体生成新版本、配置换新、可回滚。"""
    from agentpaas.api.v1.agentpacks import _refresh_agents_from_pack

    client, key, packs_dir, pack = http_client

    # 1. 从包创建智能体（HTTP）
    r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "更新测试-审稿"},
        headers=_auth(key),
    )
    assert r.status_code == 201, r.text[:300]
    agent_id = r.json()["agent_id"]

    g0 = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key)).json()
    old_prompt = g0["config"]["systemPrompt"]
    assert g0["current_version"] == 1

    # 2. 构造新版包（新版本号 + 新 systemPrompt）并安装
    NEW_PROMPT = "你是更新后的审稿专家 v2。"
    zip_path = _pack_zip_with_prompt(
        tmp_path, REVIEWER_ID, NEW_PROMPT, new_version="0.9.9")
    new_pack = _install_pack(packs_dir, zip_path)
    assert new_pack.version == "0.9.9"

    # 3. 传播到智能体
    refreshed = _refresh_agents_from_pack(new_pack)
    assert any(a["agent_id"] == agent_id and a["version"] == 2
               for a in refreshed)

    # 4. 智能体现在跑最新配置
    g1 = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key)).json()
    assert g1["current_version"] == 2
    assert g1["config"]["systemPrompt"] == NEW_PROMPT
    assert g1["config"]["systemPrompt"] != old_prompt
    # agent_dir 跟到新版本安装路径
    assert g1["agent_dir"].endswith("0.9.9")

    # 5. 旧配置仍可回滚
    rb = client.post(
        f"/api/v1/agents/{agent_id}/rollback",
        json={"target_version": 1},
        headers=_auth(key),
    )
    assert rb.status_code == 200, rb.text[:300]
    g2 = client.get(f"/api/v1/agents/{agent_id}", headers=_auth(key)).json()
    assert g2["config"]["systemPrompt"] == old_prompt


def test_create_agent_from_pack_auto_provisions_dirs(http_client, tmp_path):
    """从包创建智能体也自动开辟 Workspace/<名>/{data,workspace} 目录。"""
    client, key, packs_dir, pack = http_client
    r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "目录开辟-审稿"},
        headers=_auth(key),
    )
    assert r.status_code == 201, r.text[:300]
    body = r.json()
    assert body["work_dir"].endswith("目录开辟-审稿")
    assert body["source_dir"] == os.path.join(body["work_dir"], "data")
    assert body["run_dir"] == os.path.join(body["work_dir"], "workspace")
    assert os.path.isdir(body["source_dir"])
    assert os.path.isdir(body["run_dir"])

    # GET 回读一致
    g = client.get(f"/api/v1/agents/{body['agent_id']}", headers=_auth(key)).json()
    assert g["work_dir"] == body["work_dir"]
    assert g["run_dir"] == body["run_dir"]


def test_run_with_context_work_dir_override(tmp_path, http_client, monkeypatch):
    """工作区对话模式回归：run 带 context.work_dir 应覆盖工作目录、不报 500。

    曾因 run handler 用裸 `os`（本文件无模块级 import os，仅 `import os as _os`）
    在 work_dir override 分支 NameError 崩 500——且该分支只在 context.work_dir
    有值时触发，普通 run 测试覆盖不到。本例正好走这条分支。
    """
    client, key, packs_dir, pack = http_client
    create_r = client.post(
        f"/api/v1/agentpacks/{REVIEWER_ID}/create-agent",
        json={"name": "工作区回归"},
        headers=_auth(key),
    )
    agent_id = create_r.json()["agent_id"]

    captured = {}

    def _stub_exec(config, input_text, **kw):
        captured["work_dir"] = kw.get("work_dir")
        captured["source_dir"] = kw.get("source_dir")
        captured["inplace_dir"] = kw.get("inplace_dir")
        return ("摘要：ok", {"workspace_path": "", "input_tokens": 1,
                             "output_tokens": 1, "steps": 1, "cost_usd": 0.0,
                             "cache_read_tokens": 0, "cache_creation_tokens": 0})

    import agentpaas.api.v1.agents as _agents_mod
    monkeypatch.setattr(_agents_mod, "_execute_agent", _stub_exec)

    folder = str(tmp_path)
    r = client.post(
        f"/api/v1/agents/{agent_id}/run",
        json={"input": "总结一下", "context": {"work_dir": folder}},
        headers=_auth(key),
    )
    assert r.status_code != 500, f"work_dir override 崩了: {r.text[:300]}"
    assert r.status_code == 200, r.text[:300]
    # override 生效：_execute_agent 收到的 work_dir/source_dir 都是所选文件夹
    assert captured.get("work_dir") == folder
    assert captured.get("source_dir") == folder
    # 工作区模式:inplace_dir 传入 → 模型 [工作目录] 会被设为 F(就地操作)
    assert captured.get("inplace_dir") == folder
