from __future__ import annotations

import subprocess
import sys
from pathlib import Path

import pytest

from trae_workspace_tool.generator import REQUIRED_FILES, inspect_workspace, install_workspace
from trae_workspace_tool.models import Asset
from trae_workspace_tool.registry import (
    RegistryError,
    ensure_unique_paths,
    list_packs,
    list_profiles,
    selected_assets,
)


def run_cli(*args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [
            sys.executable,
            "-c",
            "from trae_workspace_tool.cli import main; raise SystemExit(main())",
            *args,
        ],
        check=False,
        text=True,
        capture_output=True,
    )


def test_registry_lists_internal_basic_profile_and_ops_pack() -> None:
    assert [profile.name for profile in list_profiles()] == ["internal-basic"]
    assert [pack.name for pack in list_packs()] == ["ops"]


def test_selected_assets_include_profile_and_pack_assets() -> None:
    paths = [asset.relative_path for asset in selected_assets()]

    assert paths == REQUIRED_FILES
    assert ".trae/rules/enterprise-safety.md" in paths
    assert ".trae/rules/ops-safety.md" in paths
    assert "scripts/maintenance_check.py" in paths


def test_duplicate_asset_paths_fail_fast() -> None:
    with pytest.raises(RegistryError, match="duplicate asset"):
        ensure_unique_paths((Asset("same/path.md", "one"), Asset("same/path.md", "two")))


def test_init_creates_profile_rules_pack_skill_agent_and_script(tmp_path: Path) -> None:
    result = install_workspace(tmp_path, force=False)

    assert result.created == REQUIRED_FILES
    for relative_path in REQUIRED_FILES:
        assert (tmp_path / relative_path).exists(), relative_path

    enterprise_rule = (tmp_path / ".trae/rules/enterprise-safety.md").read_text(
        encoding="utf-8"
    )
    assert "alwaysApply: true" in enterprise_rule
    assert "禁止读取" in enterprise_rule
    assert "禁止将内部代码" in enterprise_rule

    ops_rule = (tmp_path / ".trae/rules/ops-safety.md").read_text(encoding="utf-8")
    assert "alwaysApply: true" in ops_rule
    assert "不删除、不移动、不重启" in ops_rule

    skill = (tmp_path / ".trae/skills/ops-maintenance-automation/SKILL.md").read_text(
        encoding="utf-8"
    )
    assert "name: ops-maintenance-automation" in skill
    assert "## Instructions" in skill


def test_explicit_profile_and_pack_match_default_assets(tmp_path: Path) -> None:
    default_target = tmp_path / "default"
    explicit_target = tmp_path / "explicit"

    default_result = install_workspace(default_target, force=False)
    explicit_result = install_workspace(
        explicit_target,
        force=False,
        profile="internal-basic",
        pack="ops",
    )

    assert default_result.created == explicit_result.created
    for relative_path in REQUIRED_FILES:
        assert (default_target / relative_path).read_text(encoding="utf-8") == (
            explicit_target / relative_path
        ).read_text(encoding="utf-8")


def test_init_does_not_overwrite_without_force(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    rule_path = tmp_path / ".trae/rules/ops-safety.md"
    rule_path.write_text("custom", encoding="utf-8")

    result = install_workspace(tmp_path, force=False)

    assert str(rule_path.relative_to(tmp_path)) in result.skipped
    assert rule_path.read_text(encoding="utf-8") == "custom"


def test_force_overwrites_existing_files(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    rule_path = tmp_path / ".trae/rules/ops-safety.md"
    rule_path.write_text("custom", encoding="utf-8")

    install_workspace(tmp_path, force=True)

    assert "alwaysApply: true" in rule_path.read_text(encoding="utf-8")


def test_doctor_reports_missing_present_and_invalid_files(tmp_path: Path) -> None:
    missing_report = inspect_workspace(tmp_path)
    assert not missing_report.ok
    assert ".trae/rules/enterprise-safety.md" in missing_report.missing
    assert ".trae/rules/ops-safety.md" in missing_report.missing

    install_workspace(tmp_path, force=False)
    present_report = inspect_workspace(tmp_path)

    assert present_report.ok
    assert present_report.missing == []
    assert present_report.invalid == []

    enterprise_rule = tmp_path / ".trae/rules/enterprise-safety.md"
    enterprise_rule.write_text("# missing frontmatter\n", encoding="utf-8")
    invalid_report = inspect_workspace(tmp_path)

    assert not invalid_report.ok
    assert invalid_report.invalid[0].relative_path == ".trae/rules/enterprise-safety.md"
    assert invalid_report.invalid[0].reason == "missing alwaysApply: true"


def test_doctor_reports_missing_skill_name_and_script_safety_marker(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    skill_path = tmp_path / ".trae/skills/ops-maintenance-automation/SKILL.md"
    script_path = tmp_path / "scripts/maintenance_check.py"
    skill_path.write_text("---\ndescription: broken\n---\n", encoding="utf-8")
    script_path.write_text("#!/usr/bin/env python3\nprint('broken')\n", encoding="utf-8")

    report = inspect_workspace(tmp_path)
    issues = {(item.relative_path, item.reason) for item in report.invalid}

    assert not report.ok
    assert (
        ".trae/skills/ops-maintenance-automation/SKILL.md",
        "missing skill name",
    ) in issues
    assert ("scripts/maintenance_check.py", "missing root containment helper") in issues
    assert ("scripts/maintenance_check.py", "missing default dry-run marker") in issues


def test_cli_lists_profiles_and_packs() -> None:
    profiles_run = run_cli("list-profiles")
    packs_run = run_cli("list-packs")

    assert profiles_run.returncode == 0
    assert "internal-basic:" in profiles_run.stdout
    assert packs_run.returncode == 0
    assert "ops:" in packs_run.stdout


def test_cli_reports_unknown_profile_or_pack_without_traceback(tmp_path: Path) -> None:
    unknown_profile = run_cli("init", "--target", str(tmp_path), "--profile", "missing")
    unknown_pack = run_cli("doctor", "--target", str(tmp_path), "--pack", "missing")

    assert unknown_profile.returncode == 1
    assert "unknown profile: missing" in unknown_profile.stderr
    assert "Traceback" not in unknown_profile.stderr
    assert unknown_pack.returncode == 1
    assert "unknown pack: missing" in unknown_pack.stderr
    assert "Traceback" not in unknown_pack.stderr


def test_cli_reports_os_errors_without_traceback(tmp_path: Path) -> None:
    target_file = tmp_path / "not-a-directory"
    target_file.write_text("content", encoding="utf-8")

    run = run_cli("init", "--target", str(target_file))

    assert run.returncode == 1
    assert "error:" in run.stderr
    assert "Traceback" not in run.stderr


def test_generated_maintenance_script_is_dry_run_by_default_and_validates_paths(
    tmp_path: Path,
) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    (root / "backups").mkdir(parents=True)
    (root / "logs").mkdir()
    stale = root / "projects" / "tmp-old"
    stale.mkdir(parents=True)
    (root / "logs" / "app.log").write_bytes(b"x" * 2 * 1024 * 1024)

    script = tmp_path / "scripts/maintenance_check.py"
    ok_run = subprocess.run(
        [
            sys.executable,
            str(script),
            "--root",
            str(root),
            "--max-log-mb",
            "1",
            "--stale-days",
            "0",
        ],
        check=False,
        text=True,
        capture_output=True,
    )

    assert ok_run.returncode == 2
    assert "[OK] backup_dir" in ok_run.stdout
    assert "[WARN] large_logs" in ok_run.stdout
    assert "[DRY-RUN] stale_projects: would archive" in ok_run.stdout
    assert stale.exists()

    bad_run = subprocess.run(
        [sys.executable, str(script), "--root", str(tmp_path / "missing")],
        check=False,
        text=True,
        capture_output=True,
    )

    assert bad_run.returncode == 1
    assert "[ERROR] validation" in bad_run.stdout


def test_generated_maintenance_script_accepts_dry_run_flag(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    (root / "backups").mkdir(parents=True)
    (root / "logs").mkdir()
    (root / "projects" / "tmp-old").mkdir(parents=True)

    script = tmp_path / "scripts/maintenance_check.py"
    with_flag = subprocess.run(
        [sys.executable, str(script), "--root", str(root), "--dry-run", "--stale-days", "0"],
        check=False,
        text=True,
        capture_output=True,
    )
    without_flag = subprocess.run(
        [sys.executable, str(script), "--root", str(root), "--stale-days", "0"],
        check=False,
        text=True,
        capture_output=True,
    )

    assert with_flag.returncode == 0
    assert without_flag.returncode == 0
    assert "[DRY-RUN] stale_projects" in with_flag.stdout
    assert "[DRY-RUN] stale_projects" in without_flag.stdout


def test_generated_maintenance_script_skips_log_directories(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    (root / "backups").mkdir(parents=True)
    (root / "logs" / "archived.log").mkdir(parents=True)
    (root / "projects").mkdir()

    script = tmp_path / "scripts/maintenance_check.py"
    run = subprocess.run(
        [
            sys.executable,
            str(script),
            "--root",
            str(root),
            "--dry-run",
            "--max-log-mb",
            "0",
            "--stale-days",
            "999",
        ],
        check=False,
        text=True,
        capture_output=True,
    )

    assert run.returncode == 0
    assert "archived.log" not in run.stdout
    assert "[OK] large_logs: no oversized log files" in run.stdout


def test_generated_maintenance_script_skips_symlinks_outside_root(
    tmp_path: Path,
) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    outside = tmp_path / "outside"
    (root / "backups").mkdir(parents=True)
    (root / "logs").mkdir()
    (root / "projects").mkdir()
    outside_project = outside / "project"
    outside_project.mkdir(parents=True)
    outside_log = outside / "secret.log"
    outside_log.write_bytes(b"x" * 2 * 1024 * 1024)
    (root / "logs" / "outside.log").symlink_to(outside_log)
    (root / "projects" / "outside-project").symlink_to(
        outside_project,
        target_is_directory=True,
    )

    script = tmp_path / "scripts/maintenance_check.py"
    run = subprocess.run(
        [
            sys.executable,
            str(script),
            "--root",
            str(root),
            "--dry-run",
            "--max-log-mb",
            "1",
            "--stale-days",
            "0",
        ],
        check=False,
        text=True,
        capture_output=True,
    )

    assert run.returncode == 2
    assert "[WARN] large_logs: skipped path outside root: outside.log" in run.stdout
    assert (
        "[WARN] stale_projects: skipped path outside root: outside-project"
        in run.stdout
    )
    assert "outside.log is" not in run.stdout
    assert "would archive" not in run.stdout


def test_generated_maintenance_script_custom_dirs(tmp_path: Path) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    (root / "my-backups").mkdir(parents=True)
    (root / "my-logs").mkdir()
    (root / "my-projects" / "stale-work").mkdir(parents=True)

    script = tmp_path / "scripts/maintenance_check.py"
    run = subprocess.run(
        [
            sys.executable,
            str(script),
            "--root",
            str(root),
            "--dry-run",
            "--stale-days",
            "0",
            "--backup-dir",
            "my-backups",
            "--logs-dir",
            "my-logs",
            "--projects-dir",
            "my-projects",
        ],
        check=False,
        text=True,
        capture_output=True,
    )

    assert run.returncode == 0
    assert "[OK] backup_dir" in run.stdout
    assert "[OK] large_logs" in run.stdout
    assert "[DRY-RUN] stale_projects: would archive" in run.stdout
    assert "stale-work" in run.stdout


def test_generated_maintenance_script_exclude_flag_skips_named_directory(
    tmp_path: Path,
) -> None:
    install_workspace(tmp_path, force=False)
    root = tmp_path / "sample_home_lab"
    (root / "backups").mkdir(parents=True)
    (root / "logs").mkdir()
    (root / "projects" / "keep-this").mkdir(parents=True)
    (root / "projects" / "tmp-old").mkdir(parents=True)

    script = tmp_path / "scripts/maintenance_check.py"
    run = subprocess.run(
        [
            sys.executable,
            str(script),
            "--root",
            str(root),
            "--dry-run",
            "--stale-days",
            "0",
            "--exclude",
            "keep-this",
        ],
        check=False,
        text=True,
        capture_output=True,
    )

    assert "[SKIP] stale_projects: excluded keep-this" in run.stdout
    assert "[DRY-RUN] stale_projects: would archive" in run.stdout
    assert "tmp-old" in run.stdout
