"""
Tests for prediction_feedback.decay_stale_predictions().

Verifies that confidence decays exactly DECAY_AMOUNT per 24h window,
not per invocation — regression test for the hourly-over-decay bug.
"""
import json
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch

import pytest

# Patch file paths before importing the module under test
_TMP = tempfile.mkdtemp()
_ALERTS = Path(_TMP) / "prediction_alerts.json"
_METRICS = Path(_TMP) / "prediction_metrics.json"
_REWARDS = Path(_TMP) / "reward_signals.json"

import pulse.daemons.neural.prediction_feedback as fb

fb.ALERTS_FILE = _ALERTS
fb.METRICS_FILE = _METRICS
fb.REWARD_SIGNALS_FILE = _REWARDS


def _make_alert(confidence: float, age_hours: float, last_decayed_hours_ago: float = None) -> dict:
    """Helper — build a minimal active alert dict."""
    now = datetime.now(timezone.utc)
    created = now - timedelta(hours=age_hours)
    alert = {
        "task_id": f"oracle_{int(created.timestamp())}",
        "prediction": "test prediction",
        "confidence": confidence,
        "status": "active",
        "timestamp": created.isoformat(),
    }
    if last_decayed_hours_ago is not None:
        alert["last_decayed_at"] = (now - timedelta(hours=last_decayed_hours_ago)).isoformat()
    return alert


def _write_alerts(alerts: list):
    _ALERTS.write_text(json.dumps({"alerts": alerts, "total_generated": len(alerts)}))


def _read_alerts() -> list:
    return json.loads(_ALERTS.read_text())["alerts"]


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

def test_no_decay_before_24h():
    """Alerts < 24h old must not be decayed."""
    _write_alerts([_make_alert(0.7, age_hours=12)])
    decayed, archived = fb.decay_stale_predictions()
    assert decayed == 0
    assert archived == 0
    assert _read_alerts()[0]["confidence"] == 0.7


def test_decay_applied_after_24h():
    """Alerts >= 24h old with no prior decay should lose DECAY_AMOUNT."""
    _write_alerts([_make_alert(0.7, age_hours=25)])
    decayed, archived = fb.decay_stale_predictions()
    assert decayed == 1
    assert archived == 0
    assert _read_alerts()[0]["confidence"] == round(0.7 - fb.DECAY_AMOUNT, 2)


def test_no_second_decay_within_24h_window():
    """Calling decay again within 24h of last decay must NOT reduce confidence further."""
    _write_alerts([_make_alert(0.7, age_hours=30, last_decayed_hours_ago=2)])
    decayed, archived = fb.decay_stale_predictions()
    assert decayed == 0
    assert _read_alerts()[0]["confidence"] == 0.7  # unchanged


def test_second_decay_after_24h_window():
    """Calling decay after 24h+ since last decay SHOULD apply another DECAY_AMOUNT."""
    _write_alerts([_make_alert(0.7, age_hours=50, last_decayed_hours_ago=25)])
    decayed, archived = fb.decay_stale_predictions()
    assert decayed == 1
    assert _read_alerts()[0]["confidence"] == round(0.7 - fb.DECAY_AMOUNT, 2)


def test_expire_at_threshold():
    """Alert whose confidence falls below EXPIRE_THRESHOLD should be archived."""
    initial = fb.EXPIRE_THRESHOLD + fb.DECAY_AMOUNT - 0.01  # just enough to cross threshold
    _write_alerts([_make_alert(round(initial, 2), age_hours=25)])
    decayed, archived = fb.decay_stale_predictions()
    assert archived == 1
    assert _read_alerts()[0]["status"] == "expired"
    assert _read_alerts()[0]["outcome"] == "auto_expired_stale"


def test_confidence_floored_at_zero():
    """Confidence never goes negative even at very low values."""
    _write_alerts([_make_alert(0.02, age_hours=25)])
    fb.decay_stale_predictions()
    assert _read_alerts()[0]["confidence"] >= 0.0


def test_last_decayed_at_written_after_decay():
    """decay_stale_predictions() must write last_decayed_at on the alert."""
    _write_alerts([_make_alert(0.7, age_hours=25)])
    fb.decay_stale_predictions()
    alert = _read_alerts()[0]
    assert "last_decayed_at" in alert


def test_resolved_alerts_not_decayed():
    """Non-active alerts (resolved, expired) must be skipped."""
    alert = _make_alert(0.7, age_hours=48)
    alert["status"] = "resolved_success"
    _write_alerts([alert])
    decayed, archived = fb.decay_stale_predictions()
    assert decayed == 0


def test_decay_rate_is_exactly_one_per_day():
    """Simulate 3 days of hourly decay calls — alert should lose 3 × DECAY_AMOUNT total."""
    from unittest.mock import patch
    import pulse.daemons.neural.prediction_feedback as fb2

    fb2.ALERTS_FILE = _ALERTS
    start_conf = 0.8
    _write_alerts([_make_alert(start_conf, age_hours=24)])

    # Simulate 3 × 24h cycles, each with the decay called 24 times (hourly)
    base_time = datetime.now(timezone.utc)
    for day in range(3):
        for hour in range(24):
            fake_now = base_time + timedelta(hours=day * 24 + hour + 24)
            with patch("pulse.daemons.neural.prediction_feedback.datetime") as mock_dt:
                mock_dt.now.return_value = fake_now
                mock_dt.fromisoformat.side_effect = datetime.fromisoformat
                fb2.decay_stale_predictions()

    final_conf = _read_alerts()[0]["confidence"]
    expected = round(start_conf - 3 * fb.DECAY_AMOUNT, 2)
    assert final_conf == expected, f"Expected {expected} after 3 days, got {final_conf}"
