"""
test_signature_check.py — signature_check verifier + verify-done.py 테스트

작성자: 아르고스 (개발1팀 테스터)
task-1972: .done 자동 검증 게이트 구현 테스트 (TDD RED)

테스트 대상:
  - teams/shared/verifiers/signature_check.py :: verify(task_id, workspace_root)
  - scripts/verify-done.py :: verify_done(task_id, workspace_root)
"""

import sys
import subprocess
from pathlib import Path
from unittest.mock import patch, MagicMock

import pytest

# ---------------------------------------------------------------------------
# 모듈 임포트 헬퍼 (파일이 아직 없으면 ImportError → RED 상태에서 명확히 실패)
# ---------------------------------------------------------------------------

def _import_signature_check():
    sys.path.insert(0, "/home/jay/workspace/teams/shared/verifiers")
    import importlib
    return importlib.import_module("signature_check")


def _import_verify_done():
    sys.path.insert(0, "/home/jay/workspace/scripts")
    import importlib
    return importlib.import_module("verify-done".replace("-", "_"))


# ---------------------------------------------------------------------------
# 공통 헬퍼: 임시 디렉토리에 task 파일/코드 파일 생성
# ---------------------------------------------------------------------------

def _make_task_file(
    tmp_path: Path,
    task_id: str,
    level: str = "",
    signatures: list[str] | None = None,
) -> Path:
    """tmp_path/memory/tasks/{task_id}.md 생성."""
    tasks_dir = tmp_path / "memory" / "tasks"
    tasks_dir.mkdir(parents=True, exist_ok=True)

    lines = [f"# {task_id}: 테스트용 task\n\n## 목적\n테스트.\n"]

    if level:
        lines.append(f"\n## 레벨\n- {level}\n")

    if signatures is not None:
        lines.append("\n## 완료 시그니처\n")
        for sig in signatures:
            lines.append(f"- {sig}\n")

    content = "".join(lines)
    task_file = tasks_dir / f"{task_id}.md"
    task_file.write_text(content, encoding="utf-8")
    return task_file


def _make_code_file(tmp_path: Path, rel_path: str, content: str) -> Path:
    """tmp_path 하위에 코드 파일 생성."""
    target = tmp_path / rel_path
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content, encoding="utf-8")
    return target


def _make_done_file(tmp_path: Path, task_id: str) -> Path:
    """tmp_path/memory/events/{task_id}.done 생성."""
    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    done_file = events_dir / f"{task_id}.done"
    done_file.write_text(f"done: {task_id}\n", encoding="utf-8")
    return done_file


# ===========================================================================
# signature_check.verify() 테스트
# ===========================================================================

class TestSignatureCheckVerify:
    """signature_check.verify() 유닛 테스트."""

    def test_signature_pass_grep_exists(self, tmp_path: Path):
        """시나리오 1: grep 패턴이 파일에 존재 → PASS"""
        sc = _import_signature_check()

        task_id = "task-test-pass"
        code_rel = "scripts/sample.py"
        code_content = "def my_function():\n    return True\n"
        _make_code_file(tmp_path, code_rel, code_content)

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[f"[grep] `def my_function` @ `{code_rel}`"],
        )

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "PASS", f"PASS 기대, 실제: {result}"
        assert "details" in result

    def test_signature_fail_grep_missing(self, tmp_path: Path):
        """시나리오 2: grep 패턴이 파일에 없음 → FAIL"""
        sc = _import_signature_check()

        task_id = "task-test-fail"
        code_rel = "scripts/sample.py"
        _make_code_file(tmp_path, code_rel, "def other_function():\n    pass\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[f"[grep] `def missing_function` @ `{code_rel}`"],
        )

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "FAIL", f"FAIL 기대, 실제: {result}"
        assert "details" in result

    def test_signature_skip_no_section(self, tmp_path: Path):
        """시나리오 3: 시그니처 섹션 없는 기존 task → SKIP (하위 호환)"""
        sc = _import_signature_check()

        task_id = "task-old-no-sig"
        # 시그니처 섹션 없이 task 파일 생성
        _make_task_file(tmp_path, task_id)

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "SKIP", f"SKIP 기대, 실제: {result}"
        assert "details" in result

    def test_level_normal_grep_only(self, tmp_path: Path):
        """시나리오 4: Lv.1-2 (normal) task → [pytest] 시그니처는 무시, grep만 검증"""
        sc = _import_signature_check()

        task_id = "task-lv1-normal"
        code_rel = "scripts/feature.py"
        _make_code_file(tmp_path, code_rel, "FEATURE_FLAG = True\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[
                f"[grep] `FEATURE_FLAG` @ `{code_rel}`",
                # pytest 시그니처는 Lv.1-2에서 무시되어야 함
                "[pytest] `tests/test_feature.py::test_something`",
            ],
        )

        # pytest mock — 실제 pytest 실행 없이 호출 여부만 확인
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
            result = sc.verify(task_id, workspace_root=str(tmp_path))

        # normal 레벨에서는 PASS (grep 성공) 또는 최소한 pytest를 별도로 강제하지 않음
        # pytest 호출이 없었거나, grep만으로 PASS 판정
        assert result["status"] in ("PASS", "SKIP"), (
            f"normal 레벨에서 PASS 또는 SKIP 기대, 실제: {result}"
        )

        # subprocess.run 호출 중 pytest 호출이 없어야 함
        pytest_calls = [
            call for call in mock_run.call_args_list
            if call.args and isinstance(call.args[0], list) and "pytest" in call.args[0]
        ]
        assert len(pytest_calls) == 0, (
            f"Lv.1-2 (normal)에서 pytest 호출이 있으면 안 됨: {pytest_calls}"
        )

    def test_level_critical_all(self, tmp_path: Path):
        """시나리오 5: Lv.3+ (critical) → [grep] + [pytest] 모두 검증"""
        sc = _import_signature_check()

        task_id = "task-lv3-critical"
        code_rel = "scripts/secure.py"
        _make_code_file(tmp_path, code_rel, "def validate_token(): pass\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="critical",
            signatures=[
                f"[grep] `def validate_token` @ `{code_rel}`",
                "[pytest] `tests/test_secure.py::test_validate`",
            ],
        )

        pytest_called = []

        def fake_run(cmd, *args, **kwargs):
            cmd_str = str(cmd)
            if "pytest" in cmd_str:
                pytest_called.append(cmd)
                return MagicMock(returncode=0, stdout="1 passed", stderr="")
            # 실제 grep 실행 위임
            return subprocess.run.__wrapped__(cmd, *args, **kwargs) if hasattr(subprocess.run, "__wrapped__") else MagicMock(returncode=0, stdout="match", stderr="")

        with patch("subprocess.run", side_effect=fake_run):
            result = sc.verify(task_id, workspace_root=str(tmp_path))

        # critical 레벨에서는 pytest가 호출되어야 함
        assert len(pytest_called) > 0, (
            f"Lv.3+ (critical)에서 pytest 호출이 있어야 함"
        )
        assert result["status"] in ("PASS", "FAIL"), (
            f"critical 레벨에서 PASS 또는 FAIL 기대, 실제: {result}"
        )

    def test_grep_signature_multiple(self, tmp_path: Path):
        """여러 grep 시그니처 중 하나라도 실패하면 FAIL"""
        sc = _import_signature_check()

        task_id = "task-multi-grep"
        code_rel = "scripts/multi.py"
        # 첫 번째 패턴만 존재, 두 번째는 없음
        _make_code_file(tmp_path, code_rel, "def present_func():\n    pass\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[
                f"[grep] `def present_func` @ `{code_rel}`",
                f"[grep] `def absent_func` @ `{code_rel}`",
            ],
        )

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "FAIL", (
            f"일부 grep 실패 시 FAIL 기대, 실제: {result}"
        )

    def test_signature_file_not_found(self, tmp_path: Path):
        """grep 대상 파일이 존재하지 않으면 FAIL"""
        sc = _import_signature_check()

        task_id = "task-no-file"
        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=["[grep] `some_pattern` @ `scripts/nonexistent.py`"],
        )
        # 코드 파일을 생성하지 않음

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "FAIL", (
            f"파일 미존재 시 FAIL 기대, 실제: {result}"
        )

    def test_task_file_not_found_returns_skip(self, tmp_path: Path):
        """task 파일 자체가 없으면 SKIP (graceful degradation)"""
        sc = _import_signature_check()

        task_id = "task-ghost"
        # task 파일 생성 안 함

        result = sc.verify(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "SKIP", (
            f"task 파일 없을 때 SKIP 기대, 실제: {result}"
        )


# ===========================================================================
# verify-done.py :: verify_done() 테스트
# ===========================================================================

class TestVerifyDone:
    """verify-done.py verify_done() 유닛 테스트."""

    def test_verify_done_pass(self, tmp_path: Path):
        """.done 파일 + 시그니처 검증 PASS → .done 유지, status=PASS, action=keep"""
        vd = _import_verify_done()

        task_id = "task-vd-pass"
        code_rel = "scripts/app.py"
        _make_code_file(tmp_path, code_rel, "def main(): pass\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[f"[grep] `def main` @ `{code_rel}`"],
        )
        done_file = _make_done_file(tmp_path, task_id)

        result = vd.verify_done(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "PASS", f"PASS 기대, 실제: {result}"
        assert result["action"] == "keep", f"action=keep 기대, 실제: {result}"
        assert "details" in result

        # .done 파일은 유지되어야 함
        assert done_file.exists(), ".done 파일이 유지되어야 함"
        # .done.rejected 파일이 생성되면 안 됨
        rejected_file = done_file.parent / f"{task_id}.done.rejected"
        assert not rejected_file.exists(), ".done.rejected 파일이 생성되면 안 됨"

    def test_verify_done_fail_creates_rejected(self, tmp_path: Path):
        """.done 파일 + 시그니처 검증 FAIL → .done.rejected 생성, action=reject"""
        vd = _import_verify_done()

        task_id = "task-vd-fail"
        code_rel = "scripts/broken.py"
        # grep 패턴이 없는 파일
        _make_code_file(tmp_path, code_rel, "# empty\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[f"[grep] `def required_function` @ `{code_rel}`"],
        )
        done_file = _make_done_file(tmp_path, task_id)

        result = vd.verify_done(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "FAIL", f"FAIL 기대, 실제: {result}"
        assert result["action"] == "reject", f"action=reject 기대, 실제: {result}"
        assert "details" in result

        # failed_signatures 키가 result에 포함되어야 함
        assert "failed_signatures" in result, (
            f"FAIL 시 failed_signatures 포함 필요: {result}"
        )
        assert len(result["failed_signatures"]) > 0, (
            f"실패한 시그니처가 1개 이상이어야 함: {result}"
        )

        # .done.rejected 파일이 생성되어야 함
        events_dir = tmp_path / "memory" / "events"
        rejected_file = events_dir / f"{task_id}.done.rejected"
        assert rejected_file.exists(), ".done.rejected 파일이 생성되어야 함"

    def test_verify_done_skip_no_signature(self, tmp_path: Path):
        """시그니처 섹션 없는 task → SKIP, action=skip (하위 호환)"""
        vd = _import_verify_done()

        task_id = "task-vd-skip"
        # 시그니처 섹션 없이 task 생성
        _make_task_file(tmp_path, task_id)
        _make_done_file(tmp_path, task_id)

        result = vd.verify_done(task_id, workspace_root=str(tmp_path))

        assert result["status"] == "SKIP", f"SKIP 기대, 실제: {result}"
        assert result["action"] == "skip", f"action=skip 기대, 실제: {result}"
        assert "details" in result

    def test_verify_done_rejected_contains_reject_reason(self, tmp_path: Path):
        """.done.rejected 생성 시 거절 사유가 파일에 기록되어야 함"""
        vd = _import_verify_done()

        task_id = "task-vd-reason"
        code_rel = "scripts/impl.py"
        _make_code_file(tmp_path, code_rel, "# no matching content\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="normal",
            signatures=[f"[grep] `class MyClass` @ `{code_rel}`"],
        )
        _make_done_file(tmp_path, task_id)

        result = vd.verify_done(task_id, workspace_root=str(tmp_path))

        # .done.rejected 파일 내용 확인
        events_dir = tmp_path / "memory" / "events"
        rejected_file = events_dir / f"{task_id}.done.rejected"

        if rejected_file.exists():
            content = rejected_file.read_text(encoding="utf-8")
            # 거절 파일에 task_id 또는 실패 사유가 포함되어야 함
            assert task_id in content or "FAIL" in content or "class MyClass" in content, (
                f".done.rejected 파일에 사유 기록 필요. 내용: {content!r}"
            )

    def test_verify_done_autodelegate_message_on_rejected(self, tmp_path: Path):
        """.done.rejected 시 자동 재위임 메시지 관련 정보가 result에 포함"""
        vd = _import_verify_done()

        task_id = "task-vd-delegate"
        code_rel = "scripts/gate.py"
        _make_code_file(tmp_path, code_rel, "# placeholder\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="critical",
            signatures=[
                f"[grep] `def gate_function` @ `{code_rel}`",
            ],
        )
        _make_done_file(tmp_path, task_id)

        # pytest 호출 mock (critical 레벨)
        with patch("subprocess.run") as mock_run:
            # grep 실패 시뮬레이션
            mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")
            result = vd.verify_done(task_id, workspace_root=str(tmp_path))

        # FAIL + reject 상태
        assert result["status"] == "FAIL"
        assert result["action"] == "reject"

        # 재위임을 위한 실패 시그니처 정보가 포함되어야 함
        assert "failed_signatures" in result, (
            "재위임 메시지 전송을 위해 failed_signatures 필요"
        )


# ===========================================================================
# 레벨 판별 통합 시나리오
# ===========================================================================

class TestLevelDetection:
    """레벨 판별 관련 통합 테스트."""

    def test_security_level_treated_as_critical(self, tmp_path: Path):
        """security 레벨도 Lv.3+로 취급 → grep + pytest 모두 검증"""
        sc = _import_signature_check()

        task_id = "task-security-level"
        code_rel = "scripts/auth.py"
        _make_code_file(tmp_path, code_rel, "def authenticate(): pass\n")

        _make_task_file(
            tmp_path,
            task_id,
            level="security",
            signatures=[
                f"[grep] `def authenticate` @ `{code_rel}`",
                "[pytest] `tests/test_auth.py::test_authenticate`",
            ],
        )

        pytest_called = []

        def fake_run(cmd, *args, **kwargs):
            if "pytest" in str(cmd):
                pytest_called.append(cmd)
                return MagicMock(returncode=0, stdout="1 passed", stderr="")
            return MagicMock(returncode=0, stdout="match", stderr="")

        with patch("subprocess.run", side_effect=fake_run):
            sc.verify(task_id, workspace_root=str(tmp_path))

        assert len(pytest_called) > 0, (
            "security 레벨에서 pytest 호출이 있어야 함 (Lv.3+ 취급)"
        )

    def test_no_level_section_defaults_to_grep_only(self, tmp_path: Path):
        """레벨 섹션 없으면 기본적으로 grep만 검증"""
        sc = _import_signature_check()

        task_id = "task-no-level"
        code_rel = "scripts/default.py"
        _make_code_file(tmp_path, code_rel, "DEFAULT_VALUE = 42\n")

        # level 파라미터 없이 task 파일 생성 (## 레벨 섹션 없음)
        _make_task_file(
            tmp_path,
            task_id,
            signatures=[f"[grep] `DEFAULT_VALUE` @ `{code_rel}`"],
            # level="" → 레벨 섹션 미포함
        )

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="match", stderr="")
            result = sc.verify(task_id, workspace_root=str(tmp_path))

        pytest_calls = [
            c for c in mock_run.call_args_list
            if c.args and isinstance(c.args[0], list) and "pytest" in c.args[0]
        ]
        assert len(pytest_calls) == 0, (
            "레벨 미지정 시 pytest 호출 없어야 함"
        )
