"""test_reporter.py - TDD 테스트 (RED → GREEN)"""

import sys
import tempfile
import unittest
from pathlib import Path

# 모듈 경로 추가
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))

from scripts.autoresearch.reporter import estimate_cost, generate_report


def _make_log_data(
    skill: str = "ad-creative",
    started_at: str = "2026-03-26T00:00:00+00:00",
    ended_at: str = "2026-03-26T01:30:00+00:00",
    rounds: list | None = None,
    final_score: float = 0.8,
    total_rounds: int = 5,
    kept: int = 3,
    reverted: int = 2,
) -> dict:
    return {
        "skill": skill,
        "started_at": started_at,
        "ended_at": ended_at,
        "rounds": rounds if rounds is not None else [],
        "final_score": final_score,
        "total_rounds": total_rounds,
        "kept": kept,
        "reverted": reverted,
    }


def _make_round(
    round_num: int = 1,
    mutation_type: str = "규칙 추가",
    mutation_description: str = "새 규칙 추가",
    score_before: float = 0.6,
    score_after: float = 0.8,
    decision: str = "kept",
    input_tokens: int = 100,
    output_tokens: int = 50,
    items_detail: list | None = None,
) -> dict:
    return {
        "round": round_num,
        "mutation_type": mutation_type,
        "mutation_description": mutation_description,
        "score_before": score_before,
        "score_after": score_after,
        "items_detail": (
            items_detail if items_detail is not None else [{"id": "clarity", "result": "PASS", "reason": "ok"}]
        ),
        "decision": decision,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
    }


class TestEstimateCost(unittest.TestCase):
    """estimate_cost() 테스트"""

    def test_sonnet_input_rate(self) -> None:
        """Sonnet: input 1M 토큰 = $3"""
        cost = estimate_cost(input_tokens=1_000_000, output_tokens=0, model="sonnet")
        self.assertAlmostEqual(cost, 3.0, places=6)

    def test_sonnet_output_rate(self) -> None:
        """Sonnet: output 1M 토큰 = $15"""
        cost = estimate_cost(input_tokens=0, output_tokens=1_000_000, model="sonnet")
        self.assertAlmostEqual(cost, 15.0, places=6)

    def test_sonnet_combined(self) -> None:
        """Sonnet: input 50,000 + output 25,000 토큰"""
        cost = estimate_cost(input_tokens=50_000, output_tokens=25_000, model="sonnet")
        expected = (50_000 / 1_000_000) * 3.0 + (25_000 / 1_000_000) * 15.0
        self.assertAlmostEqual(cost, expected, places=6)

    def test_haiku_input_rate(self) -> None:
        """Haiku: input 1M 토큰 = $0.80"""
        cost = estimate_cost(input_tokens=1_000_000, output_tokens=0, model="haiku")
        self.assertAlmostEqual(cost, 0.80, places=6)

    def test_haiku_output_rate(self) -> None:
        """Haiku: output 1M 토큰 = $4"""
        cost = estimate_cost(input_tokens=0, output_tokens=1_000_000, model="haiku")
        self.assertAlmostEqual(cost, 4.0, places=6)

    def test_haiku_combined(self) -> None:
        """Haiku: input 1M + output 1M 토큰"""
        cost = estimate_cost(input_tokens=1_000_000, output_tokens=1_000_000, model="haiku")
        self.assertAlmostEqual(cost, 4.80, places=6)

    def test_default_model_is_sonnet(self) -> None:
        """model 미지정 시 sonnet 단가 사용"""
        cost_default = estimate_cost(input_tokens=100_000, output_tokens=50_000)
        cost_sonnet = estimate_cost(input_tokens=100_000, output_tokens=50_000, model="sonnet")
        self.assertAlmostEqual(cost_default, cost_sonnet, places=6)

    def test_zero_tokens(self) -> None:
        """토큰 0이면 비용 0"""
        cost = estimate_cost(input_tokens=0, output_tokens=0)
        self.assertAlmostEqual(cost, 0.0, places=6)


class TestGenerateReportBasic(unittest.TestCase):
    """generate_report() 기본 동작 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()

    def test_empty_rounds_generates_report(self) -> None:
        """빈 rounds → 기본 보고서 생성 (오류 없음)"""
        log_data = _make_log_data(rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        result = generate_report(log_data, output_path)
        self.assertIsInstance(result, str)
        self.assertTrue(len(result) > 0)

    def test_returns_absolute_path(self) -> None:
        """저장된 파일의 절대 경로 반환"""
        log_data = _make_log_data(rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        result = generate_report(log_data, output_path)
        self.assertTrue(Path(result).is_absolute())

    def test_report_file_created_at_output_path(self) -> None:
        """보고서 파일이 output_path에 저장됨"""
        log_data = _make_log_data(rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        result = generate_report(log_data, output_path)
        self.assertTrue(Path(result).exists())
        self.assertTrue(Path(result).is_file())

    def test_auto_create_parent_directory(self) -> None:
        """부모 디렉토리가 없으면 자동 생성"""
        nonexistent_dir = Path(self.tmpdir) / "subdir" / "nested"
        output_path = str(nonexistent_dir / "report.md")
        log_data = _make_log_data(rounds=[])
        result = generate_report(log_data, output_path)
        self.assertTrue(Path(result).exists())

    def test_report_contains_skill_name(self) -> None:
        """보고서에 스킬명 포함"""
        log_data = _make_log_data(skill="my-skill", rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("my-skill", content)


class TestGenerateReportSections(unittest.TestCase):
    """generate_report() 섹션 구분 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()
        self.kept_round = _make_round(
            round_num=1,
            mutation_type="규칙 추가",
            mutation_description="새 규칙 추가",
            score_before=0.6,
            score_after=0.8,
            decision="kept",
        )
        self.reverted_round = _make_round(
            round_num=2,
            mutation_type="표현 수정",
            mutation_description="지시문 변경",
            score_before=0.8,
            score_after=0.7,
            decision="reverted",
        )
        self.log_data = _make_log_data(
            rounds=[self.kept_round, self.reverted_round],
            final_score=0.8,
            kept=1,
            reverted=1,
        )

    def test_kept_rounds_appear_in_kept_section(self) -> None:
        """kept 라운드가 '유지된 변경' 섹션에 표시됨"""
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(self.log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # 유지 섹션에 kept 라운드 설명 포함
        self.assertIn("새 규칙 추가", content)
        self.assertIn("규칙 추가", content)

    def test_reverted_rounds_appear_in_reverted_section(self) -> None:
        """reverted 라운드가 '롤백된 변경' 섹션에 표시됨"""
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(self.log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("지시문 변경", content)
        self.assertIn("표현 수정", content)

    def test_kept_and_reverted_in_separate_sections(self) -> None:
        """kept/reverted 섹션이 분리되어 있음"""
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(self.log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # 두 섹션 헤더가 모두 존재
        self.assertIn("유지", content)
        self.assertIn("롤백", content)

    def test_empty_kept_section_handled(self) -> None:
        """kept 라운드가 없어도 섹션 표시"""
        log_data = _make_log_data(
            rounds=[self.reverted_round],
            kept=0,
            reverted=1,
        )
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIsInstance(content, str)

    def test_empty_reverted_section_handled(self) -> None:
        """reverted 라운드가 없어도 섹션 표시"""
        log_data = _make_log_data(
            rounds=[self.kept_round],
            kept=1,
            reverted=0,
        )
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIsInstance(content, str)


class TestGenerateReportScoreChange(unittest.TestCase):
    """점수 변화율 계산 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()

    def test_score_change_positive(self) -> None:
        """초기점수 0.60 → 최종점수 0.92, 변화율 +53.3%"""
        log_data = _make_log_data(
            rounds=[_make_round(score_before=0.6, score_after=0.92, decision="kept")],
            final_score=0.92,
        )
        # rounds[0].score_before가 초기 점수
        log_data["rounds"][0]["score_before"] = 0.6
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # +53.3% 변화율 포함 (반올림 오차 허용: 53.3 또는 53.33)
        self.assertTrue("+53.3" in content or "53.33" in content or "53.3%" in content)

    def test_score_change_zero_initial(self) -> None:
        """초기 점수가 0이면 변화율 표시 (ZeroDivision 없음)"""
        log_data = _make_log_data(
            rounds=[_make_round(score_before=0.0, score_after=0.5, decision="kept")],
            final_score=0.5,
        )
        log_data["rounds"][0]["score_before"] = 0.0
        output_path = str(Path(self.tmpdir) / "report.md")
        # ZeroDivisionError 없이 실행되어야 함
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIsInstance(content, str)

    def test_initial_score_from_first_round(self) -> None:
        """초기 점수는 첫 번째 라운드의 score_before"""
        rounds = [
            _make_round(round_num=1, score_before=0.5, score_after=0.7, decision="kept"),
            _make_round(round_num=2, score_before=0.7, score_after=0.9, decision="kept"),
        ]
        log_data = _make_log_data(rounds=rounds, final_score=0.9)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # 초기 점수 0.50 표시
        self.assertIn("0.50", content)

    def test_final_score_in_report(self) -> None:
        """최종 점수가 보고서에 표시됨"""
        log_data = _make_log_data(
            rounds=[_make_round(score_before=0.6, score_after=0.8, decision="kept")],
            final_score=0.8,
        )
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("0.80", content)


class TestGenerateReportCostSection(unittest.TestCase):
    """비용 섹션 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()

    def test_cost_section_exists(self) -> None:
        """비용 섹션이 보고서에 포함됨"""
        rounds = [
            _make_round(input_tokens=50_000, output_tokens=25_000, decision="kept"),
        ]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("비용", content)

    def test_total_tokens_in_report(self) -> None:
        """입력/출력 토큰 합계가 보고서에 표시됨"""
        rounds = [
            _make_round(input_tokens=50_000, output_tokens=25_000, decision="kept"),
            _make_round(input_tokens=10_000, output_tokens=5_000, decision="reverted"),
        ]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # 총 입력 60,000 또는 60000 포함
        self.assertTrue("60,000" in content or "60000" in content)

    def test_cost_estimation_in_report(self) -> None:
        """예상 비용($)이 보고서에 표시됨"""
        rounds = [
            _make_round(input_tokens=50_000, output_tokens=25_000, decision="kept"),
        ]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("$", content)

    def test_zero_tokens_no_error(self) -> None:
        """토큰 0이어도 오류 없음"""
        rounds = [_make_round(input_tokens=0, output_tokens=0, decision="kept")]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("$", content)


class TestGenerateReportMetadata(unittest.TestCase):
    """보고서 메타데이터(시작/종료 시간, 총 라운드) 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()

    def test_started_at_in_report(self) -> None:
        """시작 시간이 보고서에 포함됨"""
        log_data = _make_log_data(
            started_at="2026-03-26T00:00:00+00:00",
            rounds=[],
        )
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        # 날짜 정보 포함
        self.assertIn("2026-03-26", content)

    def test_total_rounds_in_report(self) -> None:
        """총 라운드 수가 보고서에 포함됨"""
        log_data = _make_log_data(total_rounds=50, rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("50", content)

    def test_report_is_markdown(self) -> None:
        """보고서가 마크다운 형식 (# 헤더 포함)"""
        log_data = _make_log_data(rounds=[])
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("#", content)


def _make_round_with_breakdown(
    round_num: int = 1,
    mutation_type: str = "규칙 추가",
    mutation_description: str = "새 규칙 추가",
    score_before: float = 0.6,
    score_after: float = 0.8,
    decision: str = "kept",
    input_tokens: int = 0,
    output_tokens: int = 0,
    mutation_input_tokens: int = 0,
    mutation_output_tokens: int = 0,
    execution_input_tokens: int = 0,
    execution_output_tokens: int = 0,
    judge_input_tokens: int = 0,
    judge_output_tokens: int = 0,
    items_detail: list | None = None,
) -> dict:
    d = _make_round(
        round_num=round_num,
        mutation_type=mutation_type,
        mutation_description=mutation_description,
        score_before=score_before,
        score_after=score_after,
        decision=decision,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        items_detail=items_detail,
    )
    d["mutation_input_tokens"] = mutation_input_tokens
    d["mutation_output_tokens"] = mutation_output_tokens
    d["execution_input_tokens"] = execution_input_tokens
    d["execution_output_tokens"] = execution_output_tokens
    d["judge_input_tokens"] = judge_input_tokens
    d["judge_output_tokens"] = judge_output_tokens
    return d


class TestGenerateReportCostBreakdown(unittest.TestCase):
    """비용 세분화 테스트"""

    def setUp(self) -> None:
        self.tmpdir = tempfile.mkdtemp()

    def test_cost_breakdown_with_new_fields(self) -> None:
        """새 토큰 필드가 있으면 Sonnet+Haiku 분리 비용 표시"""
        rounds = [
            _make_round_with_breakdown(
                mutation_input_tokens=10000,
                mutation_output_tokens=5000,
                execution_input_tokens=20000,
                execution_output_tokens=10000,
                judge_input_tokens=5000,
                judge_output_tokens=3000,
            )
        ]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("Sonnet:", content)
        self.assertIn("Haiku:", content)

    def test_cost_backward_compat(self) -> None:
        """새 필드 없으면 기존 Sonnet 기준 표시"""
        rounds = [_make_round(input_tokens=50000, output_tokens=25000, decision="kept")]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")
        self.assertIn("Sonnet 기준", content)

    def test_cost_calculation_accuracy(self) -> None:
        """비용 계산 정확도 검증"""
        rounds = [
            _make_round_with_breakdown(
                mutation_input_tokens=100000,
                mutation_output_tokens=50000,
                execution_input_tokens=100000,
                execution_output_tokens=50000,
                judge_input_tokens=100000,
                judge_output_tokens=50000,
            )
        ]
        log_data = _make_log_data(rounds=rounds)
        output_path = str(Path(self.tmpdir) / "report.md")
        generate_report(log_data, output_path)
        content = Path(output_path).read_text(encoding="utf-8")

        # Sonnet: (200000/1M)*3 + (100000/1M)*15 = 0.6 + 1.5 = 2.1
        # Haiku: (100000/1M)*0.8 + (50000/1M)*4 = 0.08 + 0.2 = 0.28
        # Total: 2.38
        self.assertIn("2.38", content)


if __name__ == "__main__":
    unittest.main()
