"""Tests for temporal decay functions."""

import time

import pytest

from mnemex.config import Config, set_config
from mnemex.core.decay import (
    calculate_decay_lambda,
    calculate_halflife,
    calculate_score,
    project_score_at_time,
    time_until_threshold,
)


def test_calculate_score_basic():
    """Test basic score calculation."""
    now = int(time.time())

    # Fresh memory with use_count=1 should have non-zero score
    score = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )
    assert score > 0
    assert score == pytest.approx(1.0, rel=0.01)  # Should be close to 1 when just accessed


def test_calculate_score_decay():
    """Test that score decays over time."""
    now = int(time.time())
    one_day_ago = now - 86400

    score_fresh = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    score_old = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    assert score_old < score_fresh  # Older memory has lower score


def test_calculate_score_use_count():
    """Test that higher use count increases score."""
    now = int(time.time())

    score_low = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    score_high = calculate_score(
        use_count=10,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    assert score_high > score_low  # Higher use count = higher score


def test_calculate_decay_lambda():
    """Test decay lambda calculation from half-life."""
    # 3-day half-life
    lambda_3d = calculate_decay_lambda(3.0)
    assert lambda_3d == pytest.approx(2.673e-6, rel=0.01)

    # 7-day half-life
    lambda_7d = calculate_decay_lambda(7.0)
    assert lambda_7d < lambda_3d  # Longer half-life = slower decay


def test_calculate_halflife():
    """Test half-life calculation from lambda."""
    lambda_val = 2.673e-6
    halflife = calculate_halflife(lambda_val)
    assert halflife == pytest.approx(3.0, rel=0.01)  # Should be 3 days


def test_time_until_threshold():
    """Test calculation of time until score drops below threshold."""
    now = int(time.time())

    # Memory with current score of 1.0
    remaining = time_until_threshold(
        current_score=1.0,
        threshold=0.5,  # Half the score
        last_used=now,
        lambda_=calculate_decay_lambda(3.0),  # 3-day half-life
    )

    assert remaining is not None
    # Should be approximately 3 days (259200 seconds)
    assert remaining == pytest.approx(259200, rel=0.1)


def test_time_until_threshold_already_below():
    """Test time_until_threshold when already below threshold."""
    now = int(time.time())

    remaining = time_until_threshold(
        current_score=0.3,
        threshold=0.5,
        last_used=now,
        lambda_=2.673e-6,
    )

    assert remaining is None  # Already below threshold


def test_project_score_at_time():
    """Test score projection to future time."""
    now = int(time.time())
    future = now + 86400  # 1 day from now

    projected = project_score_at_time(
        use_count=5,
        last_used=now,
        strength=1.0,
        target_time=future,
        lambda_=2.673e-6,
        beta=0.6,
    )

    current = calculate_score(
        use_count=5,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    assert projected < current  # Future score should be lower due to decay


def test_calculate_score_default_now():
    """Test calculate_score with default now parameter (should use current time)."""
    current_time = int(time.time())

    # Call without now parameter - should use current time
    score = calculate_score(
        use_count=1,
        last_used=current_time,
        strength=1.0,
        lambda_=2.673e-6,
        beta=0.6,
    )

    # Score should be close to 1.0 since last_used is current time
    assert score > 0
    assert score == pytest.approx(1.0, rel=0.01)


def test_calculate_score_default_beta():
    """Test calculate_score with default beta parameter (should use config default)."""
    config = Config()
    set_config(config)

    now = int(time.time())

    # Call without beta parameter - should use config default (0.6)
    score = calculate_score(
        use_count=5,
        last_used=now,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
    )

    assert score > 0


def test_calculate_score_power_law_model():
    """Test calculate_score with power_law decay model."""
    config = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
    set_config(config)

    now = int(time.time())
    one_day_ago = now - 86400

    # Fresh memory
    score_fresh = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
    )

    # Old memory
    score_old = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    assert score_fresh > 0
    assert score_old > 0
    assert score_old < score_fresh  # Power law decay


def test_calculate_score_power_law_different_alpha():
    """Test power_law model with different alpha values."""
    now = int(time.time())
    one_day_ago = now - 86400

    # Test with alpha = 0.8 (lighter tail)
    config1 = Config(decay_model="power_law", pl_alpha=0.8, pl_halflife_days=3.0)
    set_config(config1)

    score1 = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    # Test with alpha = 1.5 (heavier tail)
    config2 = Config(decay_model="power_law", pl_alpha=1.5, pl_halflife_days=3.0)
    set_config(config2)

    score2 = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    assert score1 > 0
    assert score2 > 0


def test_calculate_score_two_component_model():
    """Test calculate_score with two_component decay model."""
    config = Config(
        decay_model="two_component",
        tc_lambda_fast=1.603e-5,
        tc_lambda_slow=1.147e-6,
        tc_weight_fast=0.7,
    )
    set_config(config)

    now = int(time.time())
    one_day_ago = now - 86400

    # Fresh memory
    score_fresh = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
    )

    # Old memory
    score_old = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    assert score_fresh > 0
    assert score_old > 0
    assert score_old < score_fresh  # Two-component decay


def test_calculate_score_two_component_different_weights():
    """Test two_component model with different weight values."""
    now = int(time.time())
    one_day_ago = now - 86400

    # Test with different weights - both should produce valid scores
    config1 = Config(
        decay_model="two_component",
        tc_lambda_fast=1.603e-5,
        tc_lambda_slow=1.147e-6,
        tc_weight_fast=0.9,
    )
    set_config(config1)

    score1 = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    config2 = Config(
        decay_model="two_component",
        tc_lambda_fast=1.603e-5,
        tc_lambda_slow=1.147e-6,
        tc_weight_fast=0.3,
    )
    set_config(config2)

    score2 = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    # Both should produce valid scores
    assert score1 > 0
    assert score1 < 1.0
    assert score2 > 0
    assert score2 < 1.0


def test_calculate_score_exponential_model():
    """Test calculate_score with exponential decay model via config."""
    config = Config(decay_model="exponential", decay_lambda=2.673e-6)
    set_config(config)

    now = int(time.time())
    one_day_ago = now - 86400

    # Fresh memory
    score_fresh = calculate_score(
        use_count=1,
        last_used=now,
        strength=1.0,
    )

    # Old memory
    score_old = calculate_score(
        use_count=1,
        last_used=one_day_ago,
        strength=1.0,
    )

    assert score_fresh > 0
    assert score_old > 0
    assert score_old < score_fresh  # Exponential decay


def test_time_until_threshold_exponential_from_config():
    """Test time_until_threshold with exponential model from config."""
    config = Config(decay_model="exponential", decay_lambda=calculate_decay_lambda(3.0))
    set_config(config)

    now = int(time.time())

    # Test without lambda_ parameter - should use config
    remaining = time_until_threshold(
        current_score=1.0,
        threshold=0.5,
        last_used=now,
    )

    assert remaining is not None
    # Should be approximately 3 days (259200 seconds)
    assert remaining == pytest.approx(259200, rel=0.1)


def test_time_until_threshold_power_law():
    """Test time_until_threshold with power_law model."""
    config = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
    set_config(config)

    now = int(time.time())

    remaining = time_until_threshold(
        current_score=1.0,
        threshold=0.5,
        last_used=now,
    )

    assert remaining is not None
    assert remaining > 0


def test_time_until_threshold_two_component():
    """Test time_until_threshold with two_component model."""
    config = Config(
        decay_model="two_component",
        tc_lambda_fast=1.603e-5,
        tc_lambda_slow=1.147e-6,
        tc_weight_fast=0.7,
    )
    set_config(config)

    now = int(time.time())

    remaining = time_until_threshold(
        current_score=1.0,
        threshold=0.5,
        last_used=now,
    )

    assert remaining is not None
    assert remaining > 0


def test_time_until_threshold_different_thresholds():
    """Test time_until_threshold with various threshold values."""
    now = int(time.time())

    # Higher threshold (0.9) - should reach it faster
    remaining_high = time_until_threshold(
        current_score=1.0,
        threshold=0.9,
        last_used=now,
        lambda_=2.673e-6,
    )

    # Lower threshold (0.1) - should take longer
    remaining_low = time_until_threshold(
        current_score=1.0,
        threshold=0.1,
        last_used=now,
        lambda_=2.673e-6,
    )

    assert remaining_high is not None
    assert remaining_low is not None
    assert remaining_high < remaining_low


def test_time_until_threshold_never_reaches():
    """Test time_until_threshold when score never reaches threshold (edge case)."""
    # Set up a scenario where the decay is extremely slow
    config = Config(decay_model="power_law", pl_alpha=0.01, pl_halflife_days=10000.0)
    set_config(config)

    now = int(time.time())

    # Very high current score, very low threshold, but decay is so slow it might not reach
    remaining = time_until_threshold(
        current_score=1.0,
        threshold=0.00001,
        last_used=now,
    )

    # Should either return a very large number or None
    # This tests the upper bound cap logic
    if remaining is None:
        assert True  # Never reaches
    else:
        assert remaining >= 0


def test_lambda_halflife_roundtrip():
    """Test roundtrip conversion: halflife -> lambda -> halflife."""
    original_halflife = 5.0  # days

    # Convert to lambda
    lambda_val = calculate_decay_lambda(original_halflife)

    # Convert back to halflife
    result_halflife = calculate_halflife(lambda_val)

    assert result_halflife == pytest.approx(original_halflife, rel=1e-6)


def test_halflife_lambda_roundtrip():
    """Test roundtrip conversion: lambda -> halflife -> lambda."""
    original_lambda = 1.5e-6

    # Convert to halflife
    halflife = calculate_halflife(original_lambda)

    # Convert back to lambda
    result_lambda = calculate_decay_lambda(halflife)

    assert result_lambda == pytest.approx(original_lambda, rel=1e-6)


def test_calculate_decay_lambda_different_halflifes():
    """Test calculate_decay_lambda with different halflife values."""
    lambda_1d = calculate_decay_lambda(1.0)
    lambda_7d = calculate_decay_lambda(7.0)
    lambda_30d = calculate_decay_lambda(30.0)

    # Longer halflife = smaller lambda (slower decay)
    assert lambda_1d > lambda_7d > lambda_30d


def test_project_score_past_time():
    """Test project_score_at_time with past time (should show higher score)."""
    now = int(time.time())
    two_days_ago = now - 2 * 86400
    one_day_ago = now - 86400

    # Project to past time
    projected_past = project_score_at_time(
        use_count=5,
        last_used=two_days_ago,
        strength=1.0,
        target_time=one_day_ago,
        lambda_=2.673e-6,
        beta=0.6,
    )

    # Current score (2 days after last_used)
    current = calculate_score(
        use_count=5,
        last_used=two_days_ago,
        strength=1.0,
        now=now,
        lambda_=2.673e-6,
        beta=0.6,
    )

    # Past projection should have higher score
    assert projected_past > current


def test_project_score_different_models():
    """Test project_score_at_time with different decay models."""
    now = int(time.time())
    future = now + 86400

    # Power law
    config_pl = Config(decay_model="power_law", pl_alpha=1.1, pl_halflife_days=3.0)
    set_config(config_pl)

    score_pl = project_score_at_time(
        use_count=5,
        last_used=now,
        strength=1.0,
        target_time=future,
    )

    # Two component
    config_tc = Config(
        decay_model="two_component",
        tc_lambda_fast=1.603e-5,
        tc_lambda_slow=1.147e-6,
        tc_weight_fast=0.7,
    )
    set_config(config_tc)

    score_tc = project_score_at_time(
        use_count=5,
        last_used=now,
        strength=1.0,
        target_time=future,
    )

    # Exponential
    config_exp = Config(decay_model="exponential", decay_lambda=2.673e-6)
    set_config(config_exp)

    score_exp = project_score_at_time(
        use_count=5,
        last_used=now,
        strength=1.0,
        target_time=future,
    )

    # All should produce valid scores
    assert score_pl > 0
    assert score_tc > 0
    assert score_exp > 0
