"""Tests for cleanup-workspace.py — TDD 방식. tmp_path 로 격리."""

import importlib.util, os, sys, time
from pathlib import Path
import pytest

sys.path.insert(0, str(Path(__file__).parent.parent))
_spec = importlib.util.spec_from_file_location(
    "cleanup_workspace", Path(__file__).parent.parent / "cleanup-workspace.py"
)
assert _spec and _spec.loader
cw = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(cw)  # type: ignore[union-attr]


# --- helpers -----------------------------------------------------------------

def _mk(path: Path, days: float = 0, content: str = "x") -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content)
    if days:
        ts = time.time() - days * 86400
        os.utime(path, (ts, ts))
    return path


def _mk_dir(path: Path, days: float = 0) -> Path:
    path.mkdir(parents=True, exist_ok=True)
    if days:
        ts = time.time() - days * 86400
        os.utime(path, (ts, ts))
    return path


# --- 1. workspace/tmp/ (7일) -------------------------------------------------

class TestTmp:
    def test_old_is_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "tmp" / "old.txt", days=8)
        assert f in cw.find_tmp_candidates(tmp_path)

    def test_recent_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "tmp" / "new.txt", days=3)
        assert f not in cw.find_tmp_candidates(tmp_path)

    def test_exactly_7_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "tmp" / "border.txt", days=7)
        assert f not in cw.find_tmp_candidates(tmp_path)

    def test_missing_dir_empty(self, tmp_path: Path) -> None:
        assert cw.find_tmp_candidates(tmp_path) == []


# --- 2. memory/events/*.done.clear / *.done.acked (30일) --------------------

class TestEvents:
    def test_old_done_clear(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "events" / "t.done.clear", days=31)
        assert f in cw.find_events_candidates(tmp_path)

    def test_old_done_acked(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "events" / "t.done.acked", days=35)
        assert f in cw.find_events_candidates(tmp_path)

    def test_recent_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "events" / "t.done.clear", days=10)
        assert f not in cw.find_events_candidates(tmp_path)

    def test_other_suffix_ignored(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "events" / "t.done.notified", days=40)
        assert f not in cw.find_events_candidates(tmp_path)


# --- 3. memory/tasks/dispatch-*.md (90일) — task-*.md 절대 금지 -------------

class TestDispatch:
    def test_old_dispatch_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "tasks" / "dispatch-001.md", days=91)
        assert f in cw.find_dispatch_candidates(tmp_path)

    def test_recent_dispatch_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "tasks" / "dispatch-002.md", days=30)
        assert f not in cw.find_dispatch_candidates(tmp_path)

    def test_task_md_never_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "tasks" / "task-999.md", days=200)
        assert f not in cw.find_dispatch_candidates(tmp_path)

    def test_exactly_90_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "tasks" / "dispatch-003.md", days=90)
        assert f not in cw.find_dispatch_candidates(tmp_path)


# --- 4. logs/ (60일) — cleanup-*.log 제외 -----------------------------------

class TestLogs:
    def test_old_log_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "logs" / "app.log", days=61)
        assert f in cw.find_logs_candidates(tmp_path)

    def test_recent_log_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "logs" / "recent.log", days=5)
        assert f not in cw.find_logs_candidates(tmp_path)

    def test_cleanup_log_excluded(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "logs" / "cleanup-2025-01-01.log", days=100)
        assert f not in cw.find_logs_candidates(tmp_path)


# --- 5. memory/backups/ (90일) -----------------------------------------------

class TestBackups:
    def test_old_backup_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "backups" / "old.tar.gz", days=91)
        assert f in cw.find_backups_candidates(tmp_path)

    def test_recent_backup_not_candidate(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "memory" / "backups" / "new.tar.gz", days=10)
        assert f not in cw.find_backups_candidates(tmp_path)


# --- 6. teams/dev*/task-*/ (완료+30일, 보고서 필수) -------------------------

class TestTeamTask:
    def _setup(self, tmp_path: Path, team: str, task: str,
               days: float, has_report: bool) -> Path:
        task_dir = tmp_path / "teams" / team / task
        _mk(task_dir / "work.md", days=days)
        ts = time.time() - days * 86400
        os.utime(task_dir, (ts, ts))
        if has_report:
            _mk(tmp_path / "memory" / "reports" / f"{task}-rpt.md")
        return task_dir

    def test_old_with_report(self, tmp_path: Path) -> None:
        d = self._setup(tmp_path, "dev7", "task-42", days=31, has_report=True)
        assert d in cw.find_team_task_candidates(tmp_path)

    def test_old_without_report(self, tmp_path: Path) -> None:
        d = self._setup(tmp_path, "dev7", "task-43", days=31, has_report=False)
        assert d not in cw.find_team_task_candidates(tmp_path)

    def test_recent_with_report(self, tmp_path: Path) -> None:
        d = self._setup(tmp_path, "dev3", "task-44", days=10, has_report=True)
        assert d not in cw.find_team_task_candidates(tmp_path)

    def test_non_task_dir_ignored(self, tmp_path: Path) -> None:
        d = _mk_dir(tmp_path / "teams" / "dev1" / "shared", days=100)
        assert d not in cw.find_team_task_candidates(tmp_path)


# --- 7. cokacdir 업로드 (30일) -----------------------------------------------

class TestCokacDir:
    def test_old_upload_candidate(self, tmp_path: Path) -> None:
        ws = tmp_path / ".cokacdir" / "workspace"
        f = _mk(ws / "PROJ" / "file.pdf", days=31)
        assert f in cw.find_cokacdir_candidates(ws)

    def test_recent_not_candidate(self, tmp_path: Path) -> None:
        ws = tmp_path / ".cokacdir" / "workspace"
        f = _mk(ws / "PROJ" / "file.pdf", days=5)
        assert f not in cw.find_cokacdir_candidates(ws)

    def test_exactly_30_not_candidate(self, tmp_path: Path) -> None:
        ws = tmp_path / ".cokacdir" / "workspace"
        f = _mk(ws / "PROJ" / "border.pdf", days=30)
        assert f not in cw.find_cokacdir_candidates(ws)

    def test_missing_dir_empty(self, tmp_path: Path) -> None:
        assert cw.find_cokacdir_candidates(tmp_path / ".cokacdir" / "workspace") == []


# --- 8. 삭제 금지 -----------------------------------------------------------

class TestProtected:
    def test_protected_names(self) -> None:
        for name in ["CLAUDE.md", "MEMORY.md", ".env", ".env.keys",
                     "organization-structure.json", "task-timers.json", "token-ledger.json"]:
            assert name in cw.PROTECTED_NAMES

    def test_protected_dirs(self) -> None:
        for d in ["memory/reports", "memory/research", "memory/specs",
                  "memory/meetings", "memory/plans"]:
            assert d in cw.PROTECTED_DIRS

    def test_env_is_protected(self, tmp_path: Path) -> None:
        assert cw.is_protected(_mk(tmp_path / ".env"), tmp_path)

    def test_reports_subfile_protected(self, tmp_path: Path) -> None:
        assert cw.is_protected(_mk(tmp_path / "memory" / "reports" / "rpt.md"), tmp_path)

    def test_normal_file_not_protected(self, tmp_path: Path) -> None:
        assert not cw.is_protected(_mk(tmp_path / "tmp" / "garbage.txt"), tmp_path)


# --- 9. --dry-run 모드 -------------------------------------------------------

class TestDryRun:
    def test_no_deletion(self, tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
        f = _mk(tmp_path / "tmp" / "stale.txt", days=10)
        cw.run_cleanup(workspace=tmp_path, dry_run=True,
                       cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert f.exists()

    def test_output_mentions_candidate(self, tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
        _mk(tmp_path / "tmp" / "stale.txt", days=10)
        cw.run_cleanup(workspace=tmp_path, dry_run=True,
                       cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        out = capsys.readouterr().out
        assert "stale.txt" in out


# --- 10. --execute 모드 ------------------------------------------------------

class TestExecute:
    def test_deletes_old_tmp(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "tmp" / "old.txt", days=10)
        cw.run_cleanup(workspace=tmp_path, dry_run=False,
                       cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert not f.exists()

    def test_preserves_env(self, tmp_path: Path) -> None:
        env = _mk(tmp_path / ".env", days=999)
        cw.run_cleanup(workspace=tmp_path, dry_run=False,
                       cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert env.exists()

    def test_preserves_recent_tmp(self, tmp_path: Path) -> None:
        f = _mk(tmp_path / "tmp" / "fresh.txt", days=2)
        cw.run_cleanup(workspace=tmp_path, dry_run=False,
                       cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert f.exists()


# --- 11. --report 모드 -------------------------------------------------------

class TestReport:
    def test_report_keys(self, tmp_path: Path) -> None:
        r = cw.build_report(workspace=tmp_path,
                            cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert {"total_candidates", "total_size_bytes", "categories"} <= r.keys()

    def test_report_counts(self, tmp_path: Path) -> None:
        _mk(tmp_path / "tmp" / "a.txt", days=10)
        _mk(tmp_path / "tmp" / "b.txt", days=15)
        r = cw.build_report(workspace=tmp_path,
                            cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert r["total_candidates"] >= 2

    def test_report_size_positive(self, tmp_path: Path) -> None:
        _mk(tmp_path / "tmp" / "big.txt", days=10, content="x" * 1024)
        r = cw.build_report(workspace=tmp_path,
                            cokacdir_ws=tmp_path / ".cokacdir" / "workspace")
        assert r["total_size_bytes"] > 0
