"""Regression coverage for historical snapshot runner refreshes."""

from __future__ import annotations

import io
import json
import os
import sqlite3
import unittest
from contextlib import nullcontext, redirect_stdout
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import patch

import app.historical_runner as historical_runner_module
from app.config import (
    get_historical_refresh_interval_seconds,
    get_historical_refresh_max_retries,
    get_historical_refresh_retry_delay_seconds,
    get_public_full_refresh_enabled,
    get_public_full_refresh_time,
    get_public_full_refresh_timezone,
    get_public_historical_monthly_refresh_hour_interval,
    get_public_historical_monthly_refresh_minute,
    get_public_historical_weekly_refresh_minute,
    get_public_ranking_monthly_refresh_times,
    get_public_ranking_refresh_interval_seconds,
    get_public_ranking_weekly_refresh_minute,
    get_public_recent_matches_refresh_interval_seconds,
)
from app.payloads import (
    build_historical_server_summary_payload,
    build_leaderboard_snapshot_payload,
    build_recent_historical_matches_payload,
    build_recent_historical_matches_snapshot_payload,
)
from app.historical_runner import (
    _maybe_run_public_read_model_refreshes,
    _json_default,
    _run_refresh_with_retries,
    get_next_public_full_refresh_at,
    run_public_refresh_job_once,
    run_periodic_historical_refresh,
)
from app.historical_snapshots import _normalize_snapshot_limit
from app.historical_storage import list_monthly_mvp_v2_ranking
from app.postgres_display_storage import _json_payload_default
from app.rcon_historical_read_model import (
    _calculate_coverage_hours,
    _calculate_duration_seconds,
)
from app.rcon_historical_leaderboards import _dedupe_snapshot_rows


class HistoricalSnapshotRefreshTests(unittest.TestCase):
    def setUp(self) -> None:
        historical_runner_module._LAST_PUBLIC_FULL_REFRESH_LOCAL_DATE = None
        historical_runner_module._LAST_PUBLIC_RECENT_MATCHES_REFRESH_AT = None
        historical_runner_module._LAST_PUBLIC_RANKING_WEEKLY_REFRESH_SLOT = None
        historical_runner_module._LAST_PUBLIC_RANKING_MONTHLY_REFRESH_SLOT = None
        historical_runner_module._LAST_PUBLIC_HISTORICAL_WEEKLY_REFRESH_SLOT = None
        historical_runner_module._LAST_PUBLIC_HISTORICAL_MONTHLY_REFRESH_SLOT = None
        historical_runner_module._PUBLIC_REFRESH_IN_PROGRESS.clear()

    def test_runner_numeric_env_values_are_parsed_before_use(self) -> None:
        with patch.dict(
            os.environ,
            {
                "HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS": "300",
                "HLL_HISTORICAL_REFRESH_MAX_RETRIES": "4",
                "HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS": "0.5",
            },
            clear=False,
        ):
            self.assertEqual(get_historical_refresh_interval_seconds(), 300)
            self.assertEqual(get_historical_refresh_max_retries(), 4)
            self.assertEqual(get_historical_refresh_retry_delay_seconds(), 0.5)

    def test_runner_numeric_env_values_fail_with_clear_names(self) -> None:
        with patch.dict(
            os.environ,
            {"HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS": "hourly"},
            clear=False,
        ):
            with self.assertRaisesRegex(
                ValueError,
                "HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS must be an integer",
            ):
                get_historical_refresh_interval_seconds()

    def test_public_refresh_env_values_are_parsed_before_use(self) -> None:
        with patch.dict(
            os.environ,
            {
                "HLL_PUBLIC_FULL_REFRESH_ENABLED": "true",
                "HLL_PUBLIC_FULL_REFRESH_TIME": "06:00",
                "HLL_PUBLIC_FULL_REFRESH_TIMEZONE": "Europe/Madrid",
                "HLL_PUBLIC_RANKING_REFRESH_INTERVAL_SECONDS": "900",
                "HLL_PUBLIC_RECENT_MATCHES_REFRESH_INTERVAL_SECONDS": "60",
                "HLL_PUBLIC_RANKING_WEEKLY_REFRESH_MINUTE": "10",
                "HLL_PUBLIC_RANKING_MONTHLY_REFRESH_TIMES": "07:00,19:00",
                "HLL_PUBLIC_HISTORICAL_WEEKLY_REFRESH_MINUTE": "25",
                "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_MINUTE": "40",
                "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_HOUR_INTERVAL": "2",
            },
            clear=False,
        ):
            self.assertTrue(get_public_full_refresh_enabled())
            self.assertEqual(get_public_full_refresh_time(), "06:00")
            self.assertEqual(get_public_full_refresh_timezone(), "Europe/Madrid")
            self.assertEqual(get_public_ranking_refresh_interval_seconds(), 900)
            self.assertEqual(get_public_recent_matches_refresh_interval_seconds(), 60)
            self.assertEqual(get_public_ranking_weekly_refresh_minute(), 10)
            self.assertEqual(get_public_ranking_monthly_refresh_times(), ("07:00", "19:00"))
            self.assertEqual(get_public_historical_weekly_refresh_minute(), 25)
            self.assertEqual(get_public_historical_monthly_refresh_minute(), 40)
            self.assertEqual(get_public_historical_monthly_refresh_hour_interval(), 2)

    def test_next_public_full_refresh_uses_madrid_six_am(self) -> None:
        with patch.dict(
            os.environ,
            {
                "HLL_PUBLIC_FULL_REFRESH_TIME": "06:00",
                "HLL_PUBLIC_FULL_REFRESH_TIMEZONE": "Europe/Madrid",
            },
            clear=False,
        ):
            next_refresh = get_next_public_full_refresh_at(
                now=datetime(2026, 6, 10, 3, 30, tzinfo=timezone.utc),
            )

        self.assertEqual(
            next_refresh,
            datetime(2026, 6, 10, 4, 0, tzinfo=timezone.utc),
        )

    def test_public_scheduler_runs_hourly_weekly_ranking_job(self) -> None:
        historical_runner_module._LAST_PUBLIC_RECENT_MATCHES_REFRESH_AT = datetime(
            2026, 6, 10, 6, 10, tzinfo=timezone.utc
        )
        historical_runner_module._LAST_PUBLIC_RANKING_MONTHLY_REFRESH_SLOT = "2026-06-10T05:00:00Z"
        historical_runner_module._LAST_PUBLIC_HISTORICAL_WEEKLY_REFRESH_SLOT = "2026-06-10T05:25:00Z"
        historical_runner_module._LAST_PUBLIC_HISTORICAL_MONTHLY_REFRESH_SLOT = "2026-06-10T04:40:00Z"
        with (
            patch.dict(
                os.environ,
                {
                    "HLL_PUBLIC_FULL_REFRESH_ENABLED": "false",
                    "HLL_PUBLIC_RANKING_WEEKLY_REFRESH_MINUTE": "10",
                    "HLL_PUBLIC_RANKING_MONTHLY_REFRESH_TIMES": "07:00,19:00",
                    "HLL_PUBLIC_HISTORICAL_WEEKLY_REFRESH_MINUTE": "25",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_MINUTE": "40",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_HOUR_INTERVAL": "2",
                    "HLL_PUBLIC_RECENT_MATCHES_REFRESH_INTERVAL_SECONDS": "3600",
                },
                clear=False,
            ),
            patch(
                "app.historical_runner.refresh_public_weekly_ranking_snapshots",
                return_value={"status": "ok"},
            ) as weekly_ranking_refresh,
        ):
            payload = _maybe_run_public_read_model_refreshes(
                run_number=3,
                now=datetime(2026, 6, 10, 6, 10, tzinfo=timezone.utc),
            )

        self.assertEqual(payload["status"], "ok")
        self.assertIn("ranking_weekly_snapshot_result", payload)
        weekly_ranking_refresh.assert_called_once()

    def test_public_scheduler_runs_bi_hourly_monthly_historical_job(self) -> None:
        historical_runner_module._LAST_PUBLIC_RECENT_MATCHES_REFRESH_AT = datetime(
            2026, 6, 10, 6, 40, tzinfo=timezone.utc
        )
        historical_runner_module._LAST_PUBLIC_RANKING_MONTHLY_REFRESH_SLOT = "2026-06-10T05:00:00Z"
        historical_runner_module._LAST_PUBLIC_RANKING_WEEKLY_REFRESH_SLOT = "2026-06-10T06:10:00Z"
        historical_runner_module._LAST_PUBLIC_HISTORICAL_WEEKLY_REFRESH_SLOT = "2026-06-10T06:25:00Z"
        with (
            patch.dict(
                os.environ,
                {
                    "HLL_PUBLIC_FULL_REFRESH_ENABLED": "false",
                    "HLL_PUBLIC_RANKING_WEEKLY_REFRESH_MINUTE": "10",
                    "HLL_PUBLIC_RANKING_MONTHLY_REFRESH_TIMES": "07:00,19:00",
                    "HLL_PUBLIC_HISTORICAL_WEEKLY_REFRESH_MINUTE": "25",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_MINUTE": "40",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_HOUR_INTERVAL": "2",
                    "HLL_PUBLIC_RECENT_MATCHES_REFRESH_INTERVAL_SECONDS": "3600",
                },
                clear=False,
            ),
            patch(
                "app.historical_runner.refresh_public_monthly_historical_snapshots",
                return_value={"status": "ok"},
            ) as historical_monthly_refresh,
        ):
            payload = _maybe_run_public_read_model_refreshes(
                run_number=4,
                now=datetime(2026, 6, 10, 6, 40, tzinfo=timezone.utc),
            )

        self.assertEqual(payload["status"], "ok")
        self.assertIn("historical_monthly_snapshot_result", payload)
        historical_monthly_refresh.assert_called_once()

    def test_public_scheduler_skips_lower_priority_jobs_when_heavy_job_runs(self) -> None:
        historical_runner_module._LAST_PUBLIC_RECENT_MATCHES_REFRESH_AT = datetime(
            2026, 6, 10, 5, 30, tzinfo=timezone.utc
        )
        with (
            patch.dict(
                os.environ,
                {
                    "HLL_PUBLIC_FULL_REFRESH_ENABLED": "false",
                    "HLL_PUBLIC_RANKING_WEEKLY_REFRESH_MINUTE": "10",
                    "HLL_PUBLIC_RANKING_MONTHLY_REFRESH_TIMES": "07:00,19:00",
                    "HLL_PUBLIC_HISTORICAL_WEEKLY_REFRESH_MINUTE": "25",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_MINUTE": "40",
                    "HLL_PUBLIC_HISTORICAL_MONTHLY_REFRESH_HOUR_INTERVAL": "2",
                    "HLL_PUBLIC_RECENT_MATCHES_REFRESH_INTERVAL_SECONDS": "3600",
                },
                clear=False,
            ),
            patch(
                "app.historical_runner.refresh_public_monthly_ranking_snapshots",
                return_value={"status": "ok"},
            ) as monthly_ranking_refresh,
            patch("app.historical_runner.refresh_public_weekly_ranking_snapshots") as weekly_ranking_refresh,
            patch("app.historical_runner.refresh_public_weekly_historical_snapshots") as weekly_historical_refresh,
        ):
            payload = _maybe_run_public_read_model_refreshes(
                run_number=5,
                now=datetime(2026, 6, 10, 5, 30, tzinfo=timezone.utc),
            )

        self.assertEqual(payload["ranking_monthly_snapshot_result"]["status"], "ok")
        self.assertEqual(payload["ranking_weekly_snapshot_result"]["status"], "skipped")
        self.assertEqual(payload["historical_weekly_snapshot_result"]["status"], "skipped")
        monthly_ranking_refresh.assert_called_once()
        weekly_ranking_refresh.assert_not_called()
        weekly_historical_refresh.assert_not_called()

    def test_manual_public_job_runner_routes_to_supported_job(self) -> None:
        with patch(
            "app.historical_runner.refresh_public_weekly_historical_snapshots",
            return_value={"status": "ok", "job_name": "public-historical-weekly"},
        ) as weekly_historical_refresh:
            result = run_public_refresh_job_once(
                "historical-weekly",
                now=datetime(2026, 6, 10, 6, 25, tzinfo=timezone.utc),
            )

        self.assertEqual(result["job_name"], "public-historical-weekly")
        weekly_historical_refresh.assert_called_once()

    def test_historical_runner_json_default_serializes_datetime_payloads(self) -> None:
        payload = {
            "status": "ok",
            "data": {
                "generated_at": datetime(2026, 6, 11, 8, 0, tzinfo=timezone.utc),
            },
        }

        encoded = json.loads(json.dumps(payload, default=_json_default))

        self.assertEqual(encoded["data"]["generated_at"], "2026-06-11T08:00:00Z")

    def test_ranking_snapshot_dedupes_duplicate_player_rows(self) -> None:
        rows = [
            {"player_id": "player-1", "player_name": "Alpha", "metric_value": 20},
            {"player_id": "player-1", "player_name": "Alpha duplicate", "metric_value": 19},
            {"player_id": "player-2", "player_name": "Bravo", "metric_value": 18},
        ]

        deduplicated_rows = _dedupe_snapshot_rows(rows)

        self.assertEqual([row["player_id"] for row in deduplicated_rows], ["player-1", "player-2"])
        self.assertEqual(deduplicated_rows[0]["player_name"], "Alpha")

    def test_monthly_mvp_v2_missing_player_event_ledger_returns_empty_payload(self) -> None:
        monthly_window = {
            "window_start": datetime(2026, 6, 1, tzinfo=timezone.utc),
            "window_end": datetime(2026, 6, 11, tzinfo=timezone.utc),
            "window_kind": "current-month",
            "window_label": "Mes activo",
            "uses_fallback": False,
            "selection_reason": "test",
            "current_month_closed_matches": 4,
            "previous_month_closed_matches": 8,
            "minimum_closed_matches": 3,
            "current_month_has_sufficient_sample": True,
            "is_early_month": False,
        }
        with (
            patch("app.historical_storage.initialize_historical_storage", return_value=Path("dummy.sqlite3")),
            patch("app.historical_storage._select_monthly_window", return_value=monthly_window),
            patch(
                "app.historical_storage._get_monthly_player_event_coverage",
                side_effect=sqlite3.OperationalError("no such table: player_event_raw_ledger"),
            ),
        ):
            payload = list_monthly_mvp_v2_ranking(server_id="all-servers", limit=10)

        self.assertFalse(payload["event_coverage"]["ready"])
        self.assertEqual(payload["event_coverage"]["reason"], "player-event-raw-ledger-missing")
        self.assertEqual(payload["items"], [])

    def test_historical_leaderboard_snapshot_does_not_runtime_enrich_public_request(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T00:00:00Z",
            "source_range_end": "2026-06-10T00:00:00Z",
            "is_stale": False,
            "payload": {
                "items": [
                    {
                        "ranking_position": 1,
                        "player": {"name": "Player One"},
                        "metric_value": 12,
                        "matches_considered": 1,
                        "kills": 12,
                    }
                ],
                "window_start": "2026-06-09T00:00:00Z",
                "window_end": "2026-06-10T00:00:00Z",
                "limit": 10,
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads._load_runtime_leaderboard_items") as runtime_loader,
        ):
            payload = build_leaderboard_snapshot_payload(
                server_id="all-servers",
                timeframe="weekly",
                metric="kills",
                limit=10,
            )

        runtime_loader.assert_not_called()
        self.assertEqual(payload["data"]["items"][0]["metric_value"], 12)
        self.assertFalse(payload["data"]["runtime_enrichment"]["applied"])

    def test_recent_matches_snapshot_does_not_complete_from_public_scoreboard(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T21:00:00Z",
            "source_range_end": "2026-06-09T22:00:00Z",
            "is_stale": False,
            "payload": {
                "items": [
                    {
                        "match_id": "match-1",
                        "closed_at": "2026-06-09T22:00:00Z",
                    }
                ],
                "limit": 100,
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.list_recent_historical_matches") as fallback_loader,
        ):
            payload = build_recent_historical_matches_snapshot_payload(
                server_slug="all-servers",
                limit=100,
            )

        fallback_loader.assert_not_called()
        self.assertEqual(len(payload["data"]["items"]), 1)
        self.assertFalse(payload["data"].get("fallback_used", False))

    def test_legacy_recent_matches_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T21:00:00Z",
            "source_range_end": "2026-06-09T22:00:00Z",
            "is_stale": False,
            "payload": {
                "items": [
                    {
                        "match_id": "match-1",
                        "closed_at": "2026-06-09T22:00:00Z",
                    }
                ],
                "limit": 100,
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_recent_historical_matches") as fallback_loader,
        ):
            payload = build_recent_historical_matches_payload(
                server_slug="comunidad-hispana-01",
                limit=20,
            )

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["context"], "historical-recent-matches")
        self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path")
        self.assertEqual(payload["data"]["server_slug"], "comunidad-hispana-01")
        self.assertEqual(payload["data"]["items"][0]["match_id"], "match-1")

    def test_legacy_second_server_recent_matches_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T21:00:00Z",
            "source_range_end": "2026-06-09T22:00:00Z",
            "is_stale": False,
            "payload": {
                "items": [
                    {
                        "match_id": "match-2",
                        "closed_at": "2026-06-09T22:00:00Z",
                    }
                ],
                "limit": 100,
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_recent_historical_matches") as fallback_loader,
        ):
            payload = build_recent_historical_matches_payload(
                server_slug="comunidad-hispana-02",
                limit=20,
            )

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["server_slug"], "comunidad-hispana-02")
        self.assertEqual(payload["data"]["items"][0]["match_id"], "match-2")

    def test_legacy_all_servers_recent_matches_still_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T21:00:00Z",
            "source_range_end": "2026-06-09T22:00:00Z",
            "is_stale": False,
            "payload": {
                "items": [
                    {
                        "match_id": "match-all",
                        "closed_at": "2026-06-09T22:00:00Z",
                    }
                ],
                "limit": 100,
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_recent_historical_matches") as fallback_loader,
        ):
            payload = build_recent_historical_matches_payload(
                server_slug="all-servers",
                limit=20,
            )

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["server_slug"], "all-servers")
        self.assertEqual(payload["data"]["items"][0]["match_id"], "match-all")

    def test_legacy_server_summary_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T00:00:00Z",
            "source_range_end": "2026-06-10T00:00:00Z",
            "is_stale": False,
            "payload": {
                "item": {
                    "server": {"slug": "comunidad-hispana-01", "name": "Comunidad Hispana #01"},
                    "matches_count": 12,
                },
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_historical_server_summaries") as fallback_loader,
        ):
            payload = build_historical_server_summary_payload(server_slug="comunidad-hispana-01")

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["context"], "historical-server-summary")
        self.assertEqual(payload["data"]["legacy_endpoint_policy"], "snapshot-read-only-fast-path")
        self.assertEqual(payload["data"]["server_slug"], "comunidad-hispana-01")
        self.assertEqual(payload["data"]["items"][0]["matches_count"], 12)

    def test_legacy_second_server_summary_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T00:00:00Z",
            "source_range_end": "2026-06-10T00:00:00Z",
            "is_stale": False,
            "payload": {
                "item": {
                    "server": {"slug": "comunidad-hispana-02", "name": "Comunidad Hispana #02"},
                    "matches_count": 8,
                },
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_historical_server_summaries") as fallback_loader,
        ):
            payload = build_historical_server_summary_payload(server_slug="comunidad-hispana-02")

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["server_slug"], "comunidad-hispana-02")
        self.assertEqual(payload["data"]["items"][0]["matches_count"], 8)

    def test_legacy_all_servers_summary_still_uses_snapshot_fast_path(self) -> None:
        snapshot = {
            "generated_at": "2026-06-10T04:00:00Z",
            "source_range_start": "2026-06-09T00:00:00Z",
            "source_range_end": "2026-06-10T00:00:00Z",
            "is_stale": False,
            "payload": {
                "item": {
                    "server": {"slug": "all-servers", "name": "Todos los servidores"},
                    "matches_count": 20,
                },
            },
        }
        with (
            patch("app.payloads._get_historical_snapshot_record", return_value=snapshot),
            patch("app.payloads.get_historical_data_source_kind", return_value="rcon"),
            patch("app.payloads.get_rcon_historical_read_model") as rcon_loader,
            patch("app.payloads.list_historical_server_summaries") as fallback_loader,
        ):
            payload = build_historical_server_summary_payload(server_slug="all-servers")

        rcon_loader.assert_not_called()
        fallback_loader.assert_not_called()
        self.assertEqual(payload["data"]["server_slug"], "all-servers")
        self.assertEqual(payload["data"]["items"][0]["matches_count"], 20)

    def test_rcon_coverage_accepts_postgres_datetime_values(self) -> None:
        start = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc)
        end = datetime(2026, 5, 21, 11, 30, tzinfo=timezone.utc)

        self.assertEqual(_calculate_coverage_hours(start, end), 1.5)
        self.assertEqual(_calculate_duration_seconds(start, end), 5400)

    def test_snapshot_limits_are_numeric_before_snapshot_queries(self) -> None:
        self.assertEqual(_normalize_snapshot_limit("recent_matches_limit", "10"), 10)
        with self.assertRaisesRegex(ValueError, "recent_matches_limit"):
            _normalize_snapshot_limit("recent_matches_limit", "ten")

    def test_postgres_snapshot_payload_serializes_datetime_values(self) -> None:
        payload = {
            "captured_at": datetime(2026, 5, 21, 20, 12, 54, tzinfo=timezone.utc),
        }

        self.assertEqual(
            json.loads(json.dumps(payload, default=_json_payload_default)),
            {"captured_at": "2026-05-21T20:12:54Z"},
        )

    def test_runner_failure_log_includes_exception_type_and_traceback(self) -> None:
        stream = io.StringIO()
        with (
            patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
            patch(
                "app.historical_runner._run_primary_rcon_capture",
                side_effect=TypeError("bad timestamp"),
            ),
            redirect_stdout(stream),
        ):
            result = _run_refresh_with_retries(
                max_retries=0,
                retry_delay_seconds=0,
                server_slug=None,
                max_pages=None,
                page_size=None,
                run_number=1,
            )

        self.assertEqual(result["status"], "error")
        self.assertEqual(result["error_type"], "TypeError")
        self.assertIn("Traceback", result["traceback"])
        self.assertIn('"event": "historical-refresh-attempt-failed"', stream.getvalue())

    def test_runner_success_log_serializes_datetime_values(self) -> None:
        stream = io.StringIO()
        with (
            patch(
                "app.historical_runner._run_refresh_with_retries",
                return_value={
                    "status": "ok",
                    "rcon_capture_result": {
                        "captured_at": datetime(2026, 5, 22, tzinfo=timezone.utc),
                    },
                },
            ),
            redirect_stdout(stream),
        ):
            run_periodic_historical_refresh(
                interval_seconds=1,
                max_retries=0,
                retry_delay_seconds=0,
                max_runs=1,
            )

        self.assertIn('"status": "ok"', stream.getvalue())
        self.assertIn('"captured_at": "2026-05-22 00:00:00+00:00"', stream.getvalue())

    def test_runner_continues_when_legacy_snapshot_refresh_fails(self) -> None:
        with (
            patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
            patch("app.historical_runner._run_primary_rcon_capture", return_value={"status": "ok", "targets": []}),
            patch(
                "app.historical_runner._resolve_classic_fallback_policy",
                return_value=(False, "validation-rcon-primary-cycle"),
            ),
            patch("app.historical_runner._rcon_capture_has_new_useful_data", return_value=True),
            patch(
                "app.historical_runner.generate_historical_snapshots",
                side_effect=RuntimeError("legacy snapshot failure"),
            ),
            patch(
                "app.historical_runner._build_elo_mmr_rebuild_policy",
                return_value={
                    "due": False,
                    "policy": "validation-policy",
                    "last_generated_at": None,
                    "samples_since_last_rebuild": 1,
                    "minutes_since_last_rebuild": None,
                    "rebuild_interval_minutes": 60,
                    "min_new_samples": 10,
                },
            ),
            patch("app.historical_runner.refresh_player_search_index", return_value={"status": "ok"}) as search_refresh,
            patch("app.historical_runner.refresh_player_period_stats", return_value={"status": "ok"}) as period_refresh,
            patch("app.historical_runner.refresh_ranking_snapshots", return_value={"status": "ok"}) as ranking_refresh,
            patch(
                "app.historical_runner._maybe_run_database_maintenance",
                return_value={"status": "skipped", "reason": "disabled"},
            ),
        ):
            result = _run_refresh_with_retries(
                max_retries=0,
                retry_delay_seconds=0,
                server_slug=None,
                max_pages=None,
                page_size=None,
                run_number=1,
            )

        self.assertEqual(result["status"], "partial")
        self.assertEqual(result["historical_snapshot_result"]["status"], "error")
        self.assertEqual(result["snapshot_result"]["status"], "error")
        self.assertEqual(
            result["historical_snapshot_result"]["error"],
            "legacy snapshot failure",
        )
        search_refresh.assert_called_once()
        period_refresh.assert_called_once()
        ranking_refresh.assert_called_once()

    def test_runner_returns_ok_when_legacy_snapshot_and_read_models_succeed(self) -> None:
        with (
            patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
            patch("app.historical_runner._run_primary_rcon_capture", return_value={"status": "ok", "targets": []}),
            patch(
                "app.historical_runner._resolve_classic_fallback_policy",
                return_value=(False, "validation-rcon-primary-cycle"),
            ),
            patch("app.historical_runner._rcon_capture_has_new_useful_data", return_value=True),
            patch(
                "app.historical_runner.generate_historical_snapshots",
                return_value={"status": "ok", "generated_at": "2026-06-09T08:00:00Z"},
            ),
            patch(
                "app.historical_runner._build_elo_mmr_rebuild_policy",
                return_value={
                    "due": False,
                    "policy": "validation-policy",
                    "last_generated_at": None,
                    "samples_since_last_rebuild": 1,
                    "minutes_since_last_rebuild": None,
                    "rebuild_interval_minutes": 60,
                    "min_new_samples": 10,
                },
            ),
            patch("app.historical_runner.refresh_player_search_index", return_value={"status": "ok"}),
            patch("app.historical_runner.refresh_player_period_stats", return_value={"status": "ok"}),
            patch("app.historical_runner.refresh_ranking_snapshots", return_value={"status": "ok"}),
            patch(
                "app.historical_runner._maybe_run_database_maintenance",
                return_value={"status": "skipped", "reason": "disabled"},
            ),
        ):
            result = _run_refresh_with_retries(
                max_retries=0,
                retry_delay_seconds=0,
                server_slug=None,
                max_pages=None,
                page_size=None,
                run_number=1,
            )

        self.assertEqual(result["status"], "ok")
        self.assertEqual(result["historical_snapshot_result"]["status"], "ok")
        self.assertIn("player_search_index_result", result)
        self.assertIn("player_period_stats_result", result)
        self.assertIn("ranking_snapshot_result", result)


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