"""Performance regression tests for rating recompute.

Usage:
    # Run benchmarks and save baseline:
    pytest tests/test_performance_recompute.py --benchmark-only --benchmark-json=tests/benchmark_baseline.json

    # Run benchmarks and compare against saved baseline:
    pytest tests/test_performance_recompute.py --benchmark-only --benchmark-compare=tests/benchmark_baseline.json

    # Run tests without benchmarking:
    pytest tests/test_performance_recompute.py -m "not slow"

    # Run including benchmarks:
    pytest tests/test_performance_recompute.py -v
"""

import json
import os
import time
from datetime import date, timedelta

import pytest

# Set deterministic env vars for performance testing before any settings are loaded
os.environ.setdefault("ELO_SCALE_C", "400.0")
os.environ.setdefault("ELO_K_BASE", "24.0")
os.environ.setdefault("INITIAL_RATING", "1500.0")
os.environ.setdefault("ENABLE_DRIVER", "false")
os.environ.setdefault("ENABLE_TRAINER", "false")
os.environ.setdefault("ENABLE_ADJUSTMENTS", "false")
os.environ.setdefault("ENABLE_RD", "false")
os.environ.setdefault("TAB_MOCK_MODE", "true")

# Reload settings to pick up env vars
from packages.core.common.settings import reload_settings  # noqa: E402

reload_settings()

from packages.core.ratings.recompute import recompute_ratings  # noqa: E402
from packages.core.storage.repositories import (  # noqa: E402
    HorseRepository,
    MeetingRepository,
    RaceRepository,
    StarterRepository,
)

pytestmark = pytest.mark.slow

BENCHMARK_BASELINE_FILE = os.path.join(
    os.path.dirname(__file__), "benchmark_baseline.json"
)


# ── Baseline helpers ─────────────────────────────────────────────────


def _load_baseline(key: str) -> float | None:
    """Load a benchmark baseline value from JSON file if it exists.

    Reads the pytest-benchmark JSON output format and finds the mean
    execution time for the benchmark with the given test name.
    """
    if not os.path.exists(BENCHMARK_BASELINE_FILE):
        return None
    try:
        with open(BENCHMARK_BASELINE_FILE) as f:
            data = json.load(f)
        benchmarks = data.get("benchmarks", [])
        for b in benchmarks:
            name = b.get("name", "")
            if name == key:
                return b.get("stats", {}).get("mean", None)
    except (json.JSONDecodeError, KeyError, TypeError):
        return None
    return None


def _check_benchmark_baseline(test_name: str, current_mean: float) -> None:
    """Check that the current benchmark result is within 10% of the saved baseline.

    Args:
        test_name: Name of the test function (matches the "name" field in baseline JSON)
        current_mean: Current mean execution time in seconds

    Raises:
        AssertionError: If current run is more than 10% slower than baseline
    """
    baseline = _load_baseline(test_name)
    if baseline is not None and baseline > 0:
        slowdown_ratio = current_mean / baseline
        assert slowdown_ratio < 1.10, (
            f"Performance regression detected for {test_name}: "
            f"{current_mean * 1000:.2f}ms vs baseline {baseline * 1000:.2f}ms "
            f"({(slowdown_ratio - 1) * 100:.1f}% slower, limit is 10%)"
        )


# ── Test data helpers ────────────────────────────────────────────────


def _create_test_meeting(session, meeting_id: str, meeting_date: date, venue: str):
    return MeetingRepository.upsert(
        session,
        {
            "meeting": meeting_id,
            "date": meeting_date.isoformat(),
            "name": venue,
            "category": "H",
        },
    )


def _create_test_race(session, meeting, race_number: int, distance: int = 2000):
    hour = 8 + (race_number % 12)  # Keep hours in valid range (8-19)
    return RaceRepository.upsert(
        session,
        meeting.id,
        {
            "race_number": race_number,
            "distance": distance,
            "start_type": "mobile",
            "gait": "pace",
            "advertised_start_string": f"2025-05-06T{hour:02d}:00:00+12:00",
        },
    )


def _create_test_horses(session, count: int) -> list[int]:
    horse_ids = []
    for i in range(count):
        horse_id = 9000 + i
        HorseRepository.upsert(session, horse_id, f"Horse_{i}")
        horse_ids.append(horse_id)
    return horse_ids


def _create_test_starters(session, race_id: int, horse_ids: list[int], count: int):
    """Create test starters with sequential placings (1 = winner)."""
    import random  # noqa: PLC0415

    selected = random.sample(horse_ids, min(count, len(horse_ids)))
    for i, horse_id in enumerate(selected, 1):
        StarterRepository.upsert(
            session,
            race_id,
            {
                "name": f"Horse_{horse_id - 9000}",
                "horse_id": horse_id,
                "horse_name": f"Horse_{horse_id - 9000}",
                "runner_number": i,
                "barrier": i,
            },
            placing=i,
        )


# ── Fixtures ─────────────────────────────────────────────────────────


@pytest.fixture
def fixture_races_data(db_session):
    """Create 75 races with starters for benchmark.

    Uses the same db_session as the test, so data is visible.
    All changes are rolled back after the test via db_session fixture.
    """
    num_meetings = 5
    races_per_meeting = 15  # Total: 75 races
    horses_per_race = 8
    total_horses = 50  # Reusable pool of 50 horses

    base_date = date(2025, 5, 1)
    venues = ["Auckland", "Christchurch", "Wellington", "Hamilton", "Dunedin"]

    # Create horse pool
    horse_ids = _create_test_horses(db_session, total_horses)
    db_session.flush()

    # Create meetings with races and starters
    for m in range(num_meetings):
        meeting_date = base_date + timedelta(days=m * 7)
        meeting = _create_test_meeting(
            db_session, f"perf_meeting_{m}", meeting_date, venues[m % len(venues)]
        )
        db_session.flush()

        for r in range(races_per_meeting):
            race = _create_test_race(db_session, meeting, r + 1, 2000 + (r * 100))
            db_session.flush()
            _create_test_starters(db_session, race.id, horse_ids, horses_per_race)

    db_session.flush()

    date_from = base_date
    date_to = base_date + timedelta(days=num_meetings * 7)

    yield date_from, date_to

    # No cleanup needed - db_session fixture rolls back


# ── Tests ────────────────────────────────────────────────────────────


def test_recompute_performance(benchmark, db_session, fixture_races_data):
    """Benchmark recompute_ratings on a fixed dataset of ~75 races.

    This test uses pytest-benchmark to measure execution time. The
    benchmark is run multiple times and statistics are collected.

    After the benchmark runs, compares the result against a saved
    baseline if one exists in tests/benchmark_baseline.json.
    """
    date_from, date_to = fixture_races_data

    result = benchmark(recompute_ratings, db_session, date_from, date_to)

    assert result > 0, "recompute_ratings should create rating snapshots"

    # Check against saved baseline if available
    if hasattr(benchmark, "stats") and benchmark.stats is not None:
        mean_val = benchmark.stats.get("mean")
        if mean_val is not None:
            _check_benchmark_baseline("test_recompute_performance", mean_val)


def test_recompute_baseline_threshold(db_session, fixture_races_data):
    """Assert recompute completes within a reasonable time threshold.

    This test runs recompute once and measures wall-clock time.
    It serves as a quick sanity check that the operation stays
    under the 5-second threshold for 50-100 races.
    """
    date_from, date_to = fixture_races_data

    start = time.perf_counter()
    result = recompute_ratings(db_session, date_from, date_to)
    elapsed = time.perf_counter() - start

    assert result > 0, "recompute_ratings should create rating snapshots"
    assert (
        elapsed < 5.0
    ), f"Recompute took {elapsed:.2f}s, expected < 5.0s for 50-100 races"
