"""Tests for running data processing functions."""

import pandas as pd
import numpy as np
from datetime import datetime

from kaiserlift.running_processers import (
    calculate_pace_from_duration,
    seconds_to_pace_string,
    highest_pace_per_distance,
    highest_pace_per_distance_pace_pareto,
    estimate_pace_at_distance,
    riegel_pace_exponent,
    df_next_running_targets,
    add_speed_metric_column,
    predict_race_pace,
)


def test_calculate_pace_from_duration():
    """Test pace calculation from duration and distance."""
    # 27.0 minutes for 3.0 miles = 9:00 per mile = 540 seconds/mile
    assert calculate_pace_from_duration(27.0, 3.0) == 540.0
    # 47.5 minutes for 5.0 miles = 9:30 per mile = 570 seconds/mile
    assert calculate_pace_from_duration(47.5, 5.0) == 570.0
    # 60.0 minutes for 6.0 miles = 10:00 per mile = 600 seconds/mile
    assert calculate_pace_from_duration(60.0, 6.0) == 600.0
    # Edge cases
    assert np.isnan(calculate_pace_from_duration(np.nan, 5.0))
    assert np.isnan(calculate_pace_from_duration(30.0, np.nan))
    assert np.isnan(calculate_pace_from_duration(30.0, 0))  # Zero distance


def test_seconds_to_pace_string():
    """Test pace string formatting."""
    assert seconds_to_pace_string(570) == "9:30"
    assert seconds_to_pace_string(525) == "8:45"
    assert seconds_to_pace_string(600) == "10:00"
    assert seconds_to_pace_string(435) == "7:15"
    assert np.isnan(seconds_to_pace_string(0))
    assert np.isnan(seconds_to_pace_string(-10))
    assert np.isnan(seconds_to_pace_string(np.nan))


def test_highest_pace_per_distance():
    """Test Pareto front extraction for running data."""
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, 15)] * 4,
            "Exercise": ["Running"] * 4,
            "Category": ["Cardio"] * 4,
            "Distance": [5.0, 5.0, 10.0, 3.0],
            "Pace": [600, 570, 600, 540],  # 10:00, 9:30, 10:00, 9:00
        }
    )

    result = highest_pace_per_distance(df)

    # All three distances are on the Pareto front:
    # 3.0 @ 540 (9:00) - fastest pace
    # 5.0 @ 570 (9:30) - middle distance
    # 10.0 @ 600 (10:00) - longest distance
    # None is dominated because no record has both longer distance AND faster pace
    assert len(result) == 3
    assert result.loc[result["Distance"] == 3.0, "Pace"].values[0] == 540
    assert result.loc[result["Distance"] == 5.0, "Pace"].values[0] == 570
    assert result.loc[result["Distance"] == 10.0, "Pace"].values[0] == 600


def test_highest_pace_per_distance_pareto_dominance():
    """Test that Pareto dominance is correctly applied."""
    # Create a scenario where we have clear dominance
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, i) for i in range(1, 6)],
            "Exercise": ["Running"] * 5,
            "Category": ["Cardio"] * 5,
            "Distance": [3.0, 5.0, 7.0, 10.0, 13.1],
            "Pace": [540, 570, 600, 630, 660],  # Pace getting slower with distance
        }
    )

    result = highest_pace_per_distance(df)

    # All should be on the Pareto front (longer distance, slower pace is OK)
    assert len(result) == 5


def test_highest_pace_per_distance_dominated():
    """Test that dominated records are removed using time-based dominance."""
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, i) for i in range(1, 4)],
            "Exercise": ["Running"] * 3,
            "Category": ["Cardio"] * 3,
            "Distance": [5.0, 10.0, 5.0],
            "Pace": [600, 570, 630],  # 10:00, 9:30, 10:30
        }
    )

    result = highest_pace_per_distance(df)

    # In time-based Pareto (distance vs total time):
    #   best 5-mile: pace=600 → total time = 5.0 * 600 = 3000 s (50 min)
    #   10-mile:     pace=570 → total time = 10.0 * 570 = 5700 s (95 min)
    # The 10-mile run takes more total time, so it does NOT dominate the 5-mile.
    # Both records remain on the Pareto front.
    assert len(result) == 2
    distances = sorted(result["Distance"].tolist())
    assert distances == [5.0, 10.0]
    assert result.loc[result["Distance"] == 5.0, "Pace"].values[0] == 600
    assert result.loc[result["Distance"] == 10.0, "Pace"].values[0] == 570


def test_highest_pace_per_distance_time_dominance():
    """Test that a record is only dominated when total time is also better."""
    # 5 miles in 50 min (pace=600) vs 10 miles in 45 min (pace=270).
    # The 10-mile run is both farther AND faster in total time, so it dominates.
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, 1), datetime(2024, 1, 2)],
            "Exercise": ["Running"] * 2,
            "Category": ["Cardio"] * 2,
            "Distance": [5.0, 10.0],
            "Pace": [600, 270],  # 5*600=3000s (50min) vs 10*270=2700s (45min)
        }
    )

    result = highest_pace_per_distance(df)

    # 10-mile total time (2700s) < 5-mile total time (3000s), so 5-mile is dominated.
    assert len(result) == 1
    assert result["Distance"].values[0] == 10.0


def test_riegel_pace_exponent():
    """Test that the variable exponent grows with distance."""
    # Standard Riegel holds up to half marathon
    assert riegel_pace_exponent(5.0) == 0.06
    assert riegel_pace_exponent(13.1) == 0.06

    # Exponent grows beyond half marathon
    assert riegel_pace_exponent(26.2) > 0.06  # marathon
    assert riegel_pace_exponent(50.0) > riegel_pace_exponent(26.2)
    assert riegel_pace_exponent(100.0) > riegel_pace_exponent(50.0)

    # Caps beyond 100 miles
    assert riegel_pace_exponent(150.0) == riegel_pace_exponent(100.0)

    # Monotonically non-decreasing over the whole range
    distances = [1, 5, 10, 13.1, 20, 26.2, 35, 50, 75, 100, 150]
    exponents = [riegel_pace_exponent(d) for d in distances]
    for i in range(len(exponents) - 1):
        assert exponents[i] <= exponents[i + 1]


def test_estimate_pace_at_distance():
    """Test pace estimation at different distances."""
    # Best pace: 9:30 (570 sec/mi) at 5 miles
    # Estimate at 10 miles (2x distance, target ≤ 13.1 mi → exponent still 0.06)
    estimated = estimate_pace_at_distance(570, 5.0, 10.0)
    expected = 570 * (10.0 / 5.0) ** 0.06
    assert abs(estimated - expected) < 0.1

    # Same distance should return same pace
    estimated = estimate_pace_at_distance(570, 5.0, 5.0)
    assert estimated == 570

    # Shorter distance should be faster
    estimated = estimate_pace_at_distance(570, 5.0, 2.5)
    assert estimated < 570

    # Ultra distance (50 mi) should predict a slower pace than standard 0.06 would
    est_50 = estimate_pace_at_distance(440, 10.0, 50.0)
    standard_50 = 440 * (50.0 / 10.0) ** 0.06
    assert est_50 > standard_50, (
        "50-mile prediction should be slower than standard Riegel"
    )


def test_estimate_pace_at_distance_edge_cases():
    """Test edge cases for pace estimation."""
    # Zero distance
    assert np.isnan(estimate_pace_at_distance(570, 0, 5.0))
    assert np.isnan(estimate_pace_at_distance(570, 5.0, 0))

    # NaN pace
    assert np.isnan(estimate_pace_at_distance(np.nan, 5.0, 10.0))


def test_add_speed_metric_column():
    """Test speed calculation from pace."""
    df = pd.DataFrame(
        {
            "Distance": [5.0, 10.0],
            "Pace": [600, 570],  # 10:00 and 9:30 pace
        }
    )

    result = add_speed_metric_column(df)

    # Speed (mph) = 3600 / pace (sec/mi)
    # 10:00 pace = 600 sec/mi = 6.0 mph
    # 9:30 pace = 570 sec/mi = 6.316 mph
    assert "Speed" in result.columns
    assert abs(result.loc[0, "Speed"] - 6.0) < 0.01
    assert abs(result.loc[1, "Speed"] - 6.316) < 0.01


def test_df_next_running_targets():
    """Test target generation from Pareto front."""
    df_records = pd.DataFrame(
        {
            "Exercise": ["Running"] * 2,
            "Distance": [5.0, 10.0],
            "Pace": [570, 600],  # 9:30 and 10:00
        }
    )

    targets = df_next_running_targets(df_records)

    assert len(targets) == 3
    assert "Speed" in targets.columns

    sorted_targets = targets.sort_values("Distance").reset_index(drop=True)

    left_target = sorted_targets.iloc[0]
    middle_target = sorted_targets.iloc[1]
    right_target = sorted_targets.iloc[2]

    # Left edge target should be at half the leftmost pareto distance (5.0/2=2.5)
    assert left_target["Distance"] == 2.5
    assert left_target["Speed"] > 3600 / 570

    # Target should sit between the two Pareto distances (10% toward the right)
    assert abs(middle_target["Distance"] - 5.5) < 1e-6

    # Middle target speed should be faster than the longer-distance point but below the shorter
    target_speed = middle_target["Speed"]
    assert 6.0 < target_speed < 6.29

    # Right edge target should sit just beyond the longest Pareto distance at a slower speed
    assert abs(right_target["Distance"] - 10.5) < 1e-6
    assert right_target["Speed"] < 6.0


def test_df_next_running_targets_gap_filling():
    """Test that gap fillers are created for large distance gaps."""
    df_records = pd.DataFrame(
        {
            "Exercise": ["Running"] * 2,
            "Distance": [3.0, 10.0],  # Large gap
            "Pace": [540, 600],
        }
    )

    targets = df_next_running_targets(df_records)

    # Targets should be positioned between and around the Pareto points
    gap_fillers = targets[(targets["Distance"] > 3.0) & (targets["Distance"] < 10.0)]
    assert len(gap_fillers) == 1
    filler = gap_fillers.sort_values("Distance").iloc[0]
    assert 3.6 < filler["Distance"] < 3.8
    assert 6.0 < filler["Speed"] < 6.7

    edge_targets = targets[(targets["Distance"] <= 3.0) | (targets["Distance"] >= 10.0)]
    assert len(edge_targets) == 2


def test_df_next_running_targets_empty():
    """Test target generation with empty input."""
    df_records = pd.DataFrame(
        {
            "Exercise": [],
            "Distance": [],
            "Pace": [],
        }
    )

    targets = df_next_running_targets(df_records)
    assert len(targets) == 0


def test_predict_race_pace():
    """Test race pace prediction."""
    df_records = pd.DataFrame(
        {
            "Exercise": ["Running"] * 3,
            "Distance": [3.0, 5.0, 10.0],
            "Pace": [540, 570, 600],  # 9:00, 9:30, 10:00
        }
    )

    # Predict 5K (3.1 miles) pace
    prediction = predict_race_pace(df_records, "Running", 3.1)

    assert "optimistic_pace" in prediction
    assert "conservative_pace" in prediction
    assert "optimistic_time" in prediction
    assert "conservative_time" in prediction

    # Optimistic should be based on interpolation from 3.0 mile PR
    assert not np.isnan(prediction["optimistic_pace"])
    assert not np.isnan(prediction["conservative_pace"])

    # Conservative should be slower than optimistic
    assert prediction["conservative_pace"] >= prediction["optimistic_pace"]


def test_predict_race_pace_no_data():
    """Test race pace prediction with no matching exercise."""
    df_records = pd.DataFrame(
        {
            "Exercise": ["Running"],
            "Distance": [5.0],
            "Pace": [570],
        }
    )

    prediction = predict_race_pace(df_records, "NonExistent", 5.0)

    assert np.isnan(prediction["optimistic_pace"])
    assert np.isnan(prediction["conservative_pace"])
    assert prediction["optimistic_time"] == "N/A"
    assert prediction["conservative_time"] == "N/A"


def test_multiple_exercises():
    """Test handling of multiple exercise types."""
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, 1)] * 4,
            "Exercise": ["Running", "Running", "Cycling", "Cycling"],
            "Category": ["Cardio"] * 4,
            "Distance": [5.0, 10.0, 10.0, 20.0],
            "Pace": [570, 600, 300, 330],
        }
    )

    result = highest_pace_per_distance(df)

    # Should have records for both exercises
    assert "Running" in result["Exercise"].values
    assert "Cycling" in result["Exercise"].values

    # Each exercise should have its own Pareto front
    running_records = result[result["Exercise"] == "Running"]
    cycling_records = result[result["Exercise"] == "Cycling"]

    assert len(running_records) == 2
    assert len(cycling_records) == 2


def test_highest_pace_per_distance_pace_pareto_basic():
    """Test pace-distance Pareto keeps all non-dominated pace records."""
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, i) for i in range(1, 4)],
            "Exercise": ["Running"] * 3,
            "Category": ["Cardio"] * 3,
            "Distance": [3.0, 5.0, 10.0],
            "Pace": [540, 570, 600],  # 9:00, 9:30, 10:00
        }
    )

    result = highest_pace_per_distance_pace_pareto(df)

    # All three survive: no record has both longer distance AND faster pace.
    assert len(result) == 3
    assert sorted(result["Distance"].tolist()) == [3.0, 5.0, 10.0]


def test_highest_pace_per_distance_pace_pareto_dominated():
    """Test that a record dominated in pace-distance space is removed."""
    # 5mi @ 9:00 dominates 5mi @ 9:30 (same distance, faster pace).
    # But since groupby already picks the best pace per distance, this
    # tests cross-distance dominance: 10mi @ 8:30 dominates 5mi @ 9:00
    # because 10 >= 5 and 510 <= 540.
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, i) for i in range(1, 3)],
            "Exercise": ["Running"] * 2,
            "Category": ["Cardio"] * 2,
            "Distance": [5.0, 10.0],
            "Pace": [540, 510],  # 9:00, 8:30 — longer AND faster
        }
    )

    result = highest_pace_per_distance_pace_pareto(df)

    # 5mi @ 9:00 is dominated by 10mi @ 8:30 (farther and faster pace).
    assert len(result) == 1
    assert result["Distance"].values[0] == 10.0
    assert result["Pace"].values[0] == 510


def test_highest_pace_per_distance_pace_pareto_time_pareto_differs():
    """Pace Pareto and time Pareto can yield different fronts."""
    # 5mi @ 10:00 (total 50 min) vs 10mi @ 9:30 (total 95 min).
    # Time Pareto: 10mi takes MORE total time, so 5mi survives.
    # Pace Pareto: 10mi has faster pace (570 < 600), so 5mi IS dominated.
    df = pd.DataFrame(
        {
            "Date": [datetime(2024, 1, i) for i in range(1, 3)],
            "Exercise": ["Running"] * 2,
            "Category": ["Cardio"] * 2,
            "Distance": [5.0, 10.0],
            "Pace": [600, 570],  # 10:00, 9:30
        }
    )

    time_result = highest_pace_per_distance(df)
    pace_result = highest_pace_per_distance_pace_pareto(df)

    # Time Pareto: both survive (10mi takes more total time)
    assert len(time_result) == 2

    # Pace Pareto: 5mi is dominated (10mi is farther AND faster pace)
    assert len(pace_result) == 1
    assert pace_result["Distance"].values[0] == 10.0


def test_running_targets_multiple_exercises():
    """Test target generation for multiple exercises."""
    df_records = pd.DataFrame(
        {
            "Exercise": ["Running", "Running", "Cycling", "Cycling"],
            "Distance": [5.0, 10.0, 10.0, 20.0],
            "Pace": [570, 600, 300, 330],
        }
    )

    targets = df_next_running_targets(df_records)

    # Should have targets for both exercises
    assert "Running" in targets["Exercise"].values
    assert "Cycling" in targets["Exercise"].values
