"""
test_pattern_detector.py

scripts/pattern-detector.py 단위 테스트 (TDD)

테스트 항목:
1. 각 패턴 타입 감지 (보고서 내용 모킹)
   - test_missing 감지
   - pyright_error 감지
   - scope_exceeded 감지
   - qc_fail 감지
   - regression 감지
2. 팀 추출 로직 테스트
   - 보고서 내용에서 팀 추출
   - task-timers.json 역참조
   - 팀 추출 실패 시 "unknown"
3. risk_score 계산 테스트
4. 빈 보고서 디렉토리 graceful 처리
5. --days 필터링 테스트
6. team-patterns.json 없는 경우 (신규 생성)
"""

import importlib.util
import json
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pytest

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

# pattern-detector.py는 하이픈이 있으므로 importlib으로 임포트
_MODULE_PATH = _SCRIPTS_DIR / "pattern-detector.py"
spec = importlib.util.spec_from_file_location("pattern_detector", _MODULE_PATH)
assert spec is not None
pattern_detector = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(pattern_detector)

# ---------------------------------------------------------------------------
# Helper 함수
# ---------------------------------------------------------------------------


def make_reports_dir(tmp_path: Path) -> Path:
    reports_dir = tmp_path / "memory" / "reports"
    reports_dir.mkdir(parents=True, exist_ok=True)
    return reports_dir


def make_task_timers(tmp_path: Path, tasks: dict) -> Path:
    memory_dir = tmp_path / "memory"
    memory_dir.mkdir(parents=True, exist_ok=True)
    f = memory_dir / "task-timers.json"
    f.write_text(json.dumps({"tasks": tasks}), encoding="utf-8")
    return tmp_path


def make_whisper_dir(tmp_path: Path) -> Path:
    whisper_dir = tmp_path / "memory" / "whisper"
    whisper_dir.mkdir(parents=True, exist_ok=True)
    return whisper_dir


def write_report(reports_dir: Path, filename: str, content: str) -> Path:
    f = reports_dir / filename
    f.write_text(content, encoding="utf-8")
    return f


# ---------------------------------------------------------------------------
# 1. 패턴 감지 테스트
# ---------------------------------------------------------------------------


class TestPatternDetection:
    """각 패턴 타입 감지 테스트"""

    def test_detect_test_missing_keywords(self):
        """테스트 미작성 패턴 감지"""
        contents = [
            "작업 완료. 테스트 없이 배포함.",
            "이번 작업은 test 없이 진행.",
            "테스트 미작성 상태로 제출",
            "일정 문제로 테스트 SKIP 처리",
            "시간 부족으로 테스트 생략",
        ]
        for content in contents:
            patterns = pattern_detector.detect_patterns(content)
            assert "test_missing" in patterns, f"test_missing not detected in: {content!r}"

    def test_detect_pyright_error_keywords(self):
        """pyright 에러 반복 패턴 감지"""
        contents = [
            "pyright 에러 5건 발생",
            "pyright error: Cannot find module",
            "타입 에러 발생으로 빌드 실패",
        ]
        for content in contents:
            patterns = pattern_detector.detect_patterns(content)
            assert "pyright_error" in patterns, f"pyright_error not detected in: {content!r}"

    def test_detect_scope_exceeded_keywords(self):
        """scope 초과 패턴 감지"""
        contents = [
            "scope 초과로 추가 작업 필요",
            "범위 초과 발생, 다음 태스크로 이월",
            "예상치 못한 추가 작업 발생",
            "작업이 예상보다 커짐",
        ]
        for content in contents:
            patterns = pattern_detector.detect_patterns(content)
            assert "scope_exceeded" in patterns, f"scope_exceeded not detected in: {content!r}"

    def test_detect_qc_fail_keywords(self):
        """QC FAIL 패턴 감지"""
        contents = [
            "QC FAIL — 재작업 필요",
            "qc_verify: FAIL",
            "검증 결과: FAIL",
        ]
        for content in contents:
            patterns = pattern_detector.detect_patterns(content)
            assert "qc_fail" in patterns, f"qc_fail not detected in: {content!r}"

    def test_detect_regression_keywords(self):
        """회귀 발생 패턴 감지"""
        contents = [
            "배포 후 회귀 발생 확인",
            "regression 발생, 기존 기능 깨짐",
            "기존 테스트 실패 발생",
        ]
        for content in contents:
            patterns = pattern_detector.detect_patterns(content)
            assert "regression" in patterns, f"regression not detected in: {content!r}"

    def test_no_false_positives(self):
        """정상 보고서에서 패턴 미감지"""
        content = """
# task-100.1 완료 보고서

## 테스트 결과
모든 테스트 통과: 45/45 PASSED
pyright: 에러 없음
QC 통과
회귀 없음
"""
        patterns = pattern_detector.detect_patterns(content)
        assert len(patterns) == 0, f"False positives detected: {patterns}"

    def test_detect_multiple_patterns(self):
        """복수 패턴 동시 감지"""
        content = "테스트 없이 배포. 또한 pyright 에러 발생. QC FAIL."
        patterns = pattern_detector.detect_patterns(content)
        assert "test_missing" in patterns
        assert "pyright_error" in patterns
        assert "qc_fail" in patterns

    def test_detect_case_insensitive_qc_fail(self):
        """qc_fail: qc_verify.*FAIL 정규식 패턴 감지"""
        content = "qc_verify: status=FAIL, 재시도 필요"
        patterns = pattern_detector.detect_patterns(content)
        assert "qc_fail" in patterns


# ---------------------------------------------------------------------------
# 2. 팀 추출 로직 테스트
# ---------------------------------------------------------------------------


class TestTeamExtraction:
    """보고서에서 팀 추출 로직 테스트"""

    def test_extract_team_from_report_content(self):
        """보고서 내용에서 팀 추출 (팀: 패턴)"""
        content = "**팀:** dev1-team\n작업 완료"
        team = pattern_detector.extract_team_from_content(content, "task-100.1.md", {})
        assert team == "dev1-team"

    def test_extract_team_from_team_id_field(self):
        """보고서 내용에서 team_id: 패턴 추출"""
        content = "team_id: dev2-team\n작업 내용"
        team = pattern_detector.extract_team_from_content(content, "task-100.1.md", {})
        assert team == "dev2-team"

    def test_extract_team_from_task_timers(self):
        """task-timers.json에서 team_id 역참조"""
        tasks = {
            "task-200": {
                "task_id": "task-200",
                "team_id": "dev3-team",
            }
        }
        content = "일반 보고서 내용"
        team = pattern_detector.extract_team_from_content(content, "task-200.1.md", tasks)
        assert team == "dev3-team"

    def test_extract_team_unknown_fallback(self):
        """팀 추출 실패 시 unknown 반환"""
        content = "팀 정보 없는 보고서"
        team = pattern_detector.extract_team_from_content(content, "task-999.1.md", {})
        assert team == "unknown"

    def test_extract_team_priority_content_over_timers(self):
        """보고서 내용의 팀 정보가 task-timers보다 우선"""
        tasks = {
            "task-300": {
                "task_id": "task-300",
                "team_id": "dev3-team",
            }
        }
        content = "**팀:** dev1-team\n작업 완료"
        team = pattern_detector.extract_team_from_content(content, "task-300.1.md", tasks)
        assert team == "dev1-team"

    def test_extract_task_id_from_filename(self):
        """파일명에서 task-id 추출"""
        assert pattern_detector.extract_task_id_from_filename("task-100.1.md") == "task-100"
        assert pattern_detector.extract_task_id_from_filename("task-200.2.md") == "task-200"
        assert pattern_detector.extract_task_id_from_filename("random-report.md") is None

    def test_extract_team_various_formats(self):
        """다양한 팀 표기 형식 추출"""
        contents_teams = [
            ("**팀:** dev2-team", "dev2-team"),
            ("팀: dev3-team", "dev3-team"),
            ("team: dev1-team", "dev1-team"),
        ]
        for content, expected in contents_teams:
            team = pattern_detector.extract_team_from_content(content, "x.md", {})
            assert team == expected, f"Expected {expected!r} from {content!r}, got {team!r}"


# ---------------------------------------------------------------------------
# 3. risk_score 계산 테스트
# ---------------------------------------------------------------------------


class TestRiskScoreCalculation:
    """risk_score 계산 테스트"""

    def test_risk_score_single_qc_fail(self):
        """QC FAIL 1건, 보고서 10건 시 risk_score"""
        # qc_fail weight = 0.3, count = 1, total_reports = 10
        # risk_score = (1 * 0.3) / 10 = 0.03
        pattern_counts = {"qc_fail": 1}
        score = pattern_detector.calculate_risk_score(pattern_counts, 10)
        assert abs(score - 0.03) < 1e-9

    def test_risk_score_multiple_patterns(self):
        """복수 패턴 risk_score 계산"""
        # qc_fail(3*0.3) + test_missing(2*0.25) + pyright_error(1*0.2) = 0.9+0.5+0.2 = 1.6
        # total_reports = 10 → 1.6/10 = 0.16
        pattern_counts = {"qc_fail": 3, "test_missing": 2, "pyright_error": 1}
        score = pattern_detector.calculate_risk_score(pattern_counts, 10)
        assert abs(score - 0.16) < 1e-9

    def test_risk_score_zero_when_no_patterns(self):
        """패턴 없을 때 risk_score = 0"""
        score = pattern_detector.calculate_risk_score({}, 10)
        assert score == 0.0

    def test_risk_score_zero_when_no_reports(self):
        """보고서 없을 때 risk_score = 0 (0 나누기 방지)"""
        score = pattern_detector.calculate_risk_score({"qc_fail": 5}, 0)
        assert score == 0.0

    def test_risk_score_all_pattern_weights(self):
        """모든 패턴 타입 가중치 검증"""
        weights = pattern_detector.PATTERN_WEIGHTS
        assert weights["qc_fail"] == 0.3
        assert weights["test_missing"] == 0.25
        assert weights["pyright_error"] == 0.2
        assert weights["scope_exceeded"] == 0.15
        assert weights["regression"] == 0.1

    def test_risk_score_clamped_or_reasonable(self):
        """극단적 케이스에서 risk_score가 합리적 범위"""
        # 1건의 보고서에 모든 패턴 100번씩 → 값이 매우 클 수 있지만 함수는 그대로 계산
        pattern_counts = {
            "qc_fail": 10,
            "test_missing": 10,
            "pyright_error": 10,
            "scope_exceeded": 10,
            "regression": 10,
        }
        score = pattern_detector.calculate_risk_score(pattern_counts, 1)
        # (10*0.3 + 10*0.25 + 10*0.2 + 10*0.15 + 10*0.1) / 1 = 10 * 1.0 / 1 = 10.0
        assert abs(score - 10.0) < 1e-9


# ---------------------------------------------------------------------------
# 4. 빈 보고서 디렉토리 graceful 처리
# ---------------------------------------------------------------------------


class TestEmptyReportsDirectory:
    """빈 디렉토리 및 에러 처리 테스트"""

    def test_empty_reports_dir_returns_empty_result(self, tmp_path):
        """빈 보고서 디렉토리 처리"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["total_reports_analyzed"] == 0
        assert result["summary"]["total_patterns_found"] == 0
        assert result["teams"] == {}

    def test_nonexistent_reports_dir_graceful(self, tmp_path):
        """보고서 디렉토리가 없을 때 graceful 처리"""
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["total_reports_analyzed"] == 0

    def test_unreadable_report_skipped(self, tmp_path):
        """읽기 실패 보고서는 건너뜀 (디렉토리를 파일처럼 생성)"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        # 실제 읽을 수 있는 보고서 1개 + 비어있는 파일 1개
        write_report(reports_dir, "task-100.1.md", "정상 보고서. 테스트 SKIP 처리")
        write_report(reports_dir, "task-101.1.md", "")

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        # 빈 파일은 패턴 없지만, 최소한 에러 없이 실행돼야 함
        assert result["summary"]["total_reports_analyzed"] >= 1

    def test_nonexistent_task_timers_graceful(self, tmp_path):
        """task-timers.json 없어도 graceful 처리"""
        reports_dir = make_reports_dir(tmp_path)
        make_whisper_dir(tmp_path)
        write_report(reports_dir, "task-100.1.md", "**팀:** dev1-team\n정상 보고서")

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["total_reports_analyzed"] >= 1


# ---------------------------------------------------------------------------
# 5. --days 필터링 테스트
# ---------------------------------------------------------------------------


class TestDaysFiltering:
    """최근 N일 필터링 테스트"""

    def test_days_filter_excludes_old_reports(self, tmp_path):
        """30일 이전 파일 제외"""
        import os
        import time

        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        # 최신 보고서
        new_report = write_report(
            reports_dir,
            "task-new.md",
            "**팀:** dev1-team\n테스트 SKIP",
        )

        # 오래된 보고서 (60일 전)
        old_report = write_report(
            reports_dir,
            "task-old.md",
            "**팀:** dev1-team\nQC FAIL",
        )
        old_time = time.time() - (60 * 24 * 3600)
        os.utime(str(old_report), (old_time, old_time))

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        # 30일 이내 파일만 분석 → 1건
        assert result["summary"]["total_reports_analyzed"] == 1

    def test_days_filter_includes_recent_reports(self, tmp_path):
        """days=60이면 60일 이내 파일 포함"""
        import os
        import time

        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        # 최신 보고서
        write_report(reports_dir, "task-new.md", "**팀:** dev1-team\n정상")

        # 45일 전 보고서
        old_report = write_report(
            reports_dir,
            "task-old.md",
            "**팀:** dev1-team\n정상",
        )
        old_time = time.time() - (45 * 24 * 3600)
        os.utime(str(old_report), (old_time, old_time))

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=60,
        )

        assert result["summary"]["total_reports_analyzed"] == 2

    def test_days_filter_zero_means_all(self, tmp_path):
        """days=0이면 모든 파일 포함"""
        import os
        import time

        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(reports_dir, "task-1.md", "**팀:** dev1-team\n정상")
        very_old = write_report(reports_dir, "task-2.md", "**팀:** dev2-team\n정상")
        old_time = time.time() - (365 * 24 * 3600)
        os.utime(str(very_old), (old_time, old_time))

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=0,
        )

        assert result["summary"]["total_reports_analyzed"] == 2


# ---------------------------------------------------------------------------
# 6. team-patterns.json 신규 생성 테스트
# ---------------------------------------------------------------------------


class TestOutputFile:
    """team-patterns.json 저장 테스트"""

    def test_creates_new_team_patterns_json(self, tmp_path):
        """team-patterns.json이 없으면 신규 생성"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        whisper_dir = make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\n테스트 SKIP 처리",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        output_path = whisper_dir / "team-patterns.json"
        pattern_detector.save_results(result, str(output_path))

        assert output_path.exists()
        data = json.loads(output_path.read_text(encoding="utf-8"))
        assert data["version"] == 1
        assert "last_updated" in data
        assert "teams" in data
        assert "summary" in data

    def test_output_structure_has_required_fields(self, tmp_path):
        """출력 JSON 구조 검증"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL — 재작업 필요",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["version"] == 1
        assert "last_updated" in result
        assert "teams" in result
        assert "summary" in result
        assert "total_reports_analyzed" in result["summary"]
        assert "total_patterns_found" in result["summary"]
        assert "highest_risk_team" in result["summary"]

    def test_team_entry_structure(self, tmp_path):
        """팀 항목 구조 검증"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert "dev1-team" in result["teams"]
        team_data = result["teams"]["dev1-team"]
        assert "patterns" in team_data
        assert "risk_score" in team_data
        assert isinstance(team_data["patterns"], list)
        assert isinstance(team_data["risk_score"], float)

    def test_pattern_entry_structure(self, tmp_path):
        """패턴 항목 구조 검증"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        team_data = result["teams"]["dev1-team"]
        assert len(team_data["patterns"]) > 0
        pattern_entry = team_data["patterns"][0]
        assert "type" in pattern_entry
        assert "count" in pattern_entry
        assert "recent_reports" in pattern_entry
        assert isinstance(pattern_entry["recent_reports"], list)

    def test_whisper_dir_created_if_missing(self, tmp_path):
        """whisper 디렉토리가 없으면 자동 생성"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        # whisper 디렉토리를 만들지 않음

        output_path = tmp_path / "memory" / "whisper" / "team-patterns.json"
        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )
        pattern_detector.save_results(result, str(output_path))

        assert output_path.exists()


# ---------------------------------------------------------------------------
# 7. 통합 테스트: 전체 흐름
# ---------------------------------------------------------------------------


class TestIntegration:
    """analyze_reports 통합 테스트"""

    def test_full_analysis_multiple_teams(self, tmp_path):
        """복수 팀, 복수 보고서 분석"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(
            tmp_path,
            {
                "task-200": {"task_id": "task-200", "team_id": "dev2-team"},
            },
        )
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL — 재작업 필요\n테스트 SKIP",
        )
        write_report(
            reports_dir,
            "task-101.1.md",
            "**팀:** dev1-team\npyright 에러 3건 발생",
        )
        write_report(
            reports_dir,
            "task-200.1.md",
            "**팀:** dev2-team\n정상 완료",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["total_reports_analyzed"] == 3
        assert "dev1-team" in result["teams"]
        # dev2-team은 패턴이 없으므로 teams에 포함되지 않음 (정상 동작)

        dev1_patterns = {p["type"]: p["count"] for p in result["teams"]["dev1-team"]["patterns"]}
        assert dev1_patterns.get("qc_fail", 0) >= 1
        assert dev1_patterns.get("test_missing", 0) >= 1
        assert dev1_patterns.get("pyright_error", 0) >= 1

    def test_highest_risk_team_in_summary(self, tmp_path):
        """highest_risk_team이 가장 높은 risk_score 팀"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL\n테스트 SKIP\npyright 에러",
        )
        write_report(
            reports_dir,
            "task-200.1.md",
            "**팀:** dev2-team\n정상 완료",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["highest_risk_team"] == "dev1-team"

    def test_total_patterns_count(self, tmp_path):
        """total_patterns_found 합산 정확성"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        # dev1: qc_fail(1) + test_missing(1) = 2
        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL\n테스트 SKIP",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert result["summary"]["total_patterns_found"] >= 2

    def test_recent_reports_list_in_pattern(self, tmp_path):
        """recent_reports에 파일명이 포함되는지 확인"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(tmp_path, {})
        make_whisper_dir(tmp_path)

        write_report(
            reports_dir,
            "task-100.1.md",
            "**팀:** dev1-team\nQC FAIL",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        dev1_patterns = result["teams"]["dev1-team"]["patterns"]
        qc_pattern = next((p for p in dev1_patterns if p["type"] == "qc_fail"), None)
        assert qc_pattern is not None
        assert "task-100.1.md" in qc_pattern["recent_reports"]

    def test_task_timers_team_lookup(self, tmp_path):
        """task-timers.json 역참조로 팀 식별"""
        reports_dir = make_reports_dir(tmp_path)
        make_task_timers(
            tmp_path,
            {
                "task-500": {"task_id": "task-500", "team_id": "dev3-team"},
            },
        )
        make_whisper_dir(tmp_path)

        # 보고서 내용에는 팀 정보 없고, task-timers에만 있음
        write_report(
            reports_dir,
            "task-500.1.md",
            "작업 완료. 테스트 SKIP.",
        )

        result = pattern_detector.analyze_reports(
            workspace=str(tmp_path),
            days=30,
        )

        assert "dev3-team" in result["teams"]
