"""Unit tests for Dimension D temporal decay (pulse/core/temporal.py).

No DB, no network — pure function tests.
"""
import math
import pytest
from datetime import datetime, timezone, timedelta

from pulse.core.temporal import (
    get_half_life,
    temporal_confidence,
    validity_transition,
    reinforce,
    stamp_temporal_fields,
    promote_to_stable,
    HALF_LIVES,
    DEFAULT_HALF_LIFE,
    VALID, STABLE, SUPERSEDED, EXPIRED, HISTORICAL, CONTESTED,
)


# ---------------------------------------------------------------------------
# get_half_life
# ---------------------------------------------------------------------------

class TestGetHalfLife:
    def test_known_types_return_correct_values(self):
        assert get_half_life("fact") == 60
        assert get_half_life("lesson") == 180
        assert get_half_life("pattern") == 365
        assert get_half_life("episodic") == 30
        assert get_half_life("prediction") == 3

    def test_unknown_type_returns_default(self):
        assert get_half_life("nonexistent_type") == DEFAULT_HALF_LIFE

    def test_all_half_lives_positive(self):
        for ktype, hl in HALF_LIVES.items():
            assert hl > 0, f"Half-life for '{ktype}' must be positive"

    def test_pattern_longer_than_fact(self):
        """Structural knowledge outlasts facts."""
        assert get_half_life("pattern") > get_half_life("fact")

    def test_episodic_shortest(self):
        """Raw episodic recall fades fastest (except predictions)."""
        non_prediction_types = [k for k in HALF_LIVES if k != "prediction"]
        episodic_hl = get_half_life("episodic")
        for ktype in non_prediction_types:
            assert episodic_hl <= get_half_life(ktype)


# ---------------------------------------------------------------------------
# temporal_confidence
# ---------------------------------------------------------------------------

class TestTemporalConfidence:
    def test_zero_days_returns_base(self):
        """No time elapsed → no decay."""
        assert temporal_confidence(0.8, days_old=0.0) == 0.8

    def test_at_half_life_decays_to_70_percent(self):
        """Power-law: at t=HL, factor = (1+1)^-0.5 = 1/√2 ≈ 0.707."""
        base = 1.0
        hl = 180.0
        result = temporal_confidence(base, days_old=hl, half_life_days=hl)
        assert result == pytest.approx(1.0 / math.sqrt(2), abs=1e-3)

    def test_floor_at_0_01(self):
        """Confidence never drops below 0.01."""
        result = temporal_confidence(1.0, days_old=100_000.0, half_life_days=1.0)
        assert result == pytest.approx(0.01)

    def test_decay_monotone_with_age(self):
        """Older items always have lower confidence."""
        results = [temporal_confidence(0.9, days_old=d, half_life_days=100) for d in [0, 10, 50, 100, 500]]
        for i in range(len(results) - 1):
            assert results[i] >= results[i + 1]

    def test_retrieval_extends_half_life(self):
        """Frequently retrieved items decay more slowly."""
        base, days = 0.8, 90.0
        no_retrieval = temporal_confidence(base, days_old=days, half_life_days=180, retrieval_count=0)
        with_retrieval = temporal_confidence(base, days_old=days, half_life_days=180, retrieval_count=9)
        assert with_retrieval > no_retrieval

    def test_high_arousal_extends_half_life(self):
        """Emotional memories fade slower (McGaugh 2004)."""
        base, days = 0.8, 90.0
        low_arousal  = temporal_confidence(base, days_old=days, half_life_days=180, arousal=0.0)
        high_arousal = temporal_confidence(base, days_old=days, half_life_days=180, arousal=0.9)
        assert high_arousal > low_arousal

    def test_arousal_below_threshold_no_effect(self):
        """Arousal <= 0.5 has no effect on half-life."""
        base, days = 0.8, 90.0
        r0 = temporal_confidence(base, days_old=days, half_life_days=180, arousal=0.0)
        r5 = temporal_confidence(base, days_old=days, half_life_days=180, arousal=0.5)
        assert r0 == pytest.approx(r5, abs=1e-6)

    def test_overlearning_resistance_at_ec_10(self):
        """evidence_count >= 10 doubles the effective half-life."""
        base, days = 0.8, 90.0
        hl = 180
        normal    = temporal_confidence(base, days_old=days, half_life_days=hl, evidence_count=1)
        overlearn = temporal_confidence(base, days_old=days, half_life_days=hl, evidence_count=10)
        assert overlearn > normal

    def test_timestamp_auto_computes_days_old(self):
        """Providing timestamp should auto-compute days_old and match manual computation."""
        ts_old = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
        auto   = temporal_confidence(0.9, timestamp=ts_old, half_life_days=180)
        manual = temporal_confidence(0.9, days_old=30.0, half_life_days=180)
        assert auto == pytest.approx(manual, abs=1e-2)

    def test_knowledge_type_lookup(self):
        """knowledge_type triggers correct half-life lookup."""
        fact_result    = temporal_confidence(1.0, days_old=30.0, knowledge_type="fact")
        pattern_result = temporal_confidence(1.0, days_old=30.0, knowledge_type="pattern")
        # patterns decay slower → higher confidence after same time
        assert pattern_result > fact_result


# ---------------------------------------------------------------------------
# validity_transition
# ---------------------------------------------------------------------------

class TestValidityTransition:
    def test_valid_to_stable(self):
        assert validity_transition(VALID, STABLE) == STABLE

    def test_valid_to_superseded(self):
        assert validity_transition(VALID, SUPERSEDED) == SUPERSEDED

    def test_valid_to_expired(self):
        assert validity_transition(VALID, EXPIRED) == EXPIRED

    def test_valid_to_historical(self):
        assert validity_transition(VALID, HISTORICAL) == HISTORICAL

    def test_expired_to_valid_allowed(self):
        """Items can be revived from expired."""
        assert validity_transition(EXPIRED, VALID) == VALID

    def test_historical_is_terminal(self):
        """Historical has no outgoing transitions."""
        for target in [VALID, STABLE, EXPIRED, CONTESTED, SUPERSEDED]:
            assert validity_transition(HISTORICAL, target) is None

    def test_stable_cannot_go_to_valid(self):
        """Stable items cannot regress to valid."""
        assert validity_transition(STABLE, VALID) is None

    def test_invalid_state_returns_none(self):
        assert validity_transition("garbage", VALID) is None
        assert validity_transition(VALID, "garbage") is None

    def test_contested_can_resolve_to_valid(self):
        assert validity_transition(CONTESTED, VALID) == VALID

    def test_contested_can_go_historical(self):
        assert validity_transition(CONTESTED, HISTORICAL) == HISTORICAL


# ---------------------------------------------------------------------------
# reinforce
# ---------------------------------------------------------------------------

class TestReinforce:
    def test_sets_last_reinforced(self):
        item = {"validity": VALID}
        before = datetime.now(timezone.utc).isoformat()
        result = reinforce(item)
        assert "last_reinforced" in result
        assert result["last_reinforced"] >= before

    def test_revives_expired_item(self):
        item = {"validity": EXPIRED}
        result = reinforce(item)
        assert result["validity"] == VALID

    def test_does_not_change_valid_item_validity(self):
        item = {"validity": VALID}
        result = reinforce(item)
        assert result["validity"] == VALID

    def test_updates_expires_at_when_half_life_present(self):
        item = {"validity": VALID, "half_life_days": 180.0}
        result = reinforce(item)
        assert "expires_at" in result
        assert result["expires_at"]  # non-empty string


# ---------------------------------------------------------------------------
# stamp_temporal_fields
# ---------------------------------------------------------------------------

class TestStampTemporalFields:
    def test_sets_all_d_fields(self):
        item = {"type": "lesson"}
        result = stamp_temporal_fields(item)
        assert result["validity"] == VALID
        assert result["half_life_days"] == get_half_life("lesson")
        assert "last_reinforced" in result
        assert "expires_at" in result
        assert result["superseded_by"] is None

    def test_respects_knowledge_type_override(self):
        item = {}
        result = stamp_temporal_fields(item, knowledge_type="pattern")
        assert result["half_life_days"] == get_half_life("pattern")

    def test_does_not_overwrite_existing_validity(self):
        """stamp_temporal_fields uses setdefault — won't clobber existing validity."""
        item = {"type": "lesson", "validity": CONTESTED}
        result = stamp_temporal_fields(item)
        assert result["validity"] == CONTESTED

    def test_expires_at_after_creation(self):
        """expires_at should be in the future."""
        item = {"type": "lesson"}
        result = stamp_temporal_fields(item)
        expires = datetime.fromisoformat(result["expires_at"].replace("Z", "+00:00"))
        assert expires > datetime.now(timezone.utc)


# ---------------------------------------------------------------------------
# promote_to_stable
# ---------------------------------------------------------------------------

class TestPromoteToStable:
    def test_promotes_when_criteria_met(self):
        item = {"validity": VALID, "evidence_count": 5, "confidence": 0.75,
                "half_life_days": 180.0}
        result = promote_to_stable(item)
        assert result["validity"] == STABLE
        assert result["half_life_days"] == 360.0  # 2x

    def test_no_promotion_below_evidence_threshold(self):
        item = {"validity": VALID, "evidence_count": 4, "confidence": 0.8,
                "half_life_days": 180.0}
        result = promote_to_stable(item)
        assert result["validity"] == VALID

    def test_no_promotion_below_confidence_threshold(self):
        item = {"validity": VALID, "evidence_count": 10, "confidence": 0.6,
                "half_life_days": 180.0}
        result = promote_to_stable(item)
        assert result["validity"] == VALID

    def test_no_promotion_if_already_stable(self):
        item = {"validity": STABLE, "evidence_count": 10, "confidence": 0.9,
                "half_life_days": 180.0}
        result = promote_to_stable(item)
        # Should not double-promote
        assert result["half_life_days"] == 180.0
