"""
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 import weekly_report_service as weekly_report_module
from src.services.report_generation_service import (
    DAILY_REPORT_HOUR_TAIPEI,
    POSTMORTEM_MIN_DURATION_MINUTES,
    DailyKpi,
    PostmortemData,
    ReportGenerationService,
    _seconds_until_next_report,
    _seconds_until_next_monthly_report,
    _seconds_until_next_weekly_report,
)
from src.services.weekly_report_service import WeeklyReportService

_TZ_TAIPEI = timezone(timedelta(hours=8))


# =============================================================================
# WeeklyReportService Git 資料源可信度
# =============================================================================


class TestWeeklyReportGitStats:
    """週報不能把 Git 資料源失敗偽裝成 0 commits / 0 deploys。"""

    def test_git_log_failure_marks_source_failed(self, monkeypatch):
        class Result:
            returncode = 128
            stdout = ""
            stderr = "fatal: not a git repository"

        monkeypatch.setattr(
            weekly_report_module.subprocess,
            "run",
            lambda *args, **kwargs: Result(),
        )

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
        monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (0, 0, False))
        commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 0
        assert deploys == 0
        assert source_ok is False

    def test_git_log_failure_uses_gitea_fallback(self, monkeypatch):
        class Result:
            returncode = 128
            stdout = ""
            stderr = "fatal: not a git repository"

        monkeypatch.setattr(
            weekly_report_module.subprocess,
            "run",
            lambda *args, **kwargs: Result(),
        )

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
        monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (12, 4, True))
        commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 12
        assert deploys == 4
        assert source_ok is True

    def test_git_deploy_log_failure_marks_source_failed_after_commits(self, monkeypatch):
        class CommitResult:
            returncode = 0
            stdout = "abc123 feat: one\nbcd234 fix: two\n"
            stderr = ""

        class DeployResult:
            returncode = 128
            stdout = ""
            stderr = "fatal: bad revision"

        calls = []

        def fake_run(*args, **kwargs):
            calls.append(args[0])
            return CommitResult() if len(calls) == 1 else DeployResult()

        monkeypatch.setattr(weekly_report_module.subprocess, "run", fake_run)

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: True)
        monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (0, 0, False))
        commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 0
        assert deploys == 0
        assert source_ok is False

    def test_missing_local_git_worktree_uses_gitea_without_subprocess(self, monkeypatch):
        def fail_run(*_args, **_kwargs):
            raise AssertionError("subprocess git log should not run without local .git")

        monkeypatch.setattr(weekly_report_module.subprocess, "run", fail_run)

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_has_local_git_worktree", lambda cwd: False)
        monkeypatch.setattr(svc, "_get_gitea_commit_stats", lambda since: (21, 7, True))
        commits, deploys, source_ok = svc._get_git_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 21
        assert deploys == 7
        assert source_ok is True

    def test_gitea_commit_stats_counts_deploy_markers(self, monkeypatch):
        payload = [
            {"commit": {"message": "chore(cd): deploy abc123 [skip ci]\n"}},
            {"commit": {"message": "fix(api): repair source health\n"}},
            {"commit": {"message": "docs(logbook): record deploy verification [skip ci]\n"}},
        ]

        class Response:
            headers = {}

            def __enter__(self):
                return self

            def __exit__(self, *_args):
                return None

            def read(self):
                return weekly_report_module.json.dumps(payload).encode("utf-8")

        def fake_urlopen(request, timeout):
            assert "/api/v1/repos/wooo/awoooi/commits?" in request.full_url
            assert timeout == 1
            assert "Authorization" not in request.headers
            return Response()

        monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_URL", "https://gitea.example.test")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_OWNER", "wooo")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_NAME", "awoooi")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_TOKEN", "")

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        commits, deploys, source_ok = svc._get_gitea_commit_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 3
        assert deploys == 2
        assert source_ok is True

    def test_gitea_commit_stats_prefers_anonymous_read_before_token(self, monkeypatch):
        payload = [{"commit": {"message": "fix(api): one\n"}}]
        seen_authorization = []

        class Response:
            headers = {"X-Total-Count": "1"}

            def __enter__(self):
                return self

            def __exit__(self, *_args):
                return None

            def read(self):
                return weekly_report_module.json.dumps(payload).encode("utf-8")

        def fake_urlopen(request, timeout):
            seen_authorization.append(request.headers.get("Authorization"))
            return Response()

        monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_URL", "https://gitea.example.test")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_OWNER", "wooo")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_NAME", "awoooi")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_TOKEN", "redacted-token")

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_gitea_api_candidates", lambda: ["https://gitea.example.test"])
        commits, deploys, source_ok = svc._get_gitea_commit_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 1
        assert deploys == 0
        assert source_ok is True
        assert seen_authorization == [None]

    def test_gitea_commit_stats_uses_total_header_with_limited_scan(self, monkeypatch):
        payload = [{"commit": {"message": "fix(api): one\n"}} for _ in range(50)]
        calls = []

        class Response:
            headers = {"X-Total-Count": "4804"}

            def __enter__(self):
                return self

            def __exit__(self, *_args):
                return None

            def read(self):
                return weekly_report_module.json.dumps(payload).encode("utf-8")

        def fake_urlopen(request, timeout):
            calls.append(request.full_url)
            return Response()

        monkeypatch.setattr(weekly_report_module, "urlopen", fake_urlopen)
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_URL", "https://gitea.example.test")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_OWNER", "wooo")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_REPO_NAME", "awoooi")
        monkeypatch.setattr(weekly_report_module.settings, "GITEA_API_TOKEN", "")

        svc = WeeklyReportService(stats_service=object(), k3s_monitor=object())
        monkeypatch.setattr(svc, "_gitea_api_candidates", lambda: ["https://gitea.example.test"])
        commits, deploys, source_ok = svc._get_gitea_commit_stats(datetime.now(_TZ_TAIPEI))

        assert commits == 4804
        assert deploys == 0
        assert source_ok is True
        assert len(calls) == 1


# =============================================================================
# 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 = {
            "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

    def test_contains_report_source_health_assets(self):
        """日報應顯示資料源健康與自動化資產沉澱"""
        kpi = self._make_kpi()
        source_health = {
            "rollups": {
                "source_ok_count": 2,
                "source_count": 5,
                "confidence_percent": 40,
            },
            "source_health": [
                {"work_item_id": "report-source-gap:incident_summary"},
                {"work_item_id": "report-source-gap:ai_performance"},
            ],
            "automation_assets": [
                {"label": "KM", "state": "draft_ready", "done_count": 3, "blocked_count": 2},
                {"label": "PlayBook", "state": "draft_required", "done_count": 0, "blocked_count": 2},
                {"label": "腳本", "state": "readback_only", "done_count": 1, "blocked_count": 0},
                {"label": "排程", "state": "no_send_preview", "done_count": 3, "blocked_count": 0},
                {"label": "Verifier", "state": "source_health_ready", "done_count": 1, "blocked_count": 2},
            ],
            "all_zero_assessment": {
                "all_zero_observed": True,
                "verdict": "source_gap_or_no_signal_requires_review",
            },
        }
        svc = ReportGenerationService()
        report = svc.format_daily_report(kpi, source_health)

        assert "報表資料源 / 沉澱" in report
        assert "來源: <code>2/5</code>" in report
        assert "report-source-gap:incident_summary" in report
        assert "KM: draft_ready 3/5" in report
        assert "PlayBook: draft_required 0/2" in report
        assert "腳本: readback_only 1/1" in report
        assert "排程: no_send_preview 3/3" in report
        assert "Verifier: source_health_ready 1/3" in report
        assert "全 0 判讀: source_gap_or_no_signal_requires_review" in report
        assert "不自動改排程" in report

    def test_monthly_report_contains_telegram_gateway_source_health(self):
        """月報應顯示 Telegram Gateway 派送與資產沉澱。"""
        source_health = {
            "rollups": {
                "source_ok_count": 2,
                "source_count": 5,
                "confidence_percent": 40,
                "no_send_preview_count": 3,
            },
            "source_health": [
                {"work_item_id": "report-source-gap:resolution_stats"},
            ],
            "no_send_previews": [
                {
                    "cadence_id": "monthly",
                    "owner_agent": "Hermes",
                    "delivery_state": "no_send_preview",
                    "gap_source_ids": ["resolution_stats", "ai_performance"],
                },
            ],
            "automation_assets": [
                {"label": "KM", "state": "draft_ready", "done_count": 3, "blocked_count": 1},
                {"label": "Verifier", "state": "source_health_ready", "done_count": 1, "blocked_count": 1},
            ],
        }
        svc = ReportGenerationService()
        report = svc.format_monthly_report(
            source_health,
            generated_at=datetime(2026, 6, 18, 10, 0, tzinfo=_TZ_TAIPEI),
        )

        assert "AWOOOI 月報" in report
        assert "Owner: Hermes" in report
        assert "Telegram Gateway" in report
        assert "來源: <code>2/5</code>" in report
        assert "resolution_stats" in report
        assert "KM: draft_ready 3/4" in report
        assert "Verifier: source_health_ready 1/2" in report
        assert "AI Agent 受控接手" in report

    def test_weekly_and_monthly_report_schedule_helpers_return_positive_seconds(self):
        assert _seconds_until_next_report() > 0
        assert _seconds_until_next_weekly_report() > 0
        assert _seconds_until_next_monthly_report() > 0

    def test_sre_digest_preview_contains_assets_and_boundaries(self):
        """SRE 戰情室 digest 應收斂缺口、資產與 no-send 邊界"""
        source_health = {
            "rollups": {
                "source_ok_count": 2,
                "source_count": 5,
                "source_gap_count": 3,
                "confidence_percent": 40,
                "no_send_preview_count": 3,
            },
            "source_health": [
                {"work_item_id": "report-source-gap:incident_summary"},
            ],
            "work_items": [
                {"work_item_id": "report-source-gap:incident_summary"},
                {"work_item_id": "report-source-gap:ai_performance"},
            ],
            "automation_assets": [
                {"label": "KM", "state": "draft_ready", "done_count": 3, "blocked_count": 2},
                {"label": "PlayBook", "state": "draft_required", "done_count": 0, "blocked_count": 2},
                {"label": "Verifier", "state": "source_health_ready", "done_count": 1, "blocked_count": 2},
            ],
        }
        svc = ReportGenerationService()
        report = svc.format_sre_digest_preview(
            source_health,
            generated_at=datetime(2026, 6, 18, 10, 0, tzinfo=_TZ_TAIPEI),
        )

        assert "AwoooI SRE 戰情室 digest no-send preview" in report
        assert "來源: <code>2/5</code>" in report
        assert "report-source-gap:incident_summary" in report
        assert "live Telegram send: 0" in report
        assert "Gateway queue write: 0" in report
        assert "KM: draft_ready 3/5" in report
        assert "PlayBook: draft_required 0/2" in report
        assert "Verifier: source_health_ready 1/3" in report
        assert "不發 Telegram" in report
        assert "不啟動 runtime gate" in report


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


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

    def _make_postmortem(self, **kwargs) -> PostmortemData:
        now = datetime.now(_TZ_TAIPEI)
        defaults = {
            "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
