"""aio_tracker.py 단위 테스트 모음.

TDD RED→GREEN 절차에 따라 구현 전에 먼저 작성.
모든 테스트 데이터는 샘플 데이터임.
"""

import csv
import io
import textwrap
from pathlib import Path

import pytest

# 구현 모듈 임포트 (아직 존재하지 않으므로 RED 단계에서 ImportError 발생)
from aio_tracker import (
    AioTracker,
    calculate_change_rate,
    generate_markdown_report,
    parse_csv,
)


# ---------------------------------------------------------------------------
# 픽스처
# ---------------------------------------------------------------------------


@pytest.fixture
def sample_before_csv(tmp_path: Path) -> Path:
    """샘플 데이터: Before 기간 CSV 파일."""
    content = textwrap.dedent("""\
        referrer,sessions
        chat.openai.com,120
        perplexity.ai,45
        gemini.google.com,30
        claude.ai,10
        search.naver.com,200
        google.com,500
    """)
    p = tmp_path / "before.csv"
    p.write_text(content, encoding="utf-8")
    return p


@pytest.fixture
def sample_after_csv(tmp_path: Path) -> Path:
    """샘플 데이터: After 기간 CSV 파일."""
    content = textwrap.dedent("""\
        referrer,sessions
        chat.openai.com,185
        perplexity.ai,72
        gemini.google.com,28
        claude.ai,25
        search.naver.com,240
        google.com,480
        bing.com,15
    """)
    p = tmp_path / "after.csv"
    p.write_text(content, encoding="utf-8")
    return p


@pytest.fixture
def sample_before_csv_source_col(tmp_path: Path) -> Path:
    """샘플 데이터: 'source' 컬럼명 사용 CSV (referrer 대신)."""
    content = textwrap.dedent("""\
        source,sessions
        chat.openai.com,100
        perplexity.ai,50
    """)
    p = tmp_path / "before_source.csv"
    p.write_text(content, encoding="utf-8")
    return p


# ---------------------------------------------------------------------------
# parse_csv 테스트
# ---------------------------------------------------------------------------


class TestParseCsv:
    """CSV 파일 파싱 테스트."""

    def test_parse_referrer_sessions(self, sample_before_csv: Path):
        """referrer, sessions 컬럼을 올바르게 파싱해야 한다."""
        result = parse_csv(sample_before_csv)
        assert isinstance(result, dict)
        assert result["chat.openai.com"] == 120
        assert result["perplexity.ai"] == 45
        assert result["google.com"] == 500

    def test_parse_source_column(self, sample_before_csv_source_col: Path):
        """'source' 컬럼도 'referrer' 컬럼과 동일하게 파싱해야 한다."""
        result = parse_csv(sample_before_csv_source_col)
        assert result["chat.openai.com"] == 100
        assert result["perplexity.ai"] == 50

    def test_parse_returns_int_sessions(self, sample_before_csv: Path):
        """sessions 값은 정수로 반환되어야 한다."""
        result = parse_csv(sample_before_csv)
        for v in result.values():
            assert isinstance(v, int)

    def test_parse_empty_file(self, tmp_path: Path):
        """헤더만 있는 빈 CSV는 빈 딕셔너리를 반환해야 한다."""
        p = tmp_path / "empty.csv"
        p.write_text("referrer,sessions\n", encoding="utf-8")
        result = parse_csv(p)
        assert result == {}

    def test_parse_file_not_found(self, tmp_path: Path):
        """존재하지 않는 파일은 FileNotFoundError를 발생시켜야 한다."""
        with pytest.raises(FileNotFoundError):
            parse_csv(tmp_path / "nonexistent.csv")


# ---------------------------------------------------------------------------
# calculate_change_rate 테스트
# ---------------------------------------------------------------------------


class TestCalculateChangeRate:
    """변화율 계산 테스트."""

    def test_increase(self):
        """증가 시 양수 변화율을 반환해야 한다."""
        rate = calculate_change_rate(before=120, after=185)
        assert abs(rate - 54.166) < 0.01

    def test_decrease(self):
        """감소 시 음수 변화율을 반환해야 한다."""
        rate = calculate_change_rate(before=100, after=80)
        assert abs(rate - (-20.0)) < 0.01

    def test_no_change(self):
        """변화 없으면 0.0을 반환해야 한다."""
        assert calculate_change_rate(before=100, after=100) == 0.0

    def test_before_zero_returns_none(self):
        """before가 0이면 None을 반환해야 한다 (NEW 표시용)."""
        result = calculate_change_rate(before=0, after=50)
        assert result is None

    def test_both_zero_returns_none(self):
        """before와 after 모두 0이면 None을 반환해야 한다."""
        result = calculate_change_rate(before=0, after=0)
        assert result is None

    def test_returns_float(self):
        """반환값은 float이어야 한다."""
        result = calculate_change_rate(before=100, after=150)
        assert isinstance(result, float)


# ---------------------------------------------------------------------------
# AioTracker 클래스 테스트
# ---------------------------------------------------------------------------


class TestAioTracker:
    """AioTracker 핵심 기능 테스트."""

    def test_init_with_csv(self, sample_before_csv: Path, sample_after_csv: Path):
        """CSV 경로로 초기화하면 데이터가 로드되어야 한다."""
        tracker = AioTracker(
            before_csv=sample_before_csv,
            after_csv=sample_after_csv,
        )
        assert tracker.before_data is not None
        assert tracker.after_data is not None

    def test_compute_ai_summary_known_sources(
        self, sample_before_csv: Path, sample_after_csv: Path
    ):
        """ChatGPT, Perplexity 등 알려진 AI 소스가 요약에 포함되어야 한다."""
        tracker = AioTracker(
            before_csv=sample_before_csv,
            after_csv=sample_after_csv,
        )
        summary = tracker.compute_ai_summary()

        # 알려진 AI 소스가 요약에 포함되어야 함
        source_names = [row["source"] for row in summary]
        assert "ChatGPT" in source_names
        assert "Perplexity" in source_names

    def test_compute_ai_summary_excludes_non_ai(
        self, sample_before_csv: Path, sample_after_csv: Path
    ):
        """google.com 등 비-AI 소스는 요약에서 제외되어야 한다."""
        tracker = AioTracker(
            before_csv=sample_before_csv,
            after_csv=sample_after_csv,
        )
        summary = tracker.compute_ai_summary()
        source_names = [row["source"] for row in summary]
        assert "기타" not in source_names

    def test_compute_ai_summary_change_rate(
        self, sample_before_csv: Path, sample_after_csv: Path
    ):
        """ChatGPT 변화율이 정확하게 계산되어야 한다 (120→185, +54.2%)."""
        tracker = AioTracker(
            before_csv=sample_before_csv,
            after_csv=sample_after_csv,
        )
        summary = tracker.compute_ai_summary()
        chatgpt_row = next(r for r in summary if r["source"] == "ChatGPT")
        assert chatgpt_row["before"] == 120
        assert chatgpt_row["after"] == 185
        assert abs(chatgpt_row["change_rate"] - 54.166) < 0.01

    def test_compute_ai_summary_new_source(
        self, sample_before_csv: Path, sample_after_csv: Path
    ):
        """Before에 없던 소스는 change_rate가 None이어야 한다 (NEW)."""
        tracker = AioTracker(
            before_csv=sample_before_csv,
            after_csv=sample_after_csv,
        )
        summary = tracker.compute_ai_summary()
        # bing.com은 after에만 있으므로 기타에 포함되나 AI 소스 아님 — 이 케이스는 별도 테스트

    def test_compute_ai_summary_empty_data(self, tmp_path: Path):
        """빈 CSV 두 개로 초기화하면 요약이 빈 리스트여야 한다."""
        before_p = tmp_path / "empty_before.csv"
        after_p = tmp_path / "empty_after.csv"
        before_p.write_text("referrer,sessions\n", encoding="utf-8")
        after_p.write_text("referrer,sessions\n", encoding="utf-8")

        tracker = AioTracker(before_csv=before_p, after_csv=after_p)
        summary = tracker.compute_ai_summary()
        assert summary == []

    def test_compute_ai_summary_decrease(self, tmp_path: Path):
        """감소 케이스: gemini.google.com 30→28, 변화율 음수."""
        before_p = tmp_path / "b.csv"
        after_p = tmp_path / "a.csv"
        before_p.write_text("referrer,sessions\ngemini.google.com,30\n", encoding="utf-8")
        after_p.write_text("referrer,sessions\ngemini.google.com,28\n", encoding="utf-8")

        tracker = AioTracker(before_csv=before_p, after_csv=after_p)
        summary = tracker.compute_ai_summary()
        gemini_row = next((r for r in summary if r["source"] == "Gemini"), None)
        assert gemini_row is not None
        assert gemini_row["change_rate"] < 0


# ---------------------------------------------------------------------------
# generate_markdown_report 테스트
# ---------------------------------------------------------------------------


class TestGenerateMarkdownReport:
    """마크다운 리포트 생성 테스트."""

    @pytest.fixture
    def sample_summary(self):
        """샘플 데이터: compute_ai_summary 반환값 형태."""
        return [
            {"source": "ChatGPT", "before": 120, "after": 185, "change_rate": 54.166},
            {"source": "Perplexity", "before": 45, "after": 72, "change_rate": 60.0},
            {"source": "Gemini", "before": 30, "after": 28, "change_rate": -6.666},
            {"source": "Claude", "before": 0, "after": 25, "change_rate": None},
        ]

    def test_report_contains_title(self, sample_summary):
        """리포트에 '# AIO 성과 리포트' 제목이 포함되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "# AIO 성과 리포트" in report

    def test_report_contains_table_header(self, sample_summary):
        """리포트에 마크다운 표 헤더가 포함되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "| 소스 |" in report
        assert "| Before |" in report
        assert "| After |" in report
        assert "| 변화율 |" in report

    def test_report_chatgpt_row(self, sample_summary):
        """ChatGPT 행이 올바른 값으로 포함되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "ChatGPT" in report
        assert "120" in report
        assert "185" in report
        assert "+54.2%" in report

    def test_report_new_display(self, sample_summary):
        """change_rate가 None인 소스는 변화율 셀에 'NEW'가 표시되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "NEW" in report

    def test_report_negative_change(self, sample_summary):
        """음수 변화율은 '-6.7%' 형태로 표시되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "-6.7%" in report

    def test_report_date_label(self, sample_summary):
        """지정한 날짜 레이블이 리포트에 포함되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "2026-03-26" in report

    def test_report_empty_summary(self):
        """빈 요약 데이터도 에러 없이 리포트를 생성해야 한다."""
        report = generate_markdown_report([], date_label="2026-03-26")
        assert "# AIO 성과 리포트" in report

    def test_report_perplexity_plus_sign(self, sample_summary):
        """양수 변화율은 '+' 기호와 함께 표시되어야 한다."""
        report = generate_markdown_report(sample_summary, date_label="2026-03-26")
        assert "+60.0%" in report


# ---------------------------------------------------------------------------
# AI 소스 분류 통합 테스트 (config.classify_ai_source 연동)
# ---------------------------------------------------------------------------


class TestAiSourceClassification:
    """config.py의 classify_ai_source와의 통합 테스트."""

    def test_chatgpt_classified(self, tmp_path: Path):
        """chat.openai.com 레퍼러는 ChatGPT로 분류되어야 한다."""
        from config import classify_ai_source

        assert classify_ai_source("https://chat.openai.com/") == "ChatGPT"

    def test_perplexity_classified(self):
        """perplexity.ai 레퍼러는 Perplexity로 분류되어야 한다."""
        from config import classify_ai_source

        assert classify_ai_source("https://www.perplexity.ai/search?q=보험") == "Perplexity"

    def test_naver_aio_classified(self):
        """search.naver.com은 '네이버 AIO'로 분류되어야 한다."""
        from config import classify_ai_source

        assert classify_ai_source("https://search.naver.com/search.naver?q=보험") == "네이버 AIO"

    def test_unknown_referrer_is_other(self):
        """알 수 없는 레퍼러는 '기타'로 분류되어야 한다."""
        from config import classify_ai_source

        assert classify_ai_source("https://google.com/search?q=보험") == "기타"

    def test_utm_ai_prefix_classified(self):
        """utm_source가 'ai_' 접두사로 시작하면 AI 소스로 분류되어야 한다."""
        from config import classify_ai_source

        result = classify_ai_source("", utm_source="ai_perplexity")
        assert result != "기타"


# ---------------------------------------------------------------------------
# 보험 도메인 샘플 데이터 통합 테스트
# ---------------------------------------------------------------------------


class TestInsuranceDomainSample:
    """보험 도메인 샘플 데이터로 전체 파이프라인 테스트."""

    @pytest.fixture
    def insurance_before_csv(self, tmp_path: Path) -> Path:
        """샘플 데이터: 보험 사이트 Before 트래픽."""
        content = textwrap.dedent("""\
            referrer,sessions
            chat.openai.com,320
            perplexity.ai,180
            gemini.google.com,95
            claude.ai,40
            search.naver.com,850
            google.com,4200
            naver.com,3100
            direct,1200
        """)
        p = tmp_path / "insurance_before.csv"
        p.write_text(content, encoding="utf-8")
        return p

    @pytest.fixture
    def insurance_after_csv(self, tmp_path: Path) -> Path:
        """샘플 데이터: 보험 사이트 After 트래픽."""
        content = textwrap.dedent("""\
            referrer,sessions
            chat.openai.com,510
            perplexity.ai,295
            gemini.google.com,88
            claude.ai,120
            search.naver.com,1250
            google.com,4050
            naver.com,2980
            direct,1350
            chatgpt.com,75
        """)
        p = tmp_path / "insurance_after.csv"
        p.write_text(content, encoding="utf-8")
        return p

    def test_insurance_pipeline_runs(
        self, insurance_before_csv: Path, insurance_after_csv: Path
    ):
        """보험 샘플 데이터로 전체 파이프라인이 에러 없이 실행되어야 한다."""
        tracker = AioTracker(
            before_csv=insurance_before_csv,
            after_csv=insurance_after_csv,
        )
        summary = tracker.compute_ai_summary()
        report = generate_markdown_report(summary, date_label="2026-03-26")
        assert isinstance(report, str)
        assert len(report) > 0

    def test_insurance_chatgpt_increase(
        self, insurance_before_csv: Path, insurance_after_csv: Path
    ):
        """보험 샘플: ChatGPT 320→585 증가 (chat.openai.com 510 + chatgpt.com 75).

        config.AI_SOURCES에서 ChatGPT 패턴이 chatgpt.com도 포함하므로
        after 합계는 510+75=585.
        변화율 = (585-320)/320*100 ≈ +82.8%
        """
        tracker = AioTracker(
            before_csv=insurance_before_csv,
            after_csv=insurance_after_csv,
        )
        summary = tracker.compute_ai_summary()
        chatgpt_row = next(r for r in summary if r["source"] == "ChatGPT")
        assert chatgpt_row["before"] == 320
        assert chatgpt_row["after"] == 585  # chat.openai.com(510) + chatgpt.com(75)
        assert abs(chatgpt_row["change_rate"] - 82.8125) < 0.01

    def test_insurance_naver_aio_increase(
        self, insurance_before_csv: Path, insurance_after_csv: Path
    ):
        """보험 샘플: 네이버 AIO 850→1250, 약 +47.1% 증가."""
        tracker = AioTracker(
            before_csv=insurance_before_csv,
            after_csv=insurance_after_csv,
        )
        summary = tracker.compute_ai_summary()
        naver_row = next(r for r in summary if r["source"] == "네이버 AIO")
        assert naver_row["before"] == 850
        assert naver_row["after"] == 1250
        assert abs(naver_row["change_rate"] - 47.058) < 0.01

    def test_insurance_report_markdown_output(
        self, insurance_before_csv: Path, insurance_after_csv: Path
    ):
        """보험 샘플 리포트가 마크다운 표 형식을 포함해야 한다."""
        tracker = AioTracker(
            before_csv=insurance_before_csv,
            after_csv=insurance_after_csv,
        )
        summary = tracker.compute_ai_summary()
        report = generate_markdown_report(summary, date_label="2026-03-26")
        # 마크다운 표 구분선 포함 여부
        assert "|---" in report
        assert "ChatGPT" in report
        assert "네이버 AIO" in report
