from __future__ import annotations

import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path

import yaml

from paper_fetch.mcp._instructions import DEFAULT_FETCH_NOTES, DEFAULT_FETCH_VALUES, ERROR_CONTRACT, SKILL_ENVIRONMENT_VARIABLES
from tests.paths import REPO_ROOT, SKILL_DIR

STATIC_SKILL_DIR = SKILL_DIR
STATIC_SKILL_PATH = SKILL_DIR / "SKILL.md"


def write_fake_python(path: Path, log_path: Path) -> None:
    path.write_text(
        "#!/bin/sh\n"
        f'echo "$0 $@" >> "{log_path}"\n'
        'if [ "$1" = "-c" ]; then\n'
        '  printf "%s\\n" "$0"\n'
        "fi\n"
        "exit 0\n",
        encoding="utf-8",
    )
    path.chmod(path.stat().st_mode | stat.S_IXUSR)


def write_fake_mcp_cli(path: Path, log_path: Path) -> None:
    path.write_text(
        "#!/bin/sh\n"
        f'echo "$0 $@" >> "{log_path}"\n'
        'if [ "$1" = "mcp" ] && [ "$2" = "get" ]; then\n'
        "  exit 1\n"
        "fi\n"
        "exit 0\n",
        encoding="utf-8",
    )
    path.chmod(path.stat().st_mode | stat.S_IXUSR)


def copy_installer_fixture(repo_dir: Path) -> None:
    (repo_dir / "scripts").mkdir(parents=True, exist_ok=True)
    shutil.copy2(REPO_ROOT / "install.sh", repo_dir / "install.sh")
    shutil.copy2(REPO_ROOT / "install-formula-tools.sh", repo_dir / "install-formula-tools.sh")
    shutil.copy2(REPO_ROOT / "scripts" / "_skill_install_common.sh", repo_dir / "scripts" / "_skill_install_common.sh")
    shutil.copy2(REPO_ROOT / "scripts" / "install-claude-skill.sh", repo_dir / "scripts" / "install-claude-skill.sh")
    shutil.copy2(REPO_ROOT / "scripts" / "install-codex-skill.sh", repo_dir / "scripts" / "install-codex-skill.sh")
    shutil.copytree(STATIC_SKILL_DIR, repo_dir / "skills" / "paper-fetch-skill", dirs_exist_ok=True)
    shutil.copy2(REPO_ROOT / "pyproject.toml", repo_dir / "pyproject.toml")


def iter_skill_markdown_files(root: Path) -> list[Path]:
    return sorted(path for path in root.rglob("*.md") if path.is_file())


def read_skill_bundle(root: Path) -> str:
    return "\n\n".join(path.read_text(encoding="utf-8") for path in iter_skill_markdown_files(root))


def assert_skill_bundle_matches_repo(testcase: unittest.TestCase, installed_root: Path) -> None:
    expected_files = [path.relative_to(STATIC_SKILL_DIR).as_posix() for path in iter_skill_markdown_files(STATIC_SKILL_DIR)]
    actual_files = [path.relative_to(installed_root).as_posix() for path in iter_skill_markdown_files(installed_root)]

    testcase.assertEqual(actual_files, expected_files)
    for relative_path in expected_files:
        testcase.assertEqual(
            (installed_root / relative_path).read_text(encoding="utf-8"),
            (STATIC_SKILL_DIR / relative_path).read_text(encoding="utf-8"),
        )


class StaticSkillTests(unittest.TestCase):
    def test_static_skill_frontmatter_is_valid_yaml(self) -> None:
        text = STATIC_SKILL_PATH.read_text(encoding="utf-8")
        self.assertTrue(text.startswith("---\n"))
        frontmatter = text.split("---\n", 2)[1]

        metadata = yaml.safe_load(frontmatter)

        self.assertEqual(metadata["name"], "paper-fetch-skill")
        self.assertIn("description", metadata)
        description = metadata["description"]
        self.assertIn("web search", description)
        self.assertIn("候选论文", description)
        self.assertIn("全文", description)
        self.assertIn("需要阅读", description)
        self.assertIn("不替代开放式领域检索", description)

    def test_static_skill_entrypoint_stays_thin_and_points_at_references(self) -> None:
        text = STATIC_SKILL_PATH.read_text(encoding="utf-8")

        self.assertIn("resolve_paper", text)
        self.assertIn("fetch_paper", text)
        self.assertIn("list_cached", text)
        self.assertIn("get_cached", text)
        self.assertIn("batch_check", text)
        self.assertIn("has_fulltext", text)
        self.assertIn("provider_status", text)
        self.assertIn("参考文献列表", text)
        self.assertIn("搜索工具只负责发现候选", text)
        self.assertIn("普通上下文阅读/总结不因保存策略缺失而阻塞", text)
        self.assertIn("ProviderSpec.requires_browser_runtime=True", text)
        self.assertNotIn("当前 `wiley`", text)
        self.assertIn("不要仅因为本地没有 PDF", text)
        self.assertIn("references/environment.md", text)
        self.assertIn("references/cli-fallback.md", text)
        self.assertIn("references/failure-handling.md", text)
        self.assertNotIn("## 工具说明", text)
        self.assertLessEqual(len(text.splitlines()), 80)

    def test_static_skill_bundle_covers_runtime_contract(self) -> None:
        text = read_skill_bundle(STATIC_SKILL_DIR)

        self.assertIn("paper-fetch --query", text)
        self.assertIn("## Error Contract", text)
        self.assertIn("summarize_paper", text)
        self.assertIn("verify_citation_list", text)
        self.assertIn("token_estimate_breakdown", text)
        self.assertIn("citation list", text)
        self.assertIn("unreadable", text.lower())
        self.assertNotIn("not thread-safe", text)
        for key, value in DEFAULT_FETCH_VALUES:
            self.assertIn(f"`{key}={value}`", text)
        for note in DEFAULT_FETCH_NOTES:
            self.assertIn(note, text)
        for name, _description in SKILL_ENVIRONMENT_VARIABLES:
            self.assertIn(f"`{name}`", text)
        for status, _description in ERROR_CONTRACT:
            self.assertIn(f"`{status}`", text)
        self.assertNotIn("${", text)
        self.assertNotIn(str(REPO_ROOT), text)
        self.assertNotIn(".venv", text)
        self.assertNotIn(".env", text)


class InstallerSmokeTests(unittest.TestCase):
    def run_installer(
        self,
        *,
        script_name: str,
        args: list[str] | None = None,
        extra_env: dict[str, str] | None = None,
        fake_codex: bool = False,
        fake_claude: bool = False,
        create_repo_env: bool = False,
    ) -> tuple[Path, Path, Path]:
        temp_dir = tempfile.TemporaryDirectory()
        self.addCleanup(temp_dir.cleanup)
        sandbox = Path(temp_dir.name)
        repo_dir = sandbox / "repo"
        fake_bin_dir = sandbox / "bin"
        home_dir = sandbox / "home"
        codex_home = sandbox / "codex-home"
        log_path = sandbox / "python.log"

        copy_installer_fixture(repo_dir)
        if create_repo_env:
            (repo_dir / ".env").write_text("ELSEVIER_API_KEY=dev\n", encoding="utf-8")
        fake_bin_dir.mkdir(parents=True, exist_ok=True)
        home_dir.mkdir(parents=True, exist_ok=True)
        codex_home.mkdir(parents=True, exist_ok=True)
        write_fake_python(fake_bin_dir / "python3", log_path)
        if fake_codex:
            write_fake_mcp_cli(fake_bin_dir / "codex", log_path)
        if fake_claude:
            write_fake_mcp_cli(fake_bin_dir / "claude", log_path)

        env = os.environ.copy()
        env["PATH"] = f"{fake_bin_dir}{os.pathsep}{env.get('PATH', '')}"
        env["HOME"] = str(home_dir)
        env["CODEX_HOME"] = str(codex_home)
        if extra_env:
            env.update(extra_env)

        subprocess.run(
            ["bash", str(repo_dir / "scripts" / script_name), *(args or [])],
            cwd=repo_dir,
            env=env,
            check=True,
            capture_output=True,
            text=True,
        )
        return repo_dir, sandbox, log_path

    def test_full_installer_help_smoke(self) -> None:
        temp_dir = tempfile.TemporaryDirectory()
        self.addCleanup(temp_dir.cleanup)
        repo_dir = Path(temp_dir.name) / "repo"
        copy_installer_fixture(repo_dir)

        result = subprocess.run(
            ["bash", str(repo_dir / "install.sh"), "--help"],
            cwd=repo_dir,
            check=False,
            capture_output=True,
            text=True,
        )

        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertIn("One-command installer for the full paper-fetch runtime.", result.stdout)
        self.assertIn("--lite", result.stdout)
        self.assertNotIn("--skip-playwright-install", result.stdout)

    def test_full_installer_completion_mentions_elsevier_api_key(self) -> None:
        temp_dir = tempfile.TemporaryDirectory()
        self.addCleanup(temp_dir.cleanup)
        sandbox = Path(temp_dir.name)
        repo_dir = sandbox / "repo"
        fake_bin_dir = sandbox / "bin"
        home_dir = sandbox / "home"
        log_path = sandbox / "python.log"
        copy_installer_fixture(repo_dir)
        fake_bin_dir.mkdir(parents=True, exist_ok=True)
        home_dir.mkdir(parents=True, exist_ok=True)
        write_fake_python(fake_bin_dir / "python3", log_path)

        env = os.environ.copy()
        env["PATH"] = f"{fake_bin_dir}{os.pathsep}{env.get('PATH', '')}"
        env["HOME"] = str(home_dir)
        result = subprocess.run(
            ["bash", str(repo_dir / "install.sh"), "--system", "--lite", "--skip-env-file"],
            cwd=repo_dir,
            env=env,
            check=False,
            capture_output=True,
            text=True,
        )

        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertIn("Installation complete.", result.stdout)
        self.assertIn("Elsevier setup: request a key at https://dev.elsevier.com/", result.stdout)
        self.assertIn('ELSEVIER_API_KEY="..."', result.stdout)

    def test_skill_installers_prompt_for_elsevier_api_key(self) -> None:
        for script_name in ("install-claude-skill.sh", "install-codex-skill.sh"):
            with self.subTest(script_name=script_name):
                script = (
                    (REPO_ROOT / "scripts" / script_name).read_text(encoding="utf-8")
                    + (REPO_ROOT / "scripts" / "_skill_install_common.sh").read_text(encoding="utf-8")
                )
                self.assertIn("ELSEVIER_API_KEY", script)
                self.assertIn("https://dev.elsevier.com/", script)
                self.assertIn("--env-file", script)

    def test_formula_bootstrap_honors_selected_python(self) -> None:
        temp_dir = tempfile.TemporaryDirectory()
        self.addCleanup(temp_dir.cleanup)
        sandbox = Path(temp_dir.name)
        repo_dir = sandbox / "repo"
        fake_python = sandbox / "custom-python"
        log_path = sandbox / "python.log"
        copy_installer_fixture(repo_dir)
        write_fake_python(fake_python, log_path)

        env = os.environ.copy()
        env["PAPER_FETCH_INSTALL_PYTHON_BIN"] = str(fake_python)
        subprocess.run(
            [
                "bash",
                str(repo_dir / "install-formula-tools.sh"),
                "--no-node",
            ],
            cwd=repo_dir,
            env=env,
            check=True,
            capture_output=True,
            text=True,
        )

        log_text = log_path.read_text(encoding="utf-8")
        self.assertIn(f"{fake_python} -m paper_fetch.formula.install", log_text)
        self.assertIn("--target-dir", log_text)
        self.assertIn("--no-node", log_text)
        self.assertNotIn("--skip-playwright-install", log_text)

    def test_claude_installer_copies_static_skill_without_repo_bootstrap_side_effects(self) -> None:
        repo_dir, sandbox, log_path = self.run_installer(script_name="install-claude-skill.sh")

        installed_root = sandbox / "home" / ".claude" / "skills" / "paper-fetch-skill"
        installed_skill = installed_root / "SKILL.md"
        self.assertTrue(installed_skill.exists())
        assert_skill_bundle_matches_repo(self, installed_root)
        self.assertFalse((repo_dir / ".venv").exists())
        self.assertFalse((repo_dir / ".env").exists())
        self.assertIn("-m pip install --quiet .", log_path.read_text(encoding="utf-8"))
        self.assertFalse((installed_root / "agents").exists())

    def test_claude_installer_can_register_mcp_server(self) -> None:
        _, _, log_path = self.run_installer(
            script_name="install-claude-skill.sh",
            args=["--register-mcp", "--env-file", "/tmp/paper-fetch.env", "--mcp-scope", "project"],
            fake_claude=True,
        )

        log_text = log_path.read_text(encoding="utf-8")
        self.assertIn("claude mcp remove -s project paper-fetch", log_text)
        self.assertIn(
            "claude mcp add -s project -e PAPER_FETCH_ENV_FILE=/tmp/paper-fetch.env -- paper-fetch",
            log_text,
        )
        self.assertIn("-m paper_fetch.mcp.server", log_text)

    def test_codex_installer_adds_openai_manifest_shim(self) -> None:
        repo_dir, sandbox, log_path = self.run_installer(script_name="install-codex-skill.sh")

        skill_dir = sandbox / "codex-home" / "skills" / "paper-fetch-skill"
        installed_skill = skill_dir / "SKILL.md"
        manifest_path = skill_dir / "agents" / "openai.yaml"

        self.assertTrue(installed_skill.exists())
        assert_skill_bundle_matches_repo(self, skill_dir)
        self.assertTrue(manifest_path.exists())
        manifest_text = manifest_path.read_text(encoding="utf-8")
        self.assertIn('display_name: "Paper Fetch Skill"', manifest_text)
        self.assertIn("$paper-fetch-skill", manifest_text)
        self.assertIn("full-text availability", manifest_text)
        self.assertFalse((repo_dir / ".venv").exists())
        self.assertFalse((repo_dir / ".env").exists())
        self.assertIn("-m pip install --quiet .", log_path.read_text(encoding="utf-8"))

    def test_codex_installer_can_register_mcp_server(self) -> None:
        _, _, log_path = self.run_installer(
            script_name="install-codex-skill.sh",
            args=["--register-mcp", "--env-file", "/tmp/paper-fetch.env"],
            fake_codex=True,
        )

        log_text = log_path.read_text(encoding="utf-8")
        self.assertIn("codex mcp remove paper-fetch", log_text)
        self.assertIn("--env PAPER_FETCH_ENV_FILE=/tmp/paper-fetch.env", log_text)
        self.assertIn("paper-fetch --", log_text)
        self.assertIn("-X utf8 -m paper_fetch.mcp.server", log_text)
        self.assertNotIn("PLAYWRIGHT_BROWSERS_PATH", log_text)

    def test_claude_installer_does_not_auto_bind_repo_env_for_mcp_registration(self) -> None:
        _, _, log_path = self.run_installer(
            script_name="install-claude-skill.sh",
            args=["--register-mcp", "--mcp-scope", "project"],
            fake_claude=True,
            create_repo_env=True,
        )

        log_text = log_path.read_text(encoding="utf-8")
        self.assertIn("claude mcp add -s project -- paper-fetch", log_text)
        self.assertNotIn("PAPER_FETCH_ENV_FILE=", log_text)

    def test_codex_installer_does_not_auto_bind_repo_env_for_mcp_registration(self) -> None:
        _, _, log_path = self.run_installer(
            script_name="install-codex-skill.sh",
            args=["--register-mcp"],
            fake_codex=True,
            create_repo_env=True,
        )

        log_text = log_path.read_text(encoding="utf-8")
        self.assertIn("codex mcp add paper-fetch --", log_text)
        self.assertIn("paper-fetch --", log_text)
        self.assertIn("-X utf8 -m paper_fetch.mcp.server", log_text)
        self.assertNotIn("PAPER_FETCH_ENV_FILE=", log_text)


if __name__ == "__main__":
    unittest.main()
