"""
test_report_utils.py

scripts/report_utils.py 단위 테스트 (TDD RED 단계)

테스트 항목:
1. SCQA 형식 보고서 - **A** 부분 정확 추출
2. SCQA 형식 - A 뒤에 **C** 등 다른 Bold 패턴에서 끊김
3. SCQA 형식 - A 뒤에 --- 에서 끊김
4. 비SCQA 형식 - 첫 ## 섹션 본문 추출
5. 아무 패턴도 없는 경우 - 첫 10줄 반환
6. 파일 없는 경우 - None 반환
7. 빈 파일 - None 또는 빈 문자열 처리
8. 500자 초과 truncation
9. max_chars 커스텀 값
10. **A.** 패턴 (마침표 포함)
11. 멀티라인 A 내용
12. 존재하지 않는 경로 → 에러 없이 None 반환
13. extract_report_metadata - test_summary, files_count, unresolved, team_id, duration 추출
14. format_notification_message - 알림 메시지 포맷
15. extract_report_summary max_chars 기본값 1000 변경
"""

import sys
from pathlib import Path

import pytest

# scripts 디렉토리를 import path에 추가
sys.path.insert(0, str(Path(__file__).parent.parent))

from report_utils import extract_report_metadata, extract_report_summary, format_notification_message


class TestExtractReportSummary:
    """report_utils.extract_report_summary 테스트"""

    # -----------------------------------------------------------------------
    # 1. SCQA 형식 - **A**: 패턴 추출
    # -----------------------------------------------------------------------

    def test_scqa_a_colon_extracts_answer(self, tmp_path: Path) -> None:
        """**A**: 패턴에서 A 내용을 정확히 추출해야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**S**: 상황 설명\n\n"
            "**C**: 복잡한 문제 발생\n\n"
            "**Q**: 어떻게 해결할까?\n\n"
            "**A**: 로그 분석 도구를 도입하여 에러 추적을 자동화했다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "로그 분석 도구" in result

    # -----------------------------------------------------------------------
    # 2. SCQA 형식 - A 뒤 다른 Bold 패턴에서 끊김
    # -----------------------------------------------------------------------

    def test_scqa_a_stops_at_next_bold_pattern(self, tmp_path: Path) -> None:
        """**A**: 내용 이후 **Q**: 등 다른 Bold 패턴에서 끊겨야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**A**: 내용은 여기까지입니다.\n\n" "**Q**: 이건 포함되면 안 됩니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "내용은 여기까지입니다." in result
        assert "이건 포함되면 안 됩니다." not in result

    # -----------------------------------------------------------------------
    # 3. SCQA 형식 - A 뒤 --- 에서 끊김
    # -----------------------------------------------------------------------

    def test_scqa_a_stops_at_horizontal_rule(self, tmp_path: Path) -> None:
        """**A**: 내용 이후 --- 구분선에서 끊겨야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**A**: 해결책은 캐시를 적용하는 것이다.\n" "---\n" "이건 포함되면 안 됩니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "해결책은 캐시를 적용하는 것이다." in result
        assert "이건 포함되면 안 됩니다." not in result

    # -----------------------------------------------------------------------
    # 4. 비SCQA 형식 - 첫 ## 섹션 본문 추출
    # -----------------------------------------------------------------------

    def test_non_scqa_extracts_first_section_body(self, tmp_path: Path) -> None:
        """SCQA 패턴 없을 때 첫 ## 섹션 본문을 추출해야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "# 작업 보고서\n\n" "## 작업 내용\n\n" "버그를 수정했습니다.\n\n" "## 결과\n\n" "테스트 통과\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "버그를 수정했습니다." in result

    def test_non_scqa_section_body_excludes_next_section(self, tmp_path: Path) -> None:
        """첫 ## 섹션 본문 추출 시 다음 ## 섹션 내용은 포함되지 않아야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "## 요약\n\n" "데이터베이스 연결 문제를 해결했습니다.\n\n" "## 상세 내용\n\n" "이건 포함되면 안 됩니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "데이터베이스 연결 문제를 해결했습니다." in result
        assert "이건 포함되면 안 됩니다." not in result

    # -----------------------------------------------------------------------
    # 5. 아무 패턴도 없는 경우 - 첫 10줄 반환
    # -----------------------------------------------------------------------

    def test_no_pattern_returns_first_10_lines(self, tmp_path: Path) -> None:
        """SCQA도 ## 섹션도 없는 플레인 텍스트는 첫 10줄을 반환해야 한다."""
        lines = [f"줄 {i+1}: 작업 내용 설명입니다." for i in range(20)]
        report = tmp_path / "report.md"
        report.write_text("\n".join(lines), encoding="utf-8")

        result = extract_report_summary(report)

        assert result is not None
        assert "줄 1:" in result
        assert "줄 10:" in result
        # 11번째 줄 이후는 포함되지 않아야 한다
        assert "줄 11:" not in result

    def test_no_pattern_fewer_than_10_lines_returns_all(self, tmp_path: Path) -> None:
        """10줄 미만 플레인 텍스트는 모든 내용을 반환해야 한다."""
        report = tmp_path / "report.md"
        report.write_text("첫 번째 줄\n두 번째 줄\n세 번째 줄\n", encoding="utf-8")

        result = extract_report_summary(report)

        assert result is not None
        assert "첫 번째 줄" in result
        assert "세 번째 줄" in result

    # -----------------------------------------------------------------------
    # 6. 파일 없는 경우 - None 반환
    # -----------------------------------------------------------------------

    def test_nonexistent_file_returns_none(self, tmp_path: Path) -> None:
        """존재하지 않는 파일 경로는 None을 반환해야 한다."""
        missing = tmp_path / "no_such_file.md"

        result = extract_report_summary(missing)

        assert result is None

    def test_nonexistent_file_no_exception(self, tmp_path: Path) -> None:
        """존재하지 않는 파일 경로 호출 시 예외가 발생하지 않아야 한다."""
        missing = tmp_path / "does_not_exist.md"

        try:
            result = extract_report_summary(missing)
            assert result is None
        except Exception as e:
            pytest.fail(f"예외가 발생했습니다: {e}")

    # -----------------------------------------------------------------------
    # 7. 빈 파일
    # -----------------------------------------------------------------------

    def test_empty_file_returns_none_or_empty(self, tmp_path: Path) -> None:
        """0 byte 빈 파일은 None 또는 빈 문자열을 반환해야 한다."""
        empty = tmp_path / "empty.md"
        empty.write_text("", encoding="utf-8")

        result = extract_report_summary(empty)

        # None 또는 빈 문자열 둘 다 허용 (falsy 값이면 OK)
        assert not result  # None 또는 "" 모두 통과

    # -----------------------------------------------------------------------
    # 8. 500자 초과 truncation (기본 max_chars=500)
    # -----------------------------------------------------------------------

    def test_long_answer_truncated_to_default_max_chars(self, tmp_path: Path) -> None:
        """1200자짜리 A 내용은 기본 max_chars(1000)자 이내로 잘려야 한다."""
        long_content = "가" * 1200  # 1200자 한글 내용
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {long_content}\n", encoding="utf-8")

        result = extract_report_summary(report)

        assert result is not None
        assert len(result) <= 1000

    def test_short_answer_not_truncated(self, tmp_path: Path) -> None:
        """500자 이하 내용은 그대로 반환되어야 한다."""
        content = "짧은 해결 방법입니다."
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {content}\n", encoding="utf-8")

        result = extract_report_summary(report)

        assert result is not None
        assert content in result

    # -----------------------------------------------------------------------
    # 9. max_chars 커스텀 값
    # -----------------------------------------------------------------------

    def test_custom_max_chars_100(self, tmp_path: Path) -> None:
        """max_chars=100 전달 시 100자 이내로 잘려야 한다."""
        long_content = "나" * 300  # 300자 내용
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {long_content}\n", encoding="utf-8")

        result = extract_report_summary(report, max_chars=100)

        assert result is not None
        assert len(result) <= 100

    def test_custom_max_chars_50(self, tmp_path: Path) -> None:
        """max_chars=50 전달 시 50자 이내로 잘려야 한다."""
        content = "다" * 200
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {content}\n", encoding="utf-8")

        result = extract_report_summary(report, max_chars=50)

        assert result is not None
        assert len(result) <= 50

    def test_custom_max_chars_1000_returns_full_content(self, tmp_path: Path) -> None:
        """max_chars=1000 전달 시 600자 내용은 잘리지 않아야 한다."""
        content = "라" * 600
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {content}\n", encoding="utf-8")

        result = extract_report_summary(report, max_chars=1000)

        assert result is not None
        # 600자 내용이 잘리지 않고 포함되어야 한다
        assert len(result) >= 600

    # -----------------------------------------------------------------------
    # 10. **A.** 패턴 (마침표 포함)
    # -----------------------------------------------------------------------

    def test_a_dot_pattern_extracted(self, tmp_path: Path) -> None:
        """**A.** 패턴(마침표 포함)도 정상적으로 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**S.** 상황\n\n" "**A.** 해결 방법은 인덱스를 추가하는 것입니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "해결 방법은 인덱스를 추가하는 것입니다." in result

    def test_a_dot_pattern_stops_at_next_bold(self, tmp_path: Path) -> None:
        """**A.** 패턴 이후 **Q**: 등 다른 Bold 섹션 키에서 끊겨야 한다."""
        report = tmp_path / "report.md"
        # 구현의 끊김 조건: **[A-Z]** 패턴 또는 --- 또는 line.endswith("**")
        # **Q**: 형태는 re.match(r"^\s*\*\*[A-Z]\*\*", line) 으로 끊김
        report.write_text(
            "**A.** 이것이 정답입니다.\n\n" "**Q**: 이건 포함되면 안 됩니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "이것이 정답입니다." in result
        assert "이건 포함되면 안 됩니다." not in result

    # -----------------------------------------------------------------------
    # 11. 멀티라인 A 내용
    # -----------------------------------------------------------------------

    def test_multiline_a_content_all_lines_included(self, tmp_path: Path) -> None:
        """**A**: 이후 멀티라인 내용이 다음 섹션 전까지 모두 포함되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**A**: 첫 줄\n" "두번째 줄\n" "세번째 줄\n" "\n" "**다음 섹션**\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "첫 줄" in result
        assert "두번째 줄" in result
        assert "세번째 줄" in result

    def test_multiline_a_excludes_next_section(self, tmp_path: Path) -> None:
        """멀티라인 A 내용 추출 시 다음 **Bold** 섹션은 포함되지 않아야 한다."""
        report = tmp_path / "report.md"
        # 구현의 끊김 조건: line.strip().endswith("**") 이면 끊김
        # **다음 섹션** (콜론 없음) 형태는 끊김 조건에 해당
        report.write_text(
            "**A**: 첫 줄\n" "두번째 줄\n" "\n" "**다음 섹션**\n" "이건 포함되면 안 됩니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "이건 포함되면 안 됩니다." not in result

    # -----------------------------------------------------------------------
    # 12. 존재하지 않는 경로 → 에러 없이 None 반환 (.done 등)
    # -----------------------------------------------------------------------

    def test_nonexistent_done_path_returns_none_no_error(self, tmp_path: Path) -> None:
        """존재하지 않는 .done 경로는 예외 없이 None을 반환해야 한다."""
        missing_done = tmp_path / "reports" / "task-999.done"

        result = extract_report_summary(missing_done)

        assert result is None

    def test_deeply_nested_nonexistent_path_returns_none(self, tmp_path: Path) -> None:
        """중첩 경로의 존재하지 않는 파일도 None을 반환해야 한다."""
        missing = tmp_path / "a" / "b" / "c" / "report.md"

        result = extract_report_summary(missing)

        assert result is None

    # -----------------------------------------------------------------------
    # 추가: SCQA 우선순위 확인
    # -----------------------------------------------------------------------

    def test_scqa_takes_priority_over_section(self, tmp_path: Path) -> None:
        """**A** 패턴이 있으면 ## 섹션보다 우선하여 추출해야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "## 작업 내용\n\n" "섹션 본문 내용입니다.\n\n" "**A**: SCQA 답변 내용입니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "SCQA 답변 내용입니다." in result

    def test_section_takes_priority_over_first_10_lines(self, tmp_path: Path) -> None:
        """## 섹션이 있으면 첫 10줄보다 우선하여 추출해야 한다."""
        lines_before = "\n".join([f"줄 {i}" for i in range(5)])
        report = tmp_path / "report.md"
        report.write_text(
            f"{lines_before}\n\n" "## 요약 섹션\n\n" "섹션 본문입니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "섹션 본문입니다." in result

    # -----------------------------------------------------------------------
    # 추가: 한글 내용 포함 종합 테스트
    # -----------------------------------------------------------------------

    def test_korean_content_in_scqa(self, tmp_path: Path) -> None:
        """한글 SCQA 보고서에서 A 내용이 정확히 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**S**: 배포 이후 응답 속도가 현저히 느려졌습니다.\n\n"
            "**C**: 데이터베이스 쿼리 최적화가 되지 않아 병목이 발생했습니다.\n\n"
            "**Q**: 어떻게 성능을 개선할 수 있을까?\n\n"
            "**A**: 인덱스 추가와 N+1 쿼리 제거로 응답 속도를 3배 개선했습니다.\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "인덱스 추가" in result
        assert "응답 속도를 3배 개선" in result

    def test_korean_content_in_section(self, tmp_path: Path) -> None:
        """한글 ## 섹션 보고서에서 본문이 정확히 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "# 2026-03-24 작업 보고서\n\n"
            "## 작업 요약\n\n"
            "인증 모듈의 토큰 만료 처리 버그를 수정했습니다.\n"
            "JWT 검증 로직을 개선하여 보안을 강화했습니다.\n\n"
            "## 테스트 결과\n\n"
            "모든 테스트 케이스 통과\n",
            encoding="utf-8",
        )

        result = extract_report_summary(report)

        assert result is not None
        assert "인증 모듈의 토큰 만료 처리 버그를 수정했습니다." in result

    # -----------------------------------------------------------------------
    # 추가: max_chars 기본값 1000으로 변경 확인
    # -----------------------------------------------------------------------

    def test_default_max_chars_is_1000(self, tmp_path: Path) -> None:
        """기본값이 1000으로 변경되어 600자 내용이 잘리지 않아야 한다."""
        content = "마" * 600  # 600자 내용
        report = tmp_path / "report.md"
        report.write_text(f"**A**: {content}\n", encoding="utf-8")

        # max_chars 인자 없이 기본값으로 호출
        result = extract_report_summary(report)

        assert result is not None
        # 기본값이 1000이면 600자 내용은 잘리지 않아야 한다
        assert len(result) >= 600


# ---------------------------------------------------------------------------
# 보고서 메타데이터 추출 테스트
# ---------------------------------------------------------------------------

# task-906.1 기반 테스트용 보고서 픽스처 내용
TASK_906_1_REPORT = """\
# task-906.1 완료 보고서

**담당**: 오딘 (dev2-team 팀장)

---

## SCQA

**A**: auto_orch.py (591 LOC) + team_lock.py (75 LOC) 구현 완료. pytest 64건 전건 통과. pyright 0 에러.

---

## 생성/수정 파일 목록

- `orchestrator/auto_orch.py` — 메인 오케스트레이터
- `orchestrator/team_lock.py` — 팀별 뮤텍스
- `orchestrator/tests/test_phase3.py` — 테스트
- `~/.config/systemd/user/auto-orch.timer` — 타이머
- `~/.config/systemd/user/auto-orch.service` — 서비스
- `orchestrator/health.json` — 헬스 파일

---

## 테스트 결과 (정량적 증거)

**Phase 3 pytest**: 32/32 passed (0.45s)

**pyright**: 0 errors, 0 warnings

---

## 발견 이슈 및 해결

### 범위 외 미해결 (1건)

1. **qc_verify.py pyright 환경 이슈** — 범위 외 사유: qc_verify.py 자체 수정은 본 작업 범위 외
"""


TASK_924_1_REPORT = """\
# task-924.1 InfoKeyword 키워드 우선순위 평가 기능 추가

**담당**: 헤르메스 (dev1-team 팀장)
소요시간: 6분 12초

---

## SCQA

**A**: `prioritize_results()` 함수를 추가하여 백엔드에서 정렬 후 반환하도록 개선.
1. 정보성 키워드: 검색량 높은 순으로 정렬
2. 홍보성 키워드: `informational_count` 높은 순 → 동일 시 검색량 순
3. 각 결과에 `priority` + `priority_reason` 필드 추가
pytest 28건 통과, pyright 에러 0건

---

## 생성/수정 파일 목록

- `workers/info_keyword/prioritizer.py` — 우선순위 평가 함수
- `workers/info_keyword/models.py` — priority 필드 추가
- `workers/info_keyword/tests/test_prioritizer.py` — 테스트
- `workers/info_keyword/tests/test_contract.py` — 계약 테스트 갱신

---

## 발견 이슈 및 해결

### 자체 해결 (3건)
1. **black/isort 일부 불일치** — 재실행으로 해결
2. **total_blogs 미존재 가능성** — .get() 기본값 처리
3. **priority=0 falsy 문제** — 해당 없음 확인

### 범위 외 미해결 (0건)
없음
"""


class TestExtractReportMetadata:
    """report_utils.extract_report_metadata 테스트"""

    # -----------------------------------------------------------------------
    # 1. test_summary 추출 - "pytest 64건 통과" 같은 패턴
    # -----------------------------------------------------------------------

    def test_test_summary_pytest_pattern(self, tmp_path: Path) -> None:
        """SCQA A 절에서 pytest 결과 패턴이 test_summary로 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**A**: 모듈 구현 완료. pytest 64건 전건 통과.\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["test_summary"] is not None
        assert "pytest" in result["test_summary"]
        assert "64" in result["test_summary"]

    # -----------------------------------------------------------------------
    # 2. test_summary - pyright 결과 포함
    # -----------------------------------------------------------------------

    def test_test_summary_includes_pyright(self, tmp_path: Path) -> None:
        """test_summary에 pyright 0 에러 결과가 포함되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**A**: 구현 완료. pytest 64건 전건 통과. pyright 0 에러.\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["test_summary"] is not None
        assert "pyright" in result["test_summary"]

    # -----------------------------------------------------------------------
    # 3. files_count 추출 - "생성/수정 파일 목록" 섹션 bullet(-) 수
    # -----------------------------------------------------------------------

    def test_files_count_from_bullet_list(self, tmp_path: Path) -> None:
        """'생성/수정 파일 목록' 섹션에서 bullet(-) 수를 files_count로 추출해야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "## 생성/수정 파일 목록\n\n"
            "- `file_a.py` — 설명 A\n"
            "- `file_b.py` — 설명 B\n"
            "- `file_c.py` — 설명 C\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["files_count"] == 3

    # -----------------------------------------------------------------------
    # 4. unresolved 추출 - "범위 외 미해결" 섹션에서 항목 추출
    # -----------------------------------------------------------------------

    def test_unresolved_items_extracted(self, tmp_path: Path) -> None:
        """'범위 외 미해결' 섹션의 항목들이 unresolved_items 리스트로 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "### 범위 외 미해결 (1건)\n\n" "1. **qc_verify.py pyright 환경 이슈** — 범위 외 사유: 범위 외\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["unresolved_count"] == 1
        assert len(result["unresolved_items"]) == 1
        assert "qc_verify.py pyright 환경 이슈" in result["unresolved_items"][0]

    # -----------------------------------------------------------------------
    # 5. unresolved 없는 경우 - unresolved_count=0, unresolved_items=[]
    # -----------------------------------------------------------------------

    def test_no_unresolved_returns_zero_and_empty_list(self, tmp_path: Path) -> None:
        """'범위 외 미해결' 섹션이 없으면 unresolved_count=0, unresolved_items=[]이어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "## 발견 이슈 및 해결\n\n" "모든 이슈 해결 완료.\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["unresolved_count"] == 0
        assert result["unresolved_items"] == []

    # -----------------------------------------------------------------------
    # 6. team_id 추출 - "팀:" 또는 "team:" 패턴
    # -----------------------------------------------------------------------

    def test_team_id_extracted_korean_pattern(self, tmp_path: Path) -> None:
        """'담당' 줄에서 괄호 안 'dev2-team' 형태 팀 ID가 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "**담당**: 오딘 (dev2-team 팀장)\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["team_id"] is not None
        assert "dev2-team" in result["team_id"]

    def test_team_id_extracted_english_pattern(self, tmp_path: Path) -> None:
        """'team:' 패턴에서 팀 ID가 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "team: dev3-team\n\n" "작업 내용 설명\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["team_id"] is not None
        assert "dev3-team" in result["team_id"]

    # -----------------------------------------------------------------------
    # 7. duration 추출 - "소요시간" 패턴
    # -----------------------------------------------------------------------

    def test_duration_extracted(self, tmp_path: Path) -> None:
        """'소요시간' 패턴에서 duration이 추출되어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "소요시간: 12분 30초\n\n" "작업 내용\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["duration"] is not None
        assert "12분 30초" in result["duration"]

    def test_duration_none_when_not_present(self, tmp_path: Path) -> None:
        """'소요시간' 패턴이 없으면 duration은 None이어야 한다."""
        report = tmp_path / "report.md"
        report.write_text(
            "## 작업 내용\n\n" "버그 수정 완료.\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["duration"] is None

    # -----------------------------------------------------------------------
    # 8. 파일 없는 경우 - 모든 필드가 None/0/빈리스트
    # -----------------------------------------------------------------------

    def test_nonexistent_file_returns_empty_metadata(self, tmp_path: Path) -> None:
        """존재하지 않는 파일은 모든 필드가 None/0/빈리스트인 dict를 반환해야 한다."""
        missing = tmp_path / "no_such_report.md"

        result = extract_report_metadata(missing)

        assert isinstance(result, dict)
        assert result["test_summary"] is None
        assert result["files_count"] is None or result["files_count"] == 0
        assert result["unresolved_count"] == 0
        assert result["unresolved_items"] == []
        assert result["team_id"] is None
        assert result["duration"] is None

    # -----------------------------------------------------------------------
    # 9. 빈 파일 - 모든 필드가 None/0/빈리스트
    # -----------------------------------------------------------------------

    def test_empty_file_returns_empty_metadata(self, tmp_path: Path) -> None:
        """빈 파일은 모든 필드가 None/0/빈리스트인 dict를 반환해야 한다."""
        empty = tmp_path / "empty.md"
        empty.write_text("", encoding="utf-8")

        result = extract_report_metadata(empty)

        assert isinstance(result, dict)
        assert result["test_summary"] is None
        assert result["files_count"] is None or result["files_count"] == 0
        assert result["unresolved_count"] == 0
        assert result["unresolved_items"] == []
        assert result["team_id"] is None
        assert result["duration"] is None

    # -----------------------------------------------------------------------
    # 10. 실제 보고서 task-906.1 형태 전체 파싱 확인
    # -----------------------------------------------------------------------

    def test_task_906_1_full_parsing(self, tmp_path: Path) -> None:
        """task-906.1 형태 실제 보고서 전체 파싱이 정확해야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = extract_report_metadata(report)

        assert isinstance(result, dict)
        # test_summary: pytest/pyright 결과 포함
        assert result["test_summary"] is not None
        assert "pytest" in result["test_summary"] or "pyright" in result["test_summary"]
        # files_count: bullet 6개
        assert result["files_count"] == 6
        # unresolved: 1건, qc_verify.py 이슈
        assert result["unresolved_count"] == 1
        assert len(result["unresolved_items"]) == 1
        assert "qc_verify.py" in result["unresolved_items"][0]
        # team_id: dev2-team
        assert result["team_id"] is not None
        assert "dev2-team" in result["team_id"]

    # -----------------------------------------------------------------------
    # 신규: title 추출 - 첫 # 헤더에서 task ID 이후 텍스트
    # -----------------------------------------------------------------------

    def test_title_extracted_from_first_heading(self, tmp_path: Path) -> None:
        """보고서 첫 # 헤더에서 task ID 이후 텍스트를 title로 추출해야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = extract_report_metadata(report)

        assert result["title"] == "InfoKeyword 키워드 우선순위 평가 기능 추가"

    def test_title_none_when_no_heading(self, tmp_path: Path) -> None:
        """# 헤더가 없으면 title은 None이어야 한다."""
        report = tmp_path / "no_heading.md"
        report.write_text(
            "**담당**: 테스트 (dev1-team 팀장)\n\n**A**: 작업 완료.\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert result["title"] is None

    def test_issues_resolved_extracted(self, tmp_path: Path) -> None:
        """### 자체 해결 섹션에서 이슈 목록이 구조화된 형태로 추출되어야 한다."""
        report = tmp_path / "issues.md"
        report.write_text(
            "### 자체 해결 (2건)\n"
            "1. **black/isort 불일치** — 재실행으로 해결\n"
            "2. **total_blogs 미존재** — .get() 기본값 처리\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert "issues_resolved" in result
        assert len(result["issues_resolved"]) == 2
        assert result["issues_resolved"][0]["summary"] == "black/isort 불일치"
        assert result["issues_resolved"][0]["resolution"] == "재실행으로 해결"
        assert result["issues_resolved"][1]["summary"] == "total_blogs 미존재"
        assert result["issues_resolved"][1]["resolution"] == ".get() 기본값 처리"

    def test_issues_resolved_empty_when_no_section(self, tmp_path: Path) -> None:
        """### 자체 해결 섹션이 없으면 issues_resolved는 빈 리스트여야 한다."""
        report = tmp_path / "no_resolved.md"
        report.write_text(
            "**A**: 작업 완료.\n\n## 생성/수정 파일 목록\n\n- `file.py` — 설명\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert "issues_resolved" in result
        assert result["issues_resolved"] == []

    def test_issues_unresolved_extracted(self, tmp_path: Path) -> None:
        """### 범위 외 미해결 섹션에서 이슈가 구조화된 형태로 추출되어야 한다."""
        report = tmp_path / "unresolved.md"
        report.write_text(
            "### 범위 외 미해결 (1건)\n"
            "1. **qc_verify.py pyright 환경 이슈** — 범위 외 사유: 본 작업 범위 외\n",
            encoding="utf-8",
        )

        result = extract_report_metadata(report)

        assert "issues_unresolved" in result
        assert len(result["issues_unresolved"]) == 1
        assert result["issues_unresolved"][0]["summary"] == "qc_verify.py pyright 환경 이슈"
        assert "범위 외" in result["issues_unresolved"][0]["resolution"]


# ---------------------------------------------------------------------------
# 알림 메시지 포맷 테스트
# ---------------------------------------------------------------------------


class TestFormatNotificationMessage:
    """report_utils.format_notification_message 테스트"""

    # -----------------------------------------------------------------------
    # 1. 기본 포맷 - task_id + summary + metadata 결합
    # -----------------------------------------------------------------------

    def test_basic_format_contains_task_id(self, tmp_path: Path) -> None:
        """반환 메시지에 task_id가 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert "task-906.1" in result

    def test_basic_format_contains_summary(self, tmp_path: Path) -> None:
        """반환 메시지에 보고서 요약(summary)이 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert result is not None
        assert len(result) > 0

    def test_basic_format_contains_metadata(self, tmp_path: Path) -> None:
        """반환 메시지에 메타데이터(파일 수, 테스트 결과 등)가 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        # 파일 수 또는 테스트 결과 중 하나 이상 포함되어야 한다
        has_files = "6" in result or "파일" in result
        has_test = "pytest" in result or "테스트" in result or "pyright" in result
        assert has_files or has_test

    # -----------------------------------------------------------------------
    # 2. done_data 포함 - team_id, duration_seconds 반영
    # -----------------------------------------------------------------------

    def test_done_data_team_id_in_message(self, tmp_path: Path) -> None:
        """done_data에 team_id가 있으면 메시지에 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")
        done_data = {"team_id": "dev2-team", "duration_seconds": 750}

        result = format_notification_message("task-906.1", report, done_data=done_data)

        assert "dev2-team" in result

    def test_done_data_duration_seconds_in_message(self, tmp_path: Path) -> None:
        """done_data에 duration_seconds가 있으면 사람이 읽기 좋은 형태로 메시지에 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")
        done_data = {"team_id": "dev2-team", "duration_seconds": 750}

        result = format_notification_message("task-906.1", report, done_data=done_data)

        # 12분 30초 또는 750초 형태로 포함되어야 한다
        assert "12분" in result or "750" in result or "분" in result

    # -----------------------------------------------------------------------
    # 3. 보고서 없는 경우 - 기본 메시지만 반환
    # -----------------------------------------------------------------------

    def test_missing_report_returns_basic_message(self, tmp_path: Path) -> None:
        """보고서 파일이 없어도 예외 없이 기본 메시지를 반환해야 한다."""
        missing = tmp_path / "no_such_report.md"

        result = format_notification_message("task-999.1", missing)

        assert result is not None
        assert "task-999.1" in result

    def test_missing_report_no_exception(self, tmp_path: Path) -> None:
        """보고서 파일이 없을 때 예외가 발생하지 않아야 한다."""
        missing = tmp_path / "no_such_report.md"

        try:
            result = format_notification_message("task-999.1", missing)
            assert result is not None
        except Exception as e:
            pytest.fail(f"예외가 발생했습니다: {e}")

    # -----------------------------------------------------------------------
    # 4. Telegram 4096자 제한 확인
    # -----------------------------------------------------------------------

    def test_message_within_telegram_limit(self, tmp_path: Path) -> None:
        """반환 메시지가 Telegram 4096자 제한 이내여야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")
        done_data = {"team_id": "dev2-team", "duration_seconds": 750}

        result = format_notification_message("task-906.1", report, done_data=done_data)

        assert len(result) <= 4096

    def test_long_report_message_within_telegram_limit(self, tmp_path: Path) -> None:
        """매우 긴 보고서도 4096자 제한을 초과하지 않아야 한다."""
        long_content = "가" * 5000
        report = tmp_path / "long_report.md"
        report.write_text(
            f"**A**: {long_content}\n\n"
            "## 생성/수정 파일 목록\n\n" + "".join([f"- `file_{i}.py` — 설명\n" for i in range(50)]),
            encoding="utf-8",
        )

        result = format_notification_message("task-long.1", report)

        assert len(result) <= 4096

    # -----------------------------------------------------------------------
    # 5. done_data None인 경우 - summary만 포함
    # -----------------------------------------------------------------------

    def test_none_done_data_no_exception(self, tmp_path: Path) -> None:
        """done_data=None이어도 예외 없이 메시지가 반환되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        try:
            result = format_notification_message("task-906.1", report, done_data=None)
            assert result is not None
        except Exception as e:
            pytest.fail(f"예외가 발생했습니다: {e}")

    def test_none_done_data_contains_task_id(self, tmp_path: Path) -> None:
        """done_data=None이어도 task_id가 메시지에 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report, done_data=None)

        assert "task-906.1" in result

    # -----------------------------------------------------------------------
    # 6. 미해결 이슈 포함 - ⚠️ 미해결 표시
    # -----------------------------------------------------------------------

    def test_unresolved_issues_marked_in_message(self, tmp_path: Path) -> None:
        """미해결 이슈가 있으면 메시지에 경고 표시(⚠️ 또는 '미해결')가 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert "⚠️" in result or "미해결" in result

    def test_unresolved_count_shown_in_message(self, tmp_path: Path) -> None:
        """미해결 건수(1건)가 메시지에 표시되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert "1건" in result or "1" in result

    # -----------------------------------------------------------------------
    # 7. 테스트 결과 포함 - 🧪 표시
    # -----------------------------------------------------------------------

    def test_test_result_marked_in_message(self, tmp_path: Path) -> None:
        """테스트 결과가 있으면 메시지에 🧪 표시 또는 '테스트' 문구가 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert "🧪" in result or "테스트" in result or "pytest" in result

    # -----------------------------------------------------------------------
    # 8. 파일 수 포함 - 📁 표시
    # -----------------------------------------------------------------------

    def test_files_count_marked_in_message(self, tmp_path: Path) -> None:
        """파일 수가 있으면 메시지에 📁 표시 또는 '파일' 문구가 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        assert "📁" in result or "파일" in result

    # -----------------------------------------------------------------------
    # 신규: 새 포맷 테스트
    # -----------------------------------------------------------------------

    def test_new_format_bold_header(self, tmp_path: Path) -> None:
        """첫 줄이 **task-XXX.X 완료 보고** 형태여야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-924.1", report)

        assert result.startswith("**task-")
        assert "완료 보고**" in result

    def test_new_format_includes_title(self, tmp_path: Path) -> None:
        """보고서 제목(task ID 제거 후 텍스트)이 2번째 줄에 포함되어야 한다."""
        report = tmp_path / "task-906.1.md"
        report.write_text(TASK_906_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-906.1", report)

        lines = result.splitlines()
        assert len(lines) >= 2
        assert "완료 보고서" in lines[1]

    def test_new_format_includes_duration(self, tmp_path: Path) -> None:
        """done_data에 duration_seconds가 있으면 '6분 12초' 형태로 제목 옆에 표시되어야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")
        done_data = {"duration_seconds": 372}

        result = format_notification_message("task-924.1", report, done_data=done_data)

        assert "6분 12초" in result

    def test_new_format_core_results_section(self, tmp_path: Path) -> None:
        """**핵심 결과** 섹션이 메시지에 포함되어야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-924.1", report)

        assert "**핵심 결과**" in result

    def test_new_format_issues_section(self, tmp_path: Path) -> None:
        """자체 해결 섹션이 있으면 **발견/해결 이슈 섹션이 메시지에 포함되어야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-924.1", report)

        assert "**발견/해결 이슈" in result

    def test_new_format_no_emojis(self, tmp_path: Path) -> None:
        """새 포맷에서 이모지(✅📁🧪⚠️📄)가 제거되어야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-924.1", report)

        assert "✅" not in result
        assert "📁" not in result
        assert "🧪" not in result
        assert "⚠️" not in result
        assert "📄" not in result

    def test_new_format_numbered_list_in_results(self, tmp_path: Path) -> None:
        """핵심 결과가 번호 리스트(1. 2. 3.) 형태로 표시되어야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")

        result = format_notification_message("task-924.1", report)

        assert "1." in result

    def test_new_format_telegram_limit(self, tmp_path: Path) -> None:
        """새 포맷도 Telegram 4096자 제한을 준수해야 한다."""
        report = tmp_path / "task-924.1.md"
        report.write_text(TASK_924_1_REPORT, encoding="utf-8")
        done_data = {"duration_seconds": 372}

        result = format_notification_message("task-924.1", report, done_data=done_data)

        assert len(result) <= 4096
