"""PR-Eval-GradedNDCG-Dedup — run_eval.py 의 graded NDCG dedup invariant 테스트.

[[feedback_graded_ndcg_dedup_invariant]] regression. graded NDCG 는 top-N unique doc 가정
— retrieval path 가 doc 중복 박제 시 inflation (NDCG > 1.0). dedup helper + 함수 진입
시 dedup 으로 invariant 복원.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path

import pytest

# tests/search_eval/ → 프로젝트 루트
THIS_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(THIS_DIR))  # run_eval 직접 import 위해

from run_eval import (
    _dedup_returned_ids,
    count_dedup,
    graded_ndcg_at_k,
    graded_recall_at_k,
    ndcg_at_k,
)


# ─── 1. _dedup_returned_ids helper ─────────────────────────


def test_dedup_empty_returns_empty():
    out, n = _dedup_returned_ids([], 10)
    assert out == []
    assert n == 0


def test_dedup_no_duplicates_passthrough():
    out, n = _dedup_returned_ids([100, 200, 300], 10)
    assert out == [100, 200, 300]
    assert n == 0


def test_dedup_with_duplicates_first_only():
    """3868 중복 등장 → 첫 등장만 유지, 순서 보존."""
    out, n = _dedup_returned_ids([3868, 3879, 3856, 3851, 3868, 4041, 3890], 10)
    assert out == [3868, 3879, 3856, 3851, 4041, 3890]
    assert n == 1


def test_dedup_k_limit_applied_before_dedup():
    """returned[:k] 만 dedup. k 외부 등장은 무시."""
    # k=3 안에서 dedup
    out, n = _dedup_returned_ids([1, 2, 1, 3], 3)  # [:3] = [1,2,1] → [1,2], n=1
    assert out == [1, 2]
    assert n == 1


def test_dedup_count_helper():
    """count_dedup wrapper 도 정확한 카운트 반환."""
    assert count_dedup([10, 10, 20, 30], 10) == 1
    assert count_dedup([10, 20, 30], 10) == 0


def test_dedup_phase2q_kw_001_case():
    """Phase 2Q Phase 3 의 kw_001 실측 case — 3868 중복 → dedup 1건."""
    # Phase 3 returned: [3868, 3879, 3856, 3851, 3868, ...]
    returned = [3868, 3879, 3856, 3851, 3868, 3858, 3878, 3859, 3850, 3863]
    out, n = _dedup_returned_ids(returned, 10)
    assert n == 1
    assert out[:5] == [3868, 3879, 3856, 3851, 3858]  # 3868 첫 등장 보존, 두 번째 제거


# ─── 2. graded_ndcg_at_k 회귀 0 (dedup 가 정상 case 영향 X) ─────


def test_graded_ndcg_baseline_no_duplicates_unchanged():
    """unique doc 만 있는 case = dedup 영향 0 = 기존 결과 그대로."""
    returned = [3868, 3879, 3856, 4041, 3851, 3890, 3917, 3863, 3908, 3855]
    grades = {3856: 3, 3868: 2, 3879: 2}
    out = graded_ndcg_at_k(returned, grades, 10)
    # 0.808 = baseline kw_001 측정값
    assert 0.80 <= out <= 0.82


def test_graded_ndcg_with_duplicates_no_longer_inflated():
    """중복 doc 박제 case → NDCG ≤ 1.0 invariant 복원 (이전엔 > 1.0 가능)."""
    # 3856 (grade 3) 가 두 번 등장하면 이전 (dedup 미적용) 에서 inflation
    returned = [3856, 3856, 3868, 3879, 3851, 3890, 3917, 3863, 3908, 3855]
    grades = {3856: 3, 3868: 2, 3879: 2}
    out = graded_ndcg_at_k(returned, grades, 10)
    assert out <= 1.0, f"NDCG > 1.0 = invariant 위반: {out}"


def test_graded_ndcg_all_duplicates_invariant():
    """top-N 이 모두 같은 doc 의 중복 = 단 1 entry rank 1 으로 처리."""
    returned = [11504] * 10
    grades = {11504: 3, 11505: 2}
    out = graded_ndcg_at_k(returned, grades, 10)
    # rank 1 만 grade 3 hit → DCG = (2^3-1)/log2(2) = 7
    # iDCG = (2^3-1)/log2(2) + (2^2-1)/log2(3) = 7 + 3/log2(3) ≈ 8.893
    # NDCG ≈ 7 / 8.893 ≈ 0.787
    assert 0.78 <= out <= 0.80


def test_graded_ndcg_phase2q_exam_001_inflation_fix():
    """Phase 2Q Phase 3 의 exam_001 실측 — 11504 중복 박제. dedup 후 정상화."""
    # Phase 3 returned: [11504, 11504, 11533, 5106, 11533, ...] (11504 + 11533 중복)
    returned = [11504, 11504, 11533, 5106, 11533, 11504, 11479, 11475, 11533, 5090]
    grades = {11504: 3, 11505: 2}
    out = graded_ndcg_at_k(returned, grades, 10)
    # 11504 rank 1 만 hit (dedup 후), NDCG ≈ 0.787 (test 9 와 동일 결과)
    assert out <= 1.0, f"inflation 잔재: {out}"
    assert 0.78 <= out <= 0.80, f"예상 0.787 ± noise, 실제 {out}"


def test_graded_ndcg_empty_grades_returns_zero():
    assert graded_ndcg_at_k([1, 2, 3], {}, 10) == 0.0


# ─── 3. ndcg_at_k (binary) 도 dedup 적용 ──────────────────


def test_ndcg_binary_with_duplicates_invariant():
    """binary NDCG 도 같은 invariant."""
    returned = [100, 100, 200, 300]
    relevant = [100, 200]
    out = ndcg_at_k(returned, relevant, 10)
    assert out <= 1.0, f"binary NDCG > 1.0 = invariant 위반: {out}"


# ─── 4. graded_recall_at_k 영향 0 (set 변환 invariant) ────


def test_graded_recall_unaffected_by_duplicates():
    """recall 은 set 변환이라 dedup invariant 자동 만족."""
    grades = {100: 3, 200: 2}
    # 중복 있는 returned
    r1 = graded_recall_at_k([100, 100, 200, 300], grades, threshold=2, k=10)
    # dedup 한 returned
    r2 = graded_recall_at_k([100, 200, 300], grades, threshold=2, k=10)
    assert r1 == r2 == 1.0  # 100, 200 둘 다 hit
