"""
test_g3_verifier.py — g3_independent_verifier.py 유닛/통합 테스트
작성자: 닌기르수 (개발5팀 테스터)
태스크: task-1883
"""

import json
import os
import subprocess
import sys
from pathlib import Path

import pytest

# 모듈 경로 추가
sys.path.insert(0, "/home/jay/workspace/scripts")
import g3_independent_verifier as g3v  # type: ignore[import-not-found]

# ---------------------------------------------------------------------------
# 공통 픽스처
# ---------------------------------------------------------------------------

SCRIPT_PATH = "/home/jay/workspace/scripts/g3_independent_verifier.py"

REPORT_WITH_TABLE = """\
# task-1883 작업 보고서

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/sample.py | 핵심 기능 추가 | grep "MAGIC_KEYWORD" OK | verified |
"""

REPORT_WITHOUT_TABLE = """\
# task-1883 작업 보고서 (Lv.2)

**S**: 현재 문서 체계가 정비되지 않아 가이드라인이 누락되어 있다.

**C**: 이로 인해 신규 팀원의 온보딩 시간이 증가하고 있다.

## 개요
이 작업은 문서 수정만 포함합니다. 추가 내용을 채워 분량을 확보합니다.
작업 범위는 README 및 가이드 문서 갱신이며 코드 변경은 없습니다.
관련 이슈 없음. 단순 문서화 작업으로 완료됩니다.
"""


def make_report(tmp_path: Path, task_id: str, content: str) -> Path:
    """보고서 파일을 tmp_path/memory/reports/{task_id}.md 에 생성한다."""
    reports_dir = tmp_path / "memory" / "reports"
    reports_dir.mkdir(parents=True, exist_ok=True)
    report_file = reports_dir / f"{task_id}.md"
    report_file.write_text(content, encoding="utf-8")
    return report_file


def make_source_file(tmp_path: Path, rel_path: str, content: str) -> Path:
    """소스 파일을 tmp_path 하위 rel_path 위치에 생성한다."""
    target = tmp_path / rel_path
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(content, encoding="utf-8")
    return target


# ---------------------------------------------------------------------------
# 유닛 테스트: parse_verification_table
# ---------------------------------------------------------------------------


class TestParseVerificationTable:
    def test_returns_empty_when_no_table(self):
        """테이블 없는 보고서 → 빈 리스트"""
        result = g3v.parse_verification_table(REPORT_WITHOUT_TABLE)
        assert result == []

    def test_parses_verified_entry(self):
        """테이블이 있는 보고서 → 엔트리 파싱"""
        result = g3v.parse_verification_table(REPORT_WITH_TABLE)
        assert len(result) == 1
        entry = result[0]
        assert entry["file_path"] == "scripts/sample.py"
        assert entry["keyword"] == "MAGIC_KEYWORD"
        assert entry["status"] == "verified"

    def test_skips_header_row(self):
        """헤더 행이 엔트리로 포함되지 않아야 한다."""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/a.py | 변경 | grep "FOO" OK | verified |
"""
        result = g3v.parse_verification_table(report)
        # 헤더("파일", "변경 내용" 등)는 포함되지 않아야 함
        assert all(e["file_path"] != "파일" for e in result)
        assert len(result) == 1

    def test_no_keyword_when_grep_col_empty(self):
        """grep 검증 컬럼에 키워드 패턴이 없으면 keyword=None"""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/b.py | 변경 | 없음 | verified |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 1
        assert result[0]["keyword"] is None

    def test_multiple_entries(self):
        """여러 엔트리 파싱"""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/a.py | 변경A | grep "KEY_A" OK | verified |
| scripts/b.py | 변경B | grep "KEY_B" OK | verified |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 2
        assert result[0]["keyword"] == "KEY_A"
        assert result[1]["keyword"] == "KEY_B"


# ---------------------------------------------------------------------------
# 유닛 테스트: resolve_file_path
# ---------------------------------------------------------------------------


class TestResolveFilePath:
    def test_absolute_path_unchanged(self, monkeypatch):
        """절대 경로는 그대로 반환"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", "/tmp/ws")
        result = g3v.resolve_file_path("/absolute/path/file.py")
        assert str(result) == "/absolute/path/file.py"

    def test_relative_path_prefixed_with_workspace_root(self, monkeypatch, tmp_path):
        """상대 경로는 WORKSPACE_ROOT 기준으로 절대 경로 변환"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        result = g3v.resolve_file_path("scripts/sample.py")
        assert result == tmp_path / "scripts" / "sample.py"

    def test_strips_line_number_suffix(self, monkeypatch, tmp_path):
        """경로:라인번호 형식에서 라인번호 제거"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        result = g3v.resolve_file_path("scripts/sample.py:42")
        assert str(result).endswith("scripts/sample.py")
        assert ":42" not in str(result)


# ---------------------------------------------------------------------------
# 유닛 테스트: check_file_existence
# ---------------------------------------------------------------------------


class TestCheckFileExistence:
    def test_pass_when_all_files_exist(self, monkeypatch, tmp_path):
        """모든 파일이 존재하면 PASS"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        target = tmp_path / "scripts" / "sample.py"
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text("# exists")

        entries = [{"file_path": "scripts/sample.py"}]
        status, missing = g3v.check_file_existence(entries)
        assert status == "PASS"
        assert missing == []

    def test_fail_when_file_missing(self, monkeypatch, tmp_path):
        """파일이 없으면 FAIL + 파일명 반환"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        entries = [{"file_path": "scripts/nonexistent.py"}]
        status, missing = g3v.check_file_existence(entries)
        assert status == "FAIL"
        assert "scripts/nonexistent.py" in missing


# ---------------------------------------------------------------------------
# 유닛 테스트: run_grep_verification
# ---------------------------------------------------------------------------


class TestRunGrepVerification:
    def test_pass_when_keyword_found(self, monkeypatch, tmp_path):
        """키워드가 파일에 존재하면 PASS"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        src = tmp_path / "scripts" / "sample.py"
        src.parent.mkdir(parents=True, exist_ok=True)
        src.write_text("def MAGIC_KEYWORD(): pass\n")

        entries = [
            {
                "file_path": "scripts/sample.py",
                "keyword": "MAGIC_KEYWORD",
                "status": "verified",
            }
        ]
        status, failed = g3v.run_grep_verification(entries)
        assert status == "PASS"
        assert failed == []

    def test_fail_when_keyword_missing(self, monkeypatch, tmp_path):
        """키워드가 파일에 없으면 FAIL"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        src = tmp_path / "scripts" / "sample.py"
        src.parent.mkdir(parents=True, exist_ok=True)
        src.write_text("def other_function(): pass\n")

        entries = [
            {
                "file_path": "scripts/sample.py",
                "keyword": "MAGIC_KEYWORD",
                "status": "verified",
            }
        ]
        status, failed = g3v.run_grep_verification(entries)
        assert status == "FAIL"
        assert len(failed) == 1
        assert "MAGIC_KEYWORD" in failed[0]

    def test_skips_non_verified_entries(self, monkeypatch, tmp_path):
        """status가 verified가 아닌 항목은 grep 검사 스킵"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        # 파일 자체가 없어도 verified가 아니면 오류 없어야 함
        entries = [
            {
                "file_path": "scripts/sample.py",
                "keyword": "MAGIC_KEYWORD",
                "status": "pending",
            }
        ]
        status, failed = g3v.run_grep_verification(entries)
        assert status == "PASS"
        assert failed == []

    def test_skips_entry_without_keyword(self, monkeypatch, tmp_path):
        """keyword가 None이면 grep 스킵"""
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        src = tmp_path / "scripts" / "sample.py"
        src.parent.mkdir(parents=True, exist_ok=True)
        src.write_text("def func(): pass\n")

        entries = [
            {
                "file_path": "scripts/sample.py",
                "keyword": None,
                "status": "verified",
            }
        ]
        status, failed = g3v.run_grep_verification(entries)
        assert status == "PASS"
        assert failed == []


# ---------------------------------------------------------------------------
# 통합 테스트
# ---------------------------------------------------------------------------


class TestG3VerifierIntegration:
    """
    subprocess로 g3_independent_verifier.py를 직접 실행하거나,
    monkeypatch로 WORKSPACE_ROOT를 교체하여 main()을 호출하는 통합 테스트.
    """

    def _run_main_with_args(self, monkeypatch, tmp_path, task_id):
        """
        main()을 직접 호출하고 exit code를 캡처한다.
        sys.argv와 WORKSPACE_ROOT를 monkeypatch로 교체한다.
        """
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        monkeypatch.setattr(sys, "argv", ["g3_independent_verifier.py", "--task-id", task_id])
        with pytest.raises(SystemExit) as exc_info:
            g3v.main()
        return exc_info.value.code

    # ------------------------------------------------------------------
    # 시나리오 1: 정상 보고서 + 정상 코드 → PASS (exit code 0)
    # ------------------------------------------------------------------
    def test_pass_with_valid_report(self, tmp_path, monkeypatch):
        """정상 보고서 + 키워드가 존재하는 파일 → exit code 0"""
        task_id = "task-9001"

        # 보고서 생성
        report_content = """\
# task-9001 작업 보고서

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/sample.py | 핵심 기능 추가 | grep "MAGIC_KEYWORD" OK | verified |
"""
        make_report(tmp_path, task_id, report_content)

        # 대상 파일 생성 (키워드 포함)
        make_source_file(tmp_path, "scripts/sample.py", "def MAGIC_KEYWORD(): pass\n")

        # pytest 탐색 방해 방지: tests 디렉토리 없애기 (task-9001 관련 파일 없음)
        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 0

    # ------------------------------------------------------------------
    # 시나리오 2: 보고서에 있지만 코드에 없는 심볼 → FAIL (exit code 1)
    # ------------------------------------------------------------------
    def test_fail_missing_keyword(self, tmp_path, monkeypatch):
        """보고서에 verified 키워드가 있으나 실제 파일에 없음 → exit code 1 + g3-fail 생성"""
        task_id = "task-9002"

        report_content = """\
# task-9002 작업 보고서

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/sample.py | 핵심 기능 추가 | grep "MISSING_SYMBOL" OK | verified |
"""
        make_report(tmp_path, task_id, report_content)

        # 파일은 존재하지만 키워드 없음
        make_source_file(tmp_path, "scripts/sample.py", "def other_function(): pass\n")

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 1

        # g3-fail 파일 생성 확인
        fail_file = tmp_path / "memory" / "events" / f"{task_id}.g3-fail"
        assert fail_file.exists(), "g3-fail 파일이 생성되어야 한다"

        fail_data = json.loads(fail_file.read_text(encoding="utf-8"))
        assert fail_data["task_id"] == task_id
        assert len(fail_data["fail_reasons"]) > 0
        assert any("MISSING_SYMBOL" in r for r in fail_data["fail_reasons"])

    # ------------------------------------------------------------------
    # 시나리오 3: 보고서 파일 자체가 없을 때 → FAIL (exit code 1)
    # ------------------------------------------------------------------
    def test_fail_no_report(self, tmp_path, monkeypatch):
        """보고서 파일이 없으면 exit code 1 + 에러 메시지에 'report not found' 포함"""
        task_id = "task-9003"
        # 보고서 디렉토리만 있고 파일은 없음
        (tmp_path / "memory" / "reports").mkdir(parents=True, exist_ok=True)

        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        monkeypatch.setattr(sys, "argv", ["g3_independent_verifier.py", "--task-id", task_id])

        import builtins

        output_lines = []
        original_print = builtins.print

        def capturing_print(*args, **kwargs):
            line = " ".join(str(a) for a in args)
            output_lines.append(line)
            original_print(*args, **kwargs)

        monkeypatch.setattr(builtins, "print", capturing_print)

        with pytest.raises(SystemExit) as exc_info:
            g3v.main()

        assert exc_info.value.code == 1

        # 출력된 JSON에서 "report not found" 확인
        all_output = "\n".join(output_lines)
        assert "report not found" in all_output

    # ------------------------------------------------------------------
    # 시나리오 4: Lv.2 작업 → SKIP (테이블 없는 보고서, exit code 0)
    # ------------------------------------------------------------------
    def test_skip_lv2_no_table(self, tmp_path, monkeypatch):
        """테이블 없는 보고서 → PASS with SKIP (exit code 0)"""
        task_id = "task-9004"

        make_report(tmp_path, task_id, REPORT_WITHOUT_TABLE)

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 0

    # ------------------------------------------------------------------
    # 통합 테스트: subprocess로 직접 실행
    # ------------------------------------------------------------------
    def test_subprocess_no_report(self, tmp_path):
        """subprocess로 스크립트를 실행 — 보고서 없음 → returncode 1"""
        env = os.environ.copy()
        env["WORKSPACE_ROOT"] = str(tmp_path)
        (tmp_path / "memory" / "reports").mkdir(parents=True, exist_ok=True)

        result = subprocess.run(
            ["python3", SCRIPT_PATH, "--task-id", "task-subprocess-test"],
            capture_output=True,
            text=True,
            env=env,
            timeout=30,
        )
        assert result.returncode == 1
        output = result.stdout + result.stderr
        assert "report not found" in output


# ---------------------------------------------------------------------------
# 유닛 테스트: check_planned_items (task-1917)
# ---------------------------------------------------------------------------


class TestCheckPlannedItems:
    def test_fail_when_planned_items_exist(self):
        """planned 상태 항목이 있으면 FAIL"""
        entries = [
            {"file_path": "scripts/a.py", "description": "변경A", "keyword": None, "status": "planned"},
            {"file_path": "scripts/b.py", "description": "변경B", "keyword": "KEY_B", "status": "verified"},
        ]
        status, details = g3v.check_planned_items(entries)
        assert status == "FAIL"
        assert len(details) == 1
        assert "1건" in details[0]
        assert "scripts/a.py" in details[0]

    def test_pass_when_no_planned_items(self):
        """planned 항목이 없으면 PASS"""
        entries = [
            {"file_path": "scripts/a.py", "description": "변경A", "keyword": "KEY_A", "status": "verified"},
        ]
        status, details = g3v.check_planned_items(entries)
        assert status == "PASS"
        assert details == []

    def test_fail_with_multiple_planned(self):
        """planned 항목 여러 개 → 모두 FAIL 메시지에 포함"""
        entries = [
            {"file_path": "scripts/a.py", "description": "변경A", "status": "planned"},
            {"file_path": "scripts/b.py", "description": "변경B", "status": "planned"},
        ]
        status, details = g3v.check_planned_items(entries)
        assert status == "FAIL"
        assert "2건" in details[0]

    def test_pass_when_empty_entries(self):
        """빈 entries → PASS"""
        status, details = g3v.check_planned_items([])
        assert status == "PASS"
        assert details == []


# ---------------------------------------------------------------------------
# 통합 테스트: planned 항목이 있는 보고서 → g3 FAIL (task-1917)
# ---------------------------------------------------------------------------


class TestG3VerifierPlannedIntegration:
    def _run_main_with_args(self, monkeypatch, tmp_path, task_id):
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        monkeypatch.setattr(sys, "argv", ["g3_independent_verifier.py", "--task-id", task_id])
        with pytest.raises(SystemExit) as exc_info:
            g3v.main()
        return exc_info.value.code

    def test_fail_with_planned_in_report(self, tmp_path, monkeypatch):
        """보고서에 planned 항목 포함 → exit code 1"""
        task_id = "task-9010"
        report_content = """\
# task-9010 작업 보고서

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/done.py | 완료된 변경 | grep "DONE_KEY" OK | verified |
| scripts/todo.py | 미완료 변경 | 없음 | planned |
"""
        make_report(tmp_path, task_id, report_content)
        make_source_file(tmp_path, "scripts/done.py", "DONE_KEY = True\n")
        make_source_file(tmp_path, "scripts/todo.py", "# not done yet\n")

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 1

        # g3-fail 파일에 planned 관련 사유 포함
        fail_file = tmp_path / "memory" / "events" / f"{task_id}.g3-fail"
        assert fail_file.exists()
        fail_data = json.loads(fail_file.read_text(encoding="utf-8"))
        assert any("planned" in r.lower() for r in fail_data["fail_reasons"])

    def test_pass_with_all_verified(self, tmp_path, monkeypatch):
        """모든 항목이 verified → exit code 0"""
        task_id = "task-9011"
        report_content = """\
# task-9011 작업 보고서

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/a.py | 변경 A | grep "KEY_A" OK | verified |
| scripts/b.py | 변경 B | grep "KEY_B" OK | verified |
"""
        make_report(tmp_path, task_id, report_content)
        make_source_file(tmp_path, "scripts/a.py", "KEY_A = True\n")
        make_source_file(tmp_path, "scripts/b.py", "KEY_B = True\n")

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 0


# ---------------------------------------------------------------------------
# 유닛 테스트: check_report_quality (V-5)
# ---------------------------------------------------------------------------


class TestCheckReportQuality:
    def test_fail_minimal_report(self):
        """1줄짜리 보고서 → FAIL (SCQA 부족 + 분량 부족)"""
        report = "# task-xxx\n끝."
        status, issues = g3v.check_report_quality(report)
        assert status == "FAIL"
        assert len(issues) >= 1  # SCQA 부족 또는 분량 부족

    def test_fail_short_no_scqa(self):
        """200자 미만 + SCQA 없음 → FAIL"""
        report = "짧은 보고서입니다.\n내용이 부족합니다."
        status, issues = g3v.check_report_quality(report)
        assert status == "FAIL"
        assert any("SCQA" in i or "분량" in i for i in issues)

    def test_pass_scqa_with_length(self):
        """SCQA 2개 이상 + 200자 이상 → PASS"""
        report = "# 보고서\n\n**S**: 현재 상황입니다. 시스템이 정상 동작 중입니다.\n\n**C**: 문제가 발생했습니다. 특정 기능이 작동하지 않습니다.\n\n" + "추가내용 " * 30
        status, issues = g3v.check_report_quality(report)
        assert status == "PASS"
        assert issues == []

    def test_fail_scqa_only_one(self):
        """SCQA 1개만 존재 + 200자 이상 → FAIL (SCQA 부족)"""
        report = "# 보고서\n\n**S**: 현재 상황입니다.\n\n" + "내용을 채우기 위한 텍스트입니다. " * 20
        status, issues = g3v.check_report_quality(report)
        assert status == "FAIL"
        assert any("SCQA" in i for i in issues)

    def test_pass_with_header_style_scqa(self):
        """'## S -', '## C -' 형식의 SCQA → PASS"""
        report = "# 보고서\n\n## S - 상황\n현재 상황 설명입니다.\n\n## C - 문제\n문제 설명입니다.\n\n" + "내용 " * 50
        status, issues = g3v.check_report_quality(report)
        assert status == "PASS"
        assert issues == []

    def test_fail_length_under_200(self):
        """SCQA 있지만 200자 미만 → FAIL"""
        report = "**S**: 상황.\n**C**: 문제."
        status, issues = g3v.check_report_quality(report)
        assert status == "FAIL"
        assert any("분량" in i for i in issues)


# ---------------------------------------------------------------------------
# 유닛 테스트: parse_verification_table V-9 개선사항
# ---------------------------------------------------------------------------


class TestParseVerificationTableV9:
    def test_only_parses_verification_section(self):
        """'수정 파일별 검증 상태' 섹션의 테이블만 파싱, 다른 섹션 테이블 무시"""
        report = """\
## 시나리오 목록

| 시나리오 | 설명 | 결과 | 비고 |
|----------|------|------|------|
| 로그인 테스트 | 로그인 동작 확인 | 성공 | 없음 |

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/auth.py | 인증 로직 수정 | grep "validate_token" OK | verified |

## 결론

| 항목 | 상태 |
|------|------|
| 전체 | 완료 |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 1
        assert result[0]["file_path"] == "scripts/auth.py"

    def test_ignores_non_file_rows(self):
        """파일 경로 패턴이 아닌 행은 무시"""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/real.py | 실제 파일 | grep "KEY" OK | verified |
| 로그인 테스트 | 시나리오 설명 | 성공 | verified |
| 사용자 조회 | API 설명 | 통과 | verified |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 1
        assert result[0]["file_path"] == "scripts/real.py"

    def test_accepts_absolute_paths(self):
        """절대 경로(/로 시작)도 파일로 인식"""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| /home/jay/workspace/scripts/test.py | 수정 | grep "KEY" OK | verified |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 1

    def test_accepts_various_extensions(self):
        """다양한 확장자 파일 인식"""
        report = """\
## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| src/app.tsx | 컴포넌트 수정 | grep "Component" OK | verified |
| config/settings.yaml | 설정 변경 | grep "timeout" OK | verified |
"""
        result = g3v.parse_verification_table(report)
        assert len(result) == 2


# ---------------------------------------------------------------------------
# 통합 테스트: V-5 (빈 보고서로 g3 통과 방지)
# ---------------------------------------------------------------------------


class TestG3VerifierV5Integration:
    """V-5 통합 테스트: 빈 보고서로 g3 통과 방지"""

    def _run_main_with_args(self, monkeypatch, tmp_path, task_id):
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        monkeypatch.setattr(sys, "argv", ["g3_independent_verifier.py", "--task-id", task_id])
        with pytest.raises(SystemExit) as exc_info:
            g3v.main()
        return exc_info.value.code

    def test_fail_one_line_report(self, tmp_path, monkeypatch):
        """검증 시나리오 1: 1줄짜리 보고서 → g3 FAIL"""
        task_id = "task-9020"
        report_content = "# task-9020\n끝."
        make_report(tmp_path, task_id, report_content)

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 1

        # g3-fail 파일 생성 확인
        fail_file = tmp_path / "memory" / "events" / f"{task_id}.g3-fail"
        assert fail_file.exists()

    def test_pass_scqa_no_table(self, tmp_path, monkeypatch):
        """검증 시나리오 2: SCQA 포함 + 테이블 없는 조사 보고서 → g3 PASS"""
        task_id = "task-9021"
        report_content = (
            "# task-9021 조사 보고서\n\n"
            "**S**: 현재 시스템은 정상 운영 중이며, 데이터 파이프라인이 안정적으로 동작하고 있다.\n\n"
            "**C**: 그러나 최근 데이터 지연이 간헐적으로 발생하여 사용자 불만이 증가하고 있다.\n\n"
            "**Q**: 데이터 지연의 근본 원인은 무엇이며 해결 가능한가?\n\n"
            "**A**: 조사 결과 캐시 만료 정책이 원인으로 확인되었고, TTL 조정으로 해결 가능하다.\n\n"
            + "상세 내용입니다. " * 10
        )
        make_report(tmp_path, task_id, report_content)

        exit_code = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 0


# ---------------------------------------------------------------------------
# 통합 테스트: V-9 (여러 테이블이 있는 보고서 → 검증 테이블만 파싱)
# ---------------------------------------------------------------------------


class TestG3VerifierV9Integration:
    """V-9 통합 테스트: 여러 테이블이 있는 보고서 → 검증 테이블만 파싱"""

    def _run_main_with_args(self, monkeypatch, tmp_path, task_id):
        monkeypatch.setattr(g3v, "WORKSPACE_ROOT", str(tmp_path))
        monkeypatch.setattr(sys, "argv", ["g3_independent_verifier.py", "--task-id", task_id])
        import builtins
        output_lines = []
        original_print = builtins.print
        def capturing_print(*args, **kwargs):
            line = " ".join(str(a) for a in args)
            output_lines.append(line)
            original_print(*args, **kwargs)
        monkeypatch.setattr(builtins, "print", capturing_print)
        with pytest.raises(SystemExit) as exc_info:
            g3v.main()
        return exc_info.value.code, "\n".join(output_lines)

    def test_multi_table_report(self, tmp_path, monkeypatch):
        """검증 시나리오 3: 여러 테이블 → 검증 테이블만 파싱, 정보 테이블 무시"""
        task_id = "task-9030"
        report_content = """\
# task-9030 작업 보고서

**S**: 현재 인증 모듈에 보안 취약점이 보고되었다.

**C**: 토큰 만료 검증 로직이 누락되어 만료된 토큰으로 접근이 가능하다.

**Q**: 토큰 검증 로직을 추가하여 보안 취약점을 해결할 수 있는가?

**A**: validate_token 함수에 만료 검증 추가로 해결 완료. pytest 5 passed, 0 failed.

## 시나리오 목록

| 시나리오 | 설명 | 결과 | 비고 |
|----------|------|------|------|
| 만료 토큰 | 만료된 토큰 거부 | 성공 | 핵심 시나리오 |
| 유효 토큰 | 정상 토큰 허용 | 성공 | 기본 동작 |

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| scripts/auth.py | 토큰 검증 추가 | grep "validate_token" OK | verified |

## 결론

전체 작업 완료.
"""
        make_report(tmp_path, task_id, report_content)
        make_source_file(tmp_path, "scripts/auth.py", "def validate_token(): pass\n")

        exit_code, output = self._run_main_with_args(monkeypatch, tmp_path, task_id)
        assert exit_code == 0

        # JSON 결과에서 entries_found가 1이어야 (시나리오 테이블은 무시)
        result = json.loads(output)
        assert result["checks"]["report_parse"]["entries_found"] == 1
