"""
ReportGenerationService 單元測試
================================
ADR-076 Task 4: 自動報告生成

測試範圍:
- DailyKpi 計算屬性（auto_repair_rate, auto_resolve_rate）
- format_daily_report() 報告格式
- format_postmortem() 事後檢討格式
- _seconds_until_next_report() 排程計算
- PostmortemData dataclass

🔴🔴 遵循「禁止 Mock 測試鐵律」
- 純 Python 邏輯：不需要 DB/Redis/Telegram
- DB/Telegram 整合部分標記 @pytest.mark.integration

建立: 2026-04-14 (台北時區) Claude Haiku 4.5
"""

from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace

import pytest

from src.services.report_generation_service import (
    DAILY_REPORT_HOUR_TAIPEI,
    POSTMORTEM_MIN_DURATION_MINUTES,
    DailyKpi,
    PostmortemData,
    ReportGenerationService,
    _seconds_until_next_report,
)

_TZ_TAIPEI = timezone(timedelta(hours=8))


# =============================================================================
# DailyKpi 計算屬性
# =============================================================================


class TestDailyKpiRates:
    """測試 DailyKpi 計算屬性"""

    def _make_kpi(self, **kwargs) -> DailyKpi:
        now = datetime.now(_TZ_TAIPEI)
        return DailyKpi(
            period_start=now - timedelta(hours=24),
            period_end=now,
            **kwargs,
        )

    def test_auto_repair_rate_all_success(self):
        """全部成功 → 100%"""
        kpi = self._make_kpi(auto_repair_success=10, auto_repair_failed=0)
        assert kpi.auto_repair_rate == 1.0

    def test_auto_repair_rate_half(self):
        """5 成功 5 失敗 → 50%"""
        kpi = self._make_kpi(auto_repair_success=5, auto_repair_failed=5)
        assert kpi.auto_repair_rate == 0.5

    def test_auto_repair_rate_zero_attempts(self):
        """無嘗試 → 0%（不除以零）"""
        kpi = self._make_kpi(auto_repair_success=0, auto_repair_failed=0)
        assert kpi.auto_repair_rate == 0.0

    def test_auto_resolve_rate(self):
        """10 個告警 6 個自動解決 → 60%"""
        kpi = self._make_kpi(total_alerts=10, auto_resolved=6)
        assert kpi.auto_resolve_rate == 0.6

    def test_auto_resolve_rate_zero_alerts(self):
        """無告警 → 0%（不除以零）"""
        kpi = self._make_kpi(total_alerts=0, auto_resolved=0)
        assert kpi.auto_resolve_rate == 0.0


# =============================================================================
# format_daily_report
# =============================================================================


class TestFormatDailyReport:
    """測試日度巡檢報告格式"""

    def _make_kpi(self, **kwargs) -> DailyKpi:
        now = datetime.now(_TZ_TAIPEI)
        defaults = dict(
            total_alerts=20,
            auto_resolved=15,
            human_approved=3,
            auto_repair_success=12,
            auto_repair_failed=3,
            km_new_entries=5,
            playbook_count=18,
        )
        defaults.update(kwargs)
        return DailyKpi(
            period_start=now - timedelta(hours=24),
            period_end=now,
            **defaults,
        )

    def test_contains_title(self):
        """報告應包含標題"""
        kpi = self._make_kpi()
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "日度巡檢報告" in report

    def test_contains_alert_stats(self):
        """報告應包含告警統計"""
        kpi = self._make_kpi(total_alerts=20)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "20" in report

    def test_contains_auto_repair_rate(self):
        """報告應包含自動修復成功率"""
        kpi = self._make_kpi(auto_repair_success=8, auto_repair_failed=2)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        # 80.0%
        assert "80.0%" in report

    def test_contains_km_stats(self):
        """報告應包含 KM 統計"""
        kpi = self._make_kpi(km_new_entries=7)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "7" in report

    def test_contains_playbook_count(self):
        """報告應包含 Playbook 數量"""
        kpi = self._make_kpi(playbook_count=18)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "18" in report

    def test_health_excellent_threshold(self):
        """自動修復率 >= 80% → 優秀"""
        kpi = self._make_kpi(auto_repair_success=8, auto_repair_failed=2)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "優秀" in report

    def test_health_good_threshold(self):
        """自動修復率 50-79% → 良好"""
        kpi = self._make_kpi(auto_repair_success=6, auto_repair_failed=4)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "良好" in report

    def test_health_needs_attention(self):
        """自動修復率 < 50% → 需關注"""
        kpi = self._make_kpi(auto_repair_success=3, auto_repair_failed=7)
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "需關注" in report

    def test_category_breakdown_shown(self):
        """有告警分類時應顯示分類分佈"""
        kpi = self._make_kpi(
            alert_category_breakdown={"kubernetes": 5, "host_resource": 3}
        )
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "kubernetes" in report

    def test_contains_taiwan_timezone_note(self):
        """報告應標示台北時間"""
        kpi = self._make_kpi()
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "台北時間" in report

    def test_is_html_formatted(self):
        """報告應包含 HTML 標籤（Telegram HTML 格式）"""
        kpi = self._make_kpi()
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi)
        assert "<b>" in report


# =============================================================================
# format_postmortem
# =============================================================================


class TestFormatPostmortem:
    """測試事後檢討報告格式"""

    def _make_postmortem(self, **kwargs) -> PostmortemData:
        now = datetime.now(_TZ_TAIPEI)
        defaults = dict(
            incident_id="INC-20260414-001",
            title="KubePodOOMKilled on awoooi-api",
            duration_minutes=25.5,
            root_cause="記憶體洩漏導致 OOMKilled",
            resolution_action="kubectl rollout restart deployment/awoooi-api",
            ai_provider="OpenClaw (deepseek-r1:14b)",
            auto_repaired=True,
            retry_count=0,
            created_at=now - timedelta(minutes=25, seconds=30),
            resolved_at=now,
        )
        defaults.update(kwargs)
        return PostmortemData(**defaults)

    def test_contains_incident_id(self):
        """事後檢討應包含 Incident ID"""
        data = self._make_postmortem()
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "INC-20260414-001" in report

    def test_contains_duration(self):
        """事後檢討應包含持續時間"""
        data = self._make_postmortem(duration_minutes=25.5)
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "25.5" in report

    def test_auto_repaired_shown(self):
        """自動修復應顯示標記"""
        data = self._make_postmortem(auto_repaired=True)
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "自動修復" in report

    def test_human_intervene_shown(self):
        """人工介入應顯示標記"""
        data = self._make_postmortem(auto_repaired=False)
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "人工介入" in report

    def test_retry_count_shown(self):
        """重試次數應顯示"""
        data = self._make_postmortem(retry_count=2)
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "重試 2 次" in report

    def test_root_cause_shown(self):
        """根本原因應顯示"""
        data = self._make_postmortem(root_cause="記憶體洩漏導致 OOMKilled")
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "記憶體洩漏" in report

    def test_resolution_action_shown(self):
        """執行動作應顯示在 code 標籤中"""
        data = self._make_postmortem(
            resolution_action="kubectl rollout restart deployment/awoooi-api"
        )
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "kubectl rollout restart" in report
        assert "<code>" in report

    def test_no_root_cause_skips_section(self):
        """無根本原因時不應顯示根本原因區塊"""
        data = self._make_postmortem(root_cause=None)
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "根本原因" not in report

    def test_contains_taiwan_timezone_note(self):
        """事後檢討應標示台北時間"""
        data = self._make_postmortem()
        svc = ReportGenerationService()
        report = svc.format_postmortem(data)
        assert "台北時間" in report


class TestTriggerPostmortemPersistence:
    """Postmortem 產出必須同步沉澱到 KM。"""

    @pytest.mark.asyncio
    async def test_trigger_postmortem_persists_km_before_telegram_send(self, monkeypatch):
        now = datetime.now(_TZ_TAIPEI)
        created = now - timedelta(minutes=16)
        sent_messages: list[str] = []
        created_entries: list[object] = []
        op_logs: list[dict] = []

        class FakeGateway:
            async def send_to_group(self, text: str, parse_mode: str = "HTML") -> None:
                sent_messages.append(text)

        class FakeKnowledgeRepo:
            def __init__(self, _db) -> None:
                pass

            async def create(self, data):
                created_entries.append(data)
                return SimpleNamespace(id="km-postmortem-1")

        class FakeAlertOpRepo:
            async def append(self, event_type: str, **kwargs):
                op_logs.append({"event_type": event_type, **kwargs})

        @asynccontextmanager
        async def fake_db_context():
            yield SimpleNamespace()

        monkeypatch.setattr(
            "src.services.telegram_gateway.get_telegram_gateway",
            lambda: FakeGateway(),
        )
        monkeypatch.setattr("src.db.base.get_db_context", fake_db_context)
        monkeypatch.setattr(
            "src.repositories.knowledge_repository.KnowledgeDBRepository",
            FakeKnowledgeRepo,
        )
        monkeypatch.setattr(
            "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository",
            lambda: FakeAlertOpRepo(),
        )

        await ReportGenerationService().trigger_postmortem(
            incident_id="INC-20260531-POST",
            title="DockerContainerUnhealthy bitan-pharmacy",
            created_at=created,
            resolved_at=now,
            root_cause="容器健康檢查失敗",
            resolution_action="OBSERVE",
            auto_repaired=False,
        )

        assert sent_messages
        assert created_entries
        entry = created_entries[0]
        assert entry.entry_type.value == "postmortem"
        assert entry.related_incident_id == "INC-20260531-POST"
        assert entry.path_type == "postmortem"
        assert op_logs[0]["event_type"] == "KM_CONVERTED"
        assert op_logs[0]["action_detail"] == "postmortem_persisted"


# =============================================================================
# _seconds_until_next_report
# =============================================================================


class TestSecondsUntilNextReport:
    """測試排程計算邏輯"""

    def test_returns_positive_seconds(self):
        """永遠返回正數秒數"""
        seconds = _seconds_until_next_report()
        assert seconds > 0

    def test_returns_at_most_one_day(self):
        """最多等待 24 小時"""
        seconds = _seconds_until_next_report()
        assert seconds <= 86400

    def test_returns_float(self):
        """返回值為 float"""
        seconds = _seconds_until_next_report()
        assert isinstance(seconds, float)


# =============================================================================
# 常數設定
# =============================================================================


class TestServiceConstants:
    """測試服務常數"""

    def test_daily_report_hour(self):
        """日度報告觸發時間應為 08:00 台北時間"""
        assert DAILY_REPORT_HOUR_TAIPEI == 8

    def test_postmortem_min_duration(self):
        """Postmortem 最低觸發時長應為 10 分鐘"""
        assert POSTMORTEM_MIN_DURATION_MINUTES == 10
