#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
P0 fix regression tests:

1. gc_stale_dispatches — stale open-only dispatches are archived after max_age_hours;
   young dispatches and mixed-status dispatches are left alone.
2. cognitive_triage soft timeout — triage_active is cleared when elapsed > 30 min.
"""
import json
from datetime import datetime, timedelta, timezone
from unittest.mock import patch, MagicMock

import pytest

import pulse.tasks.dispatcher_lifecycle as lc
import pulse.daemons.synthesis.health.cognitive_triage as ct


# ===========================================================================
# Helpers — gc_stale_dispatches
# ===========================================================================

def _make_dispatch(dispatch_id: str, statuses: list[str],
                   age_hours: float = 0) -> dict:
    """Build a dispatch dict with tasks at given statuses and age."""
    dispatched_at = (
        datetime.now(timezone.utc) - timedelta(hours=age_hours)
    ).isoformat()
    tasks = [
        {"task_id": f"{dispatch_id}_t{i}", "status": s}
        for i, s in enumerate(statuses)
    ]
    return {
        "dispatch_id": dispatch_id,
        "dispatched_at": dispatched_at,
        "prompt": "test",
        "tasks": tasks,
    }


def _board(*dispatches: dict) -> dict:
    return {"dispatches": list(dispatches), "version": 1}


# ===========================================================================
# Fixtures — gc_stale_dispatches
# ===========================================================================

@pytest.fixture(autouse=True)
def patch_completed_dir(tmp_path, monkeypatch):
    """Redirect COMPLETED_DIR so tests never touch the real state/completed/."""
    completed = tmp_path / "completed"
    monkeypatch.setattr(lc, "COMPLETED_DIR", completed)
    return completed


# ===========================================================================
# Test 1a: stale all-open dispatch older than 6h IS archived
# ===========================================================================

def test_gc_stale_open_dispatch_older_than_max_age(patch_completed_dir):
    """A dispatch with all 'open' tasks older than 6 hours must be GC'd."""
    d = _make_dispatch("d_stale_001", ["open", "open", "open"], age_hours=7)
    board = _board(d)

    result = lc.gc_stale_dispatches(board)

    assert result is True
    assert board["dispatches"] == []

    archive_file = patch_completed_dir / "d_stale_001.json"
    assert archive_file.exists()
    data = json.loads(archive_file.read_text(encoding="utf-8"))
    assert data["dispatch_id"] == "d_stale_001"
    assert data["gc_reason"] == "stale_open"
    assert "archived_at" in data


# ===========================================================================
# Test 1b: young all-open dispatch is NOT archived
# ===========================================================================

def test_gc_young_open_dispatch_not_removed(patch_completed_dir):
    """A dispatch with all 'open' tasks younger than 6 hours must NOT be GC'd."""
    d = _make_dispatch("d_young_001", ["open", "open"], age_hours=3)
    board = _board(d)

    result = lc.gc_stale_dispatches(board)

    assert result is False
    assert len(board["dispatches"]) == 1
    assert board["dispatches"][0]["dispatch_id"] == "d_young_001"
    assert not list(patch_completed_dir.glob("*.json"))


# ===========================================================================
# Test 1c: mixed-status dispatch is NOT removed by GC
# ===========================================================================

def test_gc_mixed_status_dispatch_not_removed(patch_completed_dir):
    """A dispatch with mixed statuses (some open, some done) must NOT be GC'd,
    even if the dispatch is old. That's archive_completed's job."""
    d = _make_dispatch("d_mixed_001", ["open", "done", "open"], age_hours=10)
    board = _board(d)

    result = lc.gc_stale_dispatches(board)

    assert result is False
    assert len(board["dispatches"]) == 1
    assert board["dispatches"][0]["dispatch_id"] == "d_mixed_001"
    assert not list(patch_completed_dir.glob("*.json"))


# ===========================================================================
# Fixtures — cognitive_triage
# ===========================================================================

@pytest.fixture()
def triage_tmp(tmp_path, monkeypatch):
    """Redirect cognitive_triage file paths to tmp_path."""
    health_dir = tmp_path / "health"
    health_dir.mkdir()
    monkeypatch.setattr(ct, "HEALTH_DIR", health_dir)
    monkeypatch.setattr(ct, "DAEMON_HEALTH_FILE", health_dir / "daemon_health.json")
    monkeypatch.setattr(ct, "METRICS_FILE", health_dir / "cognitive_triage.json")
    monkeypatch.setattr(ct, "OVERRIDE_FILE", tmp_path / "homeostatic_overrides.json")
    return tmp_path


# ===========================================================================
# Test 2: soft timeout clears triage_active after 35 minutes
# ===========================================================================

def test_triage_soft_timeout_clears_active(triage_tmp):
    """When triage has been active for 35 minutes (past the 30-min
    _TRIAGE_DURATION_MIN), run_once must set triage_active = False."""
    metrics_file = ct.METRICS_FILE
    started = (
        datetime.now(timezone.utc) - timedelta(minutes=35)
    ).isoformat()

    # Pre-seed state: triage is active, started 35 min ago
    state = {
        "runs": 5,
        "triage_active": True,
        "triage_started": started,
        "total_events": 1,
        "current_severity": 0.7,
        "current_failures": ["daemon_error"],
    }
    metrics_file.write_text(json.dumps(state), encoding="utf-8")

    # No daemon health data → no new failures → severity = 0
    ct.DAEMON_HEALTH_FILE.write_text("{}", encoding="utf-8")

    result = ct.run_once()

    assert result["triage_active"] is False

    # Verify persisted state also reflects the change
    saved = json.loads(metrics_file.read_text(encoding="utf-8"))
    assert saved["triage_active"] is False
