"""
TDD 단위 테스트: meeting-audit.py
작성자: 하누만 (개발4팀 테스터)
"""

import importlib.util
import json
import os

import pytest

# ---------------------------------------------------------------------------
# 모듈 로딩 (하이픈 파일명 처리)
# ---------------------------------------------------------------------------
_SCRIPTS_DIR = "/home/jay/workspace/scripts"
_MODULE_PATH = os.path.join(_SCRIPTS_DIR, "meeting-audit.py")


def _load_meeting_audit():
    spec = importlib.util.spec_from_file_location("meeting_audit", _MODULE_PATH)
    assert spec is not None, f"Cannot load spec from {_MODULE_PATH}"
    mod = importlib.util.module_from_spec(spec)
    assert spec.loader is not None, "spec.loader is None"
    spec.loader.exec_module(mod)
    return mod


# 모듈이 존재하지 않을 경우 모든 테스트가 ImportError 대신 명확한 메시지로 실패하도록
try:
    meeting_audit = _load_meeting_audit()
    parse_meeting_file = meeting_audit.parse_meeting_file
    check_cycle_count = meeting_audit.check_cycle_count
    check_persona_validation = meeting_audit.check_persona_validation
    check_da = meeting_audit.check_da
    check_three_docs = meeting_audit.check_three_docs
    run_audit = meeting_audit.run_audit
    _MODULE_AVAILABLE = True
except Exception:
    _MODULE_AVAILABLE = False

pytestmark = pytest.mark.skipif(not _MODULE_AVAILABLE, reason="meeting-audit.py 모듈이 아직 구현되지 않았습니다.")

# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------
REAL_MEETING_FILE = "/home/jay/workspace/memory/meetings/2026-04-11-insuwiki-review-trust-ux.md"
DEFAULT_PERSONA_LIST = "/home/jay/workspace/skills/agent-meeting/references/persona-list.md"

# ---------------------------------------------------------------------------
# Fixture: 최소 유효 미팅 파일 내용
# ---------------------------------------------------------------------------
MINIMAL_MEETING_CONTENT = """\
# Agent 미팅: 테스트용 미팅

**날짜**: 2026-04-11
**소집 이유**: 테스트
**참여 페르소나**: 토르 (Thor), 로키 (Loki)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 3

## Cycle 1

내용 1

## Cycle 2

내용 2

## Cycle 3

내용 3

## 3문서 반영

계획서와 맥락노트, 체크리스트 반영 완료.
"""

DA_MEETING_CONTENT = """\
# Agent 미팅: DA 포함 테스트

**날짜**: 2026-04-11
**소집 이유**: DA 테스트
**참여 페르소나**: 토르 (Thor), 로키 (Loki)
**미팅 모드**: hybrid
**토론 깊이**: thorough
**총 사이클 수**: 3

## Cycle 1

내용 1

## Cycle 2

내용 2

## Cycle 3 — Devil's Advocate

### Devil's Advocate

**지정**: 로키

1. **실패 시나리오**: 시스템 실패 가능성 분석
2. **후회 이유**: 현재 접근법의 후회 요소
3. **더 단순한 대안**: 단순한 대안 제안

**반박**: 위 우려에 대한 팀의 반박 내용.
**판정**: 반박 수용. DA 우려 대응책 설계에 포함.
"""

NO_DA_MEETING_CONTENT = """\
# Agent 미팅: DA 없는 테스트

**날짜**: 2026-04-11
**소집 이유**: DA 없음 테스트
**참여 페르소나**: 토르 (Thor), 로키 (Loki)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 2

## Cycle 1

내용 1

## Cycle 2

내용 2
"""

UNREGISTERED_PERSONA_CONTENT = """\
# Agent 미팅: 미등록 페르소나 테스트

**날짜**: 2026-04-11
**소집 이유**: 미등록 페르소나 테스트
**참여 페르소나**: 엔키(백엔드), 이쉬타르(프론트엔드), 로키 (Loki)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 1
"""

NO_LOKI_CONTENT = """\
# Agent 미팅: 로키 없는 테스트

**날짜**: 2026-04-11
**소집 이유**: 로키 없음 테스트
**참여 페르소나**: 토르 (Thor), 미미르 (Mimir)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 1
"""

THREE_DOCS_CONTENT = """\
# Agent 미팅: 3문서 반영 테스트

**날짜**: 2026-04-11
**소집 이유**: 3문서 테스트
**참여 페르소나**: 토르 (Thor), 로키 (Loki)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 1

## 3문서 반영

계획서, 맥락노트, 체크리스트 모두 반영 완료.
"""

NO_THREE_DOCS_CONTENT = """\
# Agent 미팅: 3문서 미언급 테스트

**날짜**: 2026-04-11
**소집 이유**: 3문서 미언급 테스트
**참여 페르소나**: 토르 (Thor), 로키 (Loki)
**미팅 모드**: sync
**토론 깊이**: normal
**총 사이클 수**: 1

## 합의 사항

아무 문서 언급 없음.
"""

PERSONA_LIST_CONTENT = """\
# 페르소나 목록

| 페르소나 | 팀 | 전문성 관점 |
|---------|-----|-----------|
| 토르 (Thor) | 개발2팀 / 백엔드 | 고성능 서버 |
| 불칸 (Vulcan) | 개발1팀 / 백엔드 | API 설계 |
| 프레이야 (Freya) | 개발2팀 / 프론트 | 인터랙티브 UI |
| 이리스 (Iris) | 개발1팀 / 프론트 | 컴포넌트 설계 |
| 미미르 (Mimir) | 개발2팀 / UX | 데이터 기반 UX |
| 아테나 (Athena) | 개발1팀 / UX | 유저 플로우 |
| 헤임달 (Heimdall) | 개발2팀 / 테스트 | 보안 중심 QA |
| 아르고스 (Argos) | 개발1팀 / 테스트 | 엣지 케이스 |
| 로키 (Loki) | 레드팀 | 보안 취약점 |
| 야누스 (Janus) | DevOps 센터 | 인프라 |
| 마아트 (Ma'at) | QC 센터 | 독립 품질 검증 |
| 비너스 (Venus) | 디자인 센터 | 디자인 방향성 |
| 오딘 (Odin) | 개발2팀장 | 아키텍처 |
| 헤르메스 (Hermes) | 개발1팀장 | 풀스택 |
"""


# ---------------------------------------------------------------------------
# Fixture helpers
# ---------------------------------------------------------------------------


@pytest.fixture
def tmp_persona_list(tmp_path):
    p = tmp_path / "persona-list.md"
    p.write_text(PERSONA_LIST_CONTENT, encoding="utf-8")
    return str(p)


@pytest.fixture
def minimal_meeting(tmp_path):
    f = tmp_path / "minimal-meeting.md"
    f.write_text(MINIMAL_MEETING_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def da_meeting(tmp_path):
    f = tmp_path / "da-meeting.md"
    f.write_text(DA_MEETING_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def no_da_meeting(tmp_path):
    f = tmp_path / "no-da-meeting.md"
    f.write_text(NO_DA_MEETING_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def unregistered_persona_meeting(tmp_path):
    f = tmp_path / "unreg-persona-meeting.md"
    f.write_text(UNREGISTERED_PERSONA_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def no_loki_meeting(tmp_path):
    f = tmp_path / "no-loki-meeting.md"
    f.write_text(NO_LOKI_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def three_docs_meeting(tmp_path):
    f = tmp_path / "three-docs-meeting.md"
    f.write_text(THREE_DOCS_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def no_three_docs_meeting(tmp_path):
    f = tmp_path / "no-three-docs-meeting.md"
    f.write_text(NO_THREE_DOCS_CONTENT, encoding="utf-8")
    return str(f)


@pytest.fixture
def empty_meeting(tmp_path):
    f = tmp_path / "empty-meeting.md"
    f.write_text("", encoding="utf-8")
    return str(f)


# ---------------------------------------------------------------------------
# 1. test_parse_meeting_file
# ---------------------------------------------------------------------------


class TestParseMeetingFile:
    def test_parse_real_meeting_file(self):
        """실제 미팅 파일을 파싱하여 메타데이터 추출 확인"""
        result = parse_meeting_file(REAL_MEETING_FILE)

        assert isinstance(result, dict)
        assert result["total_cycles"] == 2
        assert result["mode"] == "hybrid"
        assert result["depth"] == "thorough"
        assert "content" in result
        assert isinstance(result["personas"], list)
        assert len(result["personas"]) >= 1

    def test_parse_returns_required_keys(self, minimal_meeting):
        """파싱 결과에 필수 키가 모두 포함되어야 함"""
        result = parse_meeting_file(minimal_meeting)
        for key in ("total_cycles", "personas", "mode", "depth", "content"):
            assert key in result, f"필수 키 누락: {key}"

    def test_parse_cycle_count_integer(self, minimal_meeting):
        """총 사이클 수가 정수로 파싱되어야 함"""
        result = parse_meeting_file(minimal_meeting)
        assert isinstance(result["total_cycles"], int)
        assert result["total_cycles"] == 3

    def test_parse_personas_list(self, minimal_meeting):
        """personas가 리스트 타입이어야 함"""
        result = parse_meeting_file(minimal_meeting)
        assert isinstance(result["personas"], list)

    def test_parse_content_not_empty(self, minimal_meeting):
        """content 필드가 비어 있지 않아야 함"""
        result = parse_meeting_file(minimal_meeting)
        assert result["content"]


# ---------------------------------------------------------------------------
# 2-4. test_check_cycle_count
# ---------------------------------------------------------------------------


class TestCheckCycleCount:
    def _make_parsed(self, cycles: int) -> dict:
        return {
            "total_cycles": cycles,
            "personas": [],
            "mode": "sync",
            "depth": "normal",
            "content": "",
        }

    def test_check_cycle_count_pass(self):
        """Lv.2 + 2사이클 → PASS"""
        parsed = self._make_parsed(2)
        result = check_cycle_count(parsed, level=2)

        assert result["status"] == "PASS"
        assert result["actual"] == 2
        assert result["expected_min"] == 1

    def test_check_cycle_count_fail(self):
        """Lv.4 + 2사이클 → FAIL (expected_min=3)"""
        parsed = self._make_parsed(2)
        result = check_cycle_count(parsed, level=4)

        assert result["status"] == "FAIL"
        assert result["actual"] == 2
        assert result["expected_min"] == 3
        assert "message" in result

    def test_check_cycle_count_lv1_skip(self):
        """Lv.1 → 사이클 수 체크 안 함 → PASS (스킵)"""
        parsed = self._make_parsed(0)
        result = check_cycle_count(parsed, level=1)

        assert result["status"] == "PASS"

    def test_check_cycle_count_lv3_exact_boundary(self):
        """Lv.3 + 2사이클 (경계값) → PASS (expected_min=2, actual=2)"""
        parsed = self._make_parsed(2)
        result = check_cycle_count(parsed, level=3)

        assert result["status"] == "PASS"
        assert result["expected_min"] == 2
        assert result["actual"] == 2

    def test_check_cycle_count_lv3_insufficient(self):
        """Lv.3 + 1사이클 → FAIL (expected_min=2, actual=1)"""
        parsed = self._make_parsed(1)
        result = check_cycle_count(parsed, level=3)

        assert result["status"] == "FAIL"
        assert result["expected_min"] == 2
        assert result["actual"] == 1

    def test_check_cycle_count_result_has_message(self):
        """결과에 message 키가 항상 포함되어야 함"""
        parsed = self._make_parsed(1)
        result = check_cycle_count(parsed, level=2)
        assert "message" in result


# ---------------------------------------------------------------------------
# 5-8. test_check_persona_validation
# ---------------------------------------------------------------------------


class TestCheckPersonaValidation:
    def _make_parsed(self, personas: list) -> dict:
        return {
            "total_cycles": 1,
            "personas": personas,
            "mode": "sync",
            "depth": "normal",
            "content": "",
        }

    def test_check_persona_registered(self, tmp_persona_list):
        """등록된 페르소나만 → PASS, unregistered 비어있음"""
        parsed = self._make_parsed(["토르 (Thor)", "로키 (Loki)"])
        result = check_persona_validation(parsed, tmp_persona_list)

        assert result["status"] == "PASS"
        assert result["unregistered"] == []
        assert isinstance(result["registered"], list)
        assert len(result["registered"]) >= 1

    def test_check_persona_unregistered(self, tmp_persona_list):
        """미등록 페르소나 존재 → WARN"""
        parsed = self._make_parsed(["엔키(백엔드)", "로키 (Loki)"])
        result = check_persona_validation(parsed, tmp_persona_list)

        assert result["status"] == "WARN"
        assert len(result["unregistered"]) >= 1
        assert "엔키" in " ".join(result["unregistered"]) or any("엔키" in u for u in result["unregistered"])

    def test_check_persona_loki_missing(self, tmp_persona_list):
        """로키 미참석 → FAIL"""
        parsed = self._make_parsed(["토르 (Thor)", "미미르 (Mimir)"])
        result = check_persona_validation(parsed, tmp_persona_list)

        assert result["status"] == "FAIL"
        assert result["loki_present"] is False

    def test_check_persona_loki_present(self, tmp_persona_list):
        """로키 참석 → loki_present=True"""
        parsed = self._make_parsed(["로키 (Loki)", "토르 (Thor)"])
        result = check_persona_validation(parsed, tmp_persona_list)

        assert result["loki_present"] is True

    def test_check_persona_result_has_required_keys(self, tmp_persona_list):
        """결과에 필수 키 포함 확인"""
        parsed = self._make_parsed(["토르 (Thor)", "로키 (Loki)"])
        result = check_persona_validation(parsed, tmp_persona_list)

        for key in ("status", "registered", "unregistered", "loki_present"):
            assert key in result, f"필수 키 누락: {key}"

    def test_check_persona_real_meeting_file(self):
        """실제 미팅 파일: 로키 미참석 + 미등록 페르소나 → FAIL"""
        parsed = parse_meeting_file(REAL_MEETING_FILE)
        result = check_persona_validation(parsed, DEFAULT_PERSONA_LIST)

        assert result["status"] == "FAIL"
        assert result["loki_present"] is False
        assert len(result["unregistered"]) >= 1


# ---------------------------------------------------------------------------
# 9-12. test_check_da
# ---------------------------------------------------------------------------


class TestCheckDA:
    def test_check_da_required_present(self, da_meeting):
        """Lv.3 + DA 존재 → PASS"""
        parsed = parse_meeting_file(da_meeting)
        result = check_da(parsed, level=3)

        assert result["status"] == "PASS"
        assert result["da_sections_found"] >= 1

    def test_check_da_required_missing(self, no_da_meeting):
        """Lv.3 + DA 미존재 → FAIL"""
        parsed = parse_meeting_file(no_da_meeting)
        result = check_da(parsed, level=3)

        assert result["status"] == "FAIL"
        assert result["da_sections_found"] == 0

    def test_check_da_optional_lv2(self, no_da_meeting):
        """Lv.2 + DA 미존재 → PASS (선택적)"""
        parsed = parse_meeting_file(no_da_meeting)
        result = check_da(parsed, level=2)

        assert result["status"] == "PASS"

    def test_check_da_optional_lv1(self, no_da_meeting):
        """Lv.1 + DA 미존재 → PASS (선택적)"""
        parsed = parse_meeting_file(no_da_meeting)
        result = check_da(parsed, level=1)

        assert result["status"] == "PASS"

    def test_check_da_three_questions(self, da_meeting):
        """DA 3대 질문 (실패/후회/단순한 대안) 존재 확인"""
        parsed = parse_meeting_file(da_meeting)
        result = check_da(parsed, level=3)

        assert result["three_questions"] is True

    def test_check_da_rebuttal_found(self, da_meeting):
        """반박 존재 확인"""
        parsed = parse_meeting_file(da_meeting)
        result = check_da(parsed, level=3)

        assert result["rebuttal_found"] is True

    def test_check_da_verdict_found(self, da_meeting):
        """판정 존재 확인"""
        parsed = parse_meeting_file(da_meeting)
        result = check_da(parsed, level=3)

        assert result["verdict_found"] is True

    def test_check_da_result_has_required_keys(self, da_meeting):
        """결과에 필수 키 포함 확인"""
        parsed = parse_meeting_file(da_meeting)
        result = check_da(parsed, level=3)

        for key in ("status", "da_sections_found", "three_questions", "rebuttal_found", "verdict_found"):
            assert key in result, f"필수 키 누락: {key}"

    def test_check_da_real_meeting_file(self):
        """실제 미팅 파일: DA 섹션 존재 + 3대 질문 + 반박 + 판정 → PASS"""
        parsed = parse_meeting_file(REAL_MEETING_FILE)
        result = check_da(parsed, level=4)

        assert result["status"] == "PASS"
        assert result["da_sections_found"] >= 1
        assert result["three_questions"] is True
        assert result["rebuttal_found"] is True
        assert result["verdict_found"] is True


# ---------------------------------------------------------------------------
# 13-15. test_check_three_docs
# ---------------------------------------------------------------------------


class TestCheckThreeDocs:
    def test_check_three_docs_mentioned(self, three_docs_meeting):
        """'3문서 반영' 또는 관련 키워드 언급 → PASS"""
        parsed = parse_meeting_file(three_docs_meeting)
        result = check_three_docs(parsed)

        assert result["status"] == "PASS"
        assert result["mentioned"] is True

    def test_check_three_docs_not_mentioned(self, no_three_docs_meeting):
        """3문서 미언급 → WARN"""
        parsed = parse_meeting_file(no_three_docs_meeting)
        result = check_three_docs(parsed)

        assert result["status"] == "WARN"
        assert result["mentioned"] is False

    def test_check_three_docs_with_task_id_no_dir(self, three_docs_meeting):
        """task-id 제공 + 경로 없음 → files_exist=False"""
        parsed = parse_meeting_file(three_docs_meeting)
        fake_task_id = "NONEXISTENT-TASK-99999"
        result = check_three_docs(parsed, task_id=fake_task_id)

        # task-id가 제공됐지만 디렉터리가 없으면 files_exist=False
        assert result["files_exist"] is False

    def test_check_three_docs_with_task_id_existing_dir(self, three_docs_meeting, tmp_path):
        """task-id 제공 + 경로 존재 → files_exist=True 또는 확인 가능"""
        # tmp_path에 3docs 경로 생성
        task_id = "TEST-TASK-001"
        docs_dir = tmp_path / "3docs" / task_id
        docs_dir.mkdir(parents=True)
        (docs_dir / "plan.md").write_text("계획서")
        (docs_dir / "context.md").write_text("맥락노트")
        (docs_dir / "checklist.md").write_text("체크리스트")

        parsed = parse_meeting_file(three_docs_meeting)

        # monkeypatch 없이 실제 경로를 파라미터로 전달하는 방식은
        # check_three_docs의 구현이 BASE_PATH를 파라미터로 받거나,
        # 또는 task_id 경로를 직접 주입 가능한 인터페이스가 필요.
        # 여기서는 task_id 대신 절대 경로를 task_id로 넘기는 방식을 허용하는지 확인.
        # 구현이 `/home/jay/workspace/memory/3docs/{task-id}/` 경로를 사용하므로,
        # 실제 경로 존재 여부만 검증 (None이 아닌 bool 반환 확인)
        result = check_three_docs(parsed, task_id=task_id)
        assert result["files_exist"] is not None

    def test_check_three_docs_result_has_required_keys(self, three_docs_meeting):
        """결과에 필수 키 포함 확인"""
        parsed = parse_meeting_file(three_docs_meeting)
        result = check_three_docs(parsed)

        for key in ("status", "mentioned", "files_exist"):
            assert key in result, f"필수 키 누락: {key}"

    def test_check_three_docs_no_task_id_files_exist_none(self, three_docs_meeting):
        """task-id 없으면 files_exist=None"""
        parsed = parse_meeting_file(three_docs_meeting)
        result = check_three_docs(parsed, task_id=None)

        assert result["files_exist"] is None

    def test_check_three_docs_real_meeting_file(self):
        """실제 미팅 파일: 3문서 반영 언급 없음 → WARN"""
        parsed = parse_meeting_file(REAL_MEETING_FILE)
        result = check_three_docs(parsed)

        assert result["status"] == "WARN"
        assert result["mentioned"] is False


# ---------------------------------------------------------------------------
# 16. test_run_audit_full
# ---------------------------------------------------------------------------


class TestRunAuditFull:
    def test_run_audit_full_json_structure(self, minimal_meeting):
        """전체 감사 실행 결과가 올바른 JSON 구조를 가져야 함"""
        result = run_audit(minimal_meeting, level=3)

        assert isinstance(result, dict)
        for key in ("cycle_count", "persona_validation", "da_check", "three_docs"):
            assert key in result, f"감사 결과에 키 누락: {key}"

    def test_run_audit_each_section_has_status(self, minimal_meeting):
        """각 섹션에 status 키가 있어야 함"""
        result = run_audit(minimal_meeting, level=2)

        for section in ("cycle_count", "persona_validation", "da_check", "three_docs"):
            assert "status" in result[section], f"{section}에 status 누락"
            assert result[section]["status"] in (
                "PASS",
                "WARN",
                "FAIL",
            ), f"{section}.status가 유효하지 않음: {result[section]['status']}"

    def test_run_audit_with_task_id(self, minimal_meeting):
        """task_id 파라미터 전달 시 에러 없이 실행되어야 함"""
        result = run_audit(minimal_meeting, level=2, task_id="TASK-TEST-001")
        assert isinstance(result, dict)

    def test_run_audit_serializable_to_json(self, minimal_meeting):
        """결과가 JSON 직렬화 가능해야 함"""
        result = run_audit(minimal_meeting, level=2)
        json_str = json.dumps(result, ensure_ascii=False)
        assert len(json_str) > 0


# ---------------------------------------------------------------------------
# 17. test_empty_meeting_file
# ---------------------------------------------------------------------------


class TestEmptyMeetingFile:
    def test_empty_meeting_file(self, empty_meeting):
        """빈 파일 → 적절한 에러 또는 전체 FAIL"""
        try:
            result = parse_meeting_file(empty_meeting)
            # 빈 파일 파싱 시 예외 없이 반환된 경우:
            # total_cycles가 0 또는 None이거나 전체 감사가 FAIL이어야 함
            if result is not None:
                assert result.get("total_cycles", 0) == 0 or result.get("total_cycles") is None
        except (ValueError, KeyError, AttributeError):
            pass  # 예외 발생도 허용

    def test_empty_meeting_run_audit(self, empty_meeting):
        """빈 파일로 run_audit 실행 시 FAIL 또는 예외"""
        try:
            result = run_audit(empty_meeting, level=2)
            # 결과가 반환된 경우 전체 섹션이 FAIL이어야 함
            if result is not None and isinstance(result, dict):
                statuses = [v.get("status") for v in result.values() if isinstance(v, dict) and "status" in v]
                # 적어도 하나는 FAIL 또는 WARN이어야 함
                assert any(s in ("FAIL", "WARN") for s in statuses)
        except (ValueError, KeyError, AttributeError, FileNotFoundError):
            pass  # 예외 발생도 허용


# ---------------------------------------------------------------------------
# 18. test_nonexistent_file
# ---------------------------------------------------------------------------


class TestNonexistentFile:
    def test_nonexistent_file_parse(self):
        """존재하지 않는 파일 파싱 → FileNotFoundError 또는 에러 dict"""
        fake_path = "/home/jay/workspace/nonexistent-12345.md"

        try:
            result = parse_meeting_file(fake_path)
            # 예외 없이 반환된 경우: 에러 상태를 나타내는 dict여야 함
            assert result is None or (isinstance(result, dict) and "error" in result)
        except FileNotFoundError:
            pass  # 예상된 예외
        except Exception as e:
            # FileNotFoundError 외의 예외는 FileNotFoundError와 동등하게 처리 허용
            assert "not found" in str(e).lower() or "no such file" in str(e).lower() or True

    def test_nonexistent_file_run_audit(self):
        """존재하지 않는 파일로 run_audit → FileNotFoundError 또는 에러 dict"""
        fake_path = "/home/jay/workspace/nonexistent-12345.md"

        try:
            result = run_audit(fake_path, level=2)
            # 예외 없이 반환된 경우: error 키가 있어야 함
            assert result is None or (isinstance(result, dict) and "error" in result)
        except FileNotFoundError:
            pass  # 예상된 예외
        except Exception:
            pass  # 어떤 예외도 허용 (파일 없음 처리)


# ---------------------------------------------------------------------------
# 19. test_run_audit_real_file — 실제 미팅 파일로 전체 감사
# ---------------------------------------------------------------------------


class TestRunAuditRealFile:
    @pytest.mark.skipif(not os.path.exists(REAL_MEETING_FILE), reason="실제 미팅 파일이 존재하지 않습니다.")
    def test_run_audit_real_file(self):
        """
        실제 미팅 파일 + --level 4 기준 검증:
        - cycle_count: FAIL (actual=2, expected_min=3)
        - persona_validation: FAIL (로키 미참석 + 미등록 페르소나 존재)
        - da_check: PASS (DA 섹션, 3대 질문, 반박, 판정 모두 존재)
        - three_docs: WARN (3문서 반영 언급 없음)
        """
        result = run_audit(REAL_MEETING_FILE, level=4)

        assert isinstance(result, dict)

        # cycle_count 검증
        cc = result["cycle_count"]
        assert cc["status"] == "FAIL", f"cycle_count should be FAIL, got {cc['status']}"
        assert cc["actual"] == 2
        assert cc["expected_min"] == 3

        # persona_validation 검증
        pv = result["persona_validation"]
        assert pv["status"] == "FAIL", f"persona_validation should be FAIL, got {pv['status']}"
        assert pv["loki_present"] is False
        assert len(pv["unregistered"]) >= 1

        # da_check 검증
        da = result["da_check"]
        assert da["status"] == "PASS", f"da_check should be PASS, got {da['status']}"
        assert da["da_sections_found"] >= 1
        assert da["three_questions"] is True
        assert da["rebuttal_found"] is True
        assert da["verdict_found"] is True

        # three_docs 검증
        td = result["three_docs"]
        assert td["status"] == "WARN", f"three_docs should be WARN, got {td['status']}"
        assert td["mentioned"] is False

    @pytest.mark.skipif(not os.path.exists(REAL_MEETING_FILE), reason="실제 미팅 파일이 존재하지 않습니다.")
    def test_run_audit_real_file_lv2(self):
        """실제 미팅 파일 + Lv.2: cycle_count PASS (2사이클 >= min 1)"""
        result = run_audit(REAL_MEETING_FILE, level=2)

        cc = result["cycle_count"]
        assert cc["status"] == "PASS"
        assert cc["actual"] == 2

    @pytest.mark.skipif(not os.path.exists(REAL_MEETING_FILE), reason="실제 미팅 파일이 존재하지 않습니다.")
    def test_run_audit_real_file_json_serializable(self):
        """실제 미팅 파일 감사 결과가 JSON 직렬화 가능해야 함"""
        result = run_audit(REAL_MEETING_FILE, level=3)
        json_str = json.dumps(result, ensure_ascii=False)
        assert len(json_str) > 0
        # 역직렬화 검증
        restored = json.loads(json_str)
        assert restored["cycle_count"]["status"] in ("PASS", "WARN", "FAIL")
