"""Regression 6 (회장 verbatim · task-2658).

R1: legacy `.worktrees/` 위치 worktree 존재 → SPAWN_VERIFIED
R2: cokacdir `/home/jay/.cokacdir/workspace/<sid>/wt-<task>-<team>/` → SPAWN_VERIFIED
R3: .worktrees 없고 cokacdir 에만 → SPAWN_VERIFIED (★ false negative 금지 핵심)
R4: executor process 부재 + result/report/done 모두 존재
        → CALLBACK_RECOVERED_AFTER_VISIBILITY_GAP (★ 2 source 교차 강제)
R5: 전 source 부재 + fire 시각으로부터 hard_timeout(30분) 경과 → TRUE_SILENT_DROP
R6: schedule_history pending → SPAWN_PENDING (★ TRUE_SILENT_DROP 단정 금지)

테스트는 sys.path 를 worktree 의 utils 디렉터리 부모로 끌어들여
`utils.anu_spawn_visibility_guard` 를 import 한다 — pyright LSP 가
worktree 를 인덱싱 못 해 import 미해결 경고를 띄울 수 있으나 (task-2657
사례 동일) 실 import 와 pytest 는 정상 동작한다.
"""

from __future__ import annotations

import json
import sys
import unittest
from pathlib import Path

# worktree 의 utils 패키지를 import path 에 추가
_REPO_ROOT = Path(__file__).resolve().parents[2]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from utils.anu_spawn_visibility_guard import (  # noqa: E402
    HARD_TIMEOUT_SECONDS,
    SpawnVisibilityStatus,
    classify_spawn_visibility,
    collect_sources,
)


def _make_layout(tmp: Path):
    """테스트용 격리 root 구성.

    Returns (legacy_root, cokacdir_root, schedule_history_dir, memory_dir).
    """
    legacy = tmp / "workspace" / ".worktrees"
    cokacdir = tmp / "cokacdir" / "workspace"
    history = tmp / "cokacdir" / "schedule_history"
    memory = tmp / "workspace" / "memory"
    for d in (legacy, cokacdir, history, memory / "events", memory / "reports", memory / ".callback_inbox"):
        d.mkdir(parents=True, exist_ok=True)
    return legacy, cokacdir, history, memory


def _empty_ps():
    return ()


def _ps_with(task_short: str, team: str):
    def lister():
        return (
            f"1234 python3 /home/jay/.cokacdir/workspace/SID/wt-{task_short}-{team}/runner.py "
            f"--task-id task-{task_short} --team {team}",
        )

    return lister


class RegressionR1LegacyWorktree(unittest.TestCase):
    """R1: legacy `.worktrees/` 위치 worktree 존재 → SPAWN_VERIFIED."""

    def test_r1(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            (legacy / "wt-2657-dev6").mkdir(parents=True)

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id=None,
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_ps_with("2657", "dev6"),
            )

            self.assertTrue(snapshot.legacy_worktree_present, "legacy worktree 검출 실패")
            self.assertTrue(snapshot.executor_process_present, "executor process 검출 실패")

            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=120,
                first_response_not_refusal=True,
            )

            self.assertEqual(
                decision.status,
                SpawnVisibilityStatus.SPAWN_VERIFIED,
                f"R1 FAIL — got {decision.status} ({decision.reason})",
            )


class RegressionR2CokacdirWorktree(unittest.TestCase):
    """R2: cokacdir 위치 worktree 존재 → SPAWN_VERIFIED."""

    def test_r2(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            sid = "426931FE"
            (cokacdir / sid / "wt-2657-dev6").mkdir(parents=True)
            (legacy / "wt-2657-dev6").mkdir(parents=True)  # 양쪽 모두 존재 케이스

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id=sid,
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_ps_with("2657", "dev6"),
            )

            self.assertTrue(snapshot.cokacdir_worktree_present, "cokacdir worktree 검출 실패")
            self.assertTrue(snapshot.schedule_workspace_dir_present, "schedule_workspace_dir 검출 실패")

            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=120,
                first_response_not_refusal=True,
            )

            self.assertEqual(
                decision.status,
                SpawnVisibilityStatus.SPAWN_VERIFIED,
                f"R2 FAIL — got {decision.status} ({decision.reason})",
            )


class RegressionR3CokacdirOnlyFalseNegativeBlock(unittest.TestCase):
    """R3: .worktrees 없고 cokacdir 에만 → SPAWN_VERIFIED.

    ★ false negative 금지 핵심 (task-2657 사고 직접 재발 방지)
    """

    def test_r3(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            sid = "426931FE"
            # ★ 의도적으로 legacy 미생성. cokacdir 만 생성.
            (cokacdir / sid / "wt-2657-dev6").mkdir(parents=True)

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id=sid,
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_ps_with("2657", "dev6"),
            )

            self.assertFalse(
                snapshot.legacy_worktree_present,
                "legacy 가 비어 있어야 한다 (테스트 셋업 오류)",
            )
            self.assertTrue(
                snapshot.cokacdir_worktree_present,
                "cokacdir worktree 검출 실패 — false negative!",
            )

            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=120,
                first_response_not_refusal=True,
            )

            self.assertEqual(
                decision.status,
                SpawnVisibilityStatus.SPAWN_VERIFIED,
                f"R3 FAIL (false negative!) — got {decision.status} ({decision.reason})",
            )

    def test_r3_without_schedule_id_fallback_glob(self):
        """R3 보강: schedule_id 미제공 시 광역 glob 으로도 SPAWN_VERIFIED."""
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            sid = "AAA12345"
            (cokacdir / sid / "wt-2657-dev6").mkdir(parents=True)

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id=None,  # ★ schedule_id 모름
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_ps_with("2657", "dev6"),
            )

            self.assertTrue(
                snapshot.cokacdir_worktree_present,
                "schedule_id 없이도 광역 glob 으로 cokacdir worktree 잡아야 함",
            )
            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=120,
                first_response_not_refusal=True,
            )
            self.assertEqual(decision.status, SpawnVisibilityStatus.SPAWN_VERIFIED)


class RegressionR4CallbackRecoveredAfterGap(unittest.TestCase):
    """R4: process 부재 + result/report/done 모두 존재
       → CALLBACK_RECOVERED_AFTER_VISIBILITY_GAP (★ 2 source 교차 강제)."""

    def test_r4(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            task_id = "task-2657"

            (memory / "events" / f"{task_id}.done").write_text("done", encoding="utf-8")
            (memory / "events" / f"{task_id}.axis-3-result-260525.json").write_text(
                json.dumps({"task_id": task_id, "completion_status": "ok"}),
                encoding="utf-8",
            )
            (memory / "reports" / f"{task_id}.md").write_text("# report", encoding="utf-8")

            snapshot = collect_sources(
                task_id,
                "dev6",
                schedule_id=None,
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_empty_ps,  # ★ executor process 부재
            )

            self.assertFalse(snapshot.executor_process_present, "process 부재 셋업 오류")
            self.assertTrue(snapshot.done_present)
            self.assertTrue(snapshot.result_present)
            self.assertTrue(snapshot.report_present)
            self.assertGreaterEqual(
                len(snapshot.callback_evidence_sources()),
                2,
                "callback evidence 2 source 교차 미충족",
            )

            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=600,
                initial_anu_absent_observed=True,
                first_response_not_refusal=False,
            )

            self.assertEqual(
                decision.status,
                SpawnVisibilityStatus.CALLBACK_RECOVERED_AFTER_VISIBILITY_GAP,
                f"R4 FAIL — got {decision.status} ({decision.reason})",
            )
            self.assertGreaterEqual(decision.crossed_sources_count, 2)


class RegressionR5TrueSilentDrop(unittest.TestCase):
    """R5: 전 source 부재 + 30분 경과 → TRUE_SILENT_DROP."""

    def test_r5(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id="DEADBEEF",  # ★ history 파일 자체 없음
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_empty_ps,
            )

            self.assertEqual(snapshot.positive_sources(), ())
            self.assertEqual(snapshot.callback_evidence_sources(), ())

            decision = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=HARD_TIMEOUT_SECONDS + 60,
                initial_anu_absent_observed=True,
                first_response_not_refusal=False,
            )

            self.assertEqual(
                decision.status,
                SpawnVisibilityStatus.TRUE_SILENT_DROP,
                f"R5 FAIL — got {decision.status} ({decision.reason})",
            )
            self.assertTrue(decision.timeout.silent_drop_eligible)
            self.assertEqual(decision.timeout.blocking_exceptions, ())


class RegressionR6SchedulePendingBlocksSilentDrop(unittest.TestCase):
    """R6: schedule_history pending → SPAWN_PENDING (★ TRUE_SILENT_DROP 단정 금지)."""

    def test_r6(self):
        import tempfile

        with tempfile.TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            legacy, cokacdir, history, memory = _make_layout(tmp)
            sid = "PENDING1"

            (history / f"{sid}.log").write_text(
                json.dumps({"ts": "2026-05-25T12:00:00+09:00", "status": "pending", "schedule_id": sid}) + "\n",
                encoding="utf-8",
            )

            snapshot = collect_sources(
                "task-2657",
                "dev6",
                schedule_id=sid,
                legacy_root=legacy,
                cokacdir_root=cokacdir,
                schedule_history_dir=history,
                memory_dir=memory,
                process_lister=_empty_ps,
            )

            self.assertTrue(snapshot.schedule_history_present)
            self.assertEqual(snapshot.schedule_history_last_status, "pending")

            # timeout 미경과
            decision_short = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=120,
                first_response_not_refusal=False,
            )
            self.assertEqual(
                decision_short.status,
                SpawnVisibilityStatus.SPAWN_PENDING,
                f"R6 FAIL — got {decision_short.status} ({decision_short.reason})",
            )

            # 30분 경과해도 pending 예외로 silent drop 단정 금지
            decision_long = classify_spawn_visibility(
                snapshot,
                elapsed_since_fire_seconds=HARD_TIMEOUT_SECONDS + 60,
                first_response_not_refusal=False,
            )
            self.assertNotEqual(
                decision_long.status,
                SpawnVisibilityStatus.TRUE_SILENT_DROP,
                "R6 FAIL — pending 상태에서 TRUE_SILENT_DROP 으로 단정됨!",
            )
            self.assertFalse(
                decision_long.timeout.silent_drop_eligible,
                "timeout gate 가 pending 예외를 통과시킴",
            )


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