#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Tests pour le Moteur de Decay
═══════════════════════════════════════════════════════════════════════════════
"""

import pytest
import yaml
import json
import subprocess
import sys
from pathlib import Path
from datetime import datetime, timedelta
from decay import DecayEngine, DecayReport
from config import DECAY_RATE, DECAY_RATE_REINFORCED
from conftest import REFERENCE_DATE, days_ago


# ═══════════════════════════════════════════════════════════════════════════════
#  FIXTURES
# ═══════════════════════════════════════════════════════════════════════════════

def _write_yaml(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False),
        encoding="utf-8",
    )


def _load_yaml(path: Path) -> dict:
    return yaml.safe_load(path.read_text(encoding="utf-8"))


@pytest.fixture
def decay_project(tmp_path):
    """Creates an Argus project with patterns having different last_seen dates."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    # _index.yaml
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "statistics": {"activity": {"last_consolidation": None}},
    })

    # Patterns with varying ages (relative to REFERENCE_DATE = 2026-03-01)
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "version": "1.0.0",
        "patterns": [
            {
                "id": "PAT-FRESH-001",
                "name": "Fresh Pattern",
                "category": "null_safety",
                "confidence": 0.90,
                "occurrences": 2,
                "last_seen": days_ago(14),        # Recent (14 days ago)
                "source_episodes": ["INC-20260215-001"],
            },
            {
                "id": "PAT-OLD-001",
                "name": "Old Pattern",
                "category": "async",
                "confidence": 0.70,
                "occurrences": 0,
                "last_seen": days_ago(273),       # Very old (~9 months)
                "source_episodes": [],
            },
            {
                "id": "PAT-NEVER-001",
                "name": "Never Seen Pattern",
                "category": "performance",
                "confidence": 0.50,
                "occurrences": 0,
                "last_seen": None,               # Never seen
                "source_episodes": [],
            },
            {
                "id": "PAT-SEC-001",
                "name": "Security Pattern",
                "category": "security",
                "confidence": 0.60,
                "occurrences": 0,
                "last_seen": days_ago(425),       # Very old but protected
                "source_episodes": [],
            },
            {
                "id": "PAT-CRIT-001",
                "name": "Critical Linked",
                "category": "regression",
                "severity_typical": "critical",
                "confidence": 0.40,
                "occurrences": 1,
                "last_seen": days_ago(365),       # Old but critical
                "source_episodes": ["INC-20250301-001"],
            },
        ],
    })

    # Heuristics
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
        "version": "1.0.0",
        "test_selection": [
            {
                "id": "HEU-SEL-001",
                "name": "Test What Changed",
                "confidence": 0.95,
                "last_seen": days_ago(28),        # Recent (28 days ago)
            },
            {
                "id": "HEU-OLD-001",
                "name": "Old Heuristic",
                "confidence": 0.60,
                "last_seen": days_ago(365),       # Old (365 days ago)
            },
        ],
    })

    # Incidents dir (with a critical one)
    incidents = memory / "episodic" / "incidents"
    incidents.mkdir(parents=True, exist_ok=True)
    _write_yaml(incidents / "critical.yaml", {
        "id": "INC-20250301-001",
        "severity": "P0",
        "root_cause": {
            "matched_patterns": ["PAT-CRIT-001"],
        },
    })

    # Other episodic dirs
    for sub in ["analyses", "decisions", "executions"]:
        (memory / "episodic" / sub).mkdir(parents=True, exist_ok=True)

    return root


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: SCAN — Grace Period
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayGracePeriod:
    def test_fresh_pattern_no_decay(self, decay_project):
        """Patterns within grace period should not decay."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        fresh = [a for a in report.actions if a.target_id == "PAT-FRESH-001"]
        assert len(fresh) == 0  # Within grace period (14 days old)

    def test_old_pattern_decays(self, decay_project):
        """Old patterns should have decay actions."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        old = [a for a in report.actions if a.target_id == "PAT-OLD-001" and not a.protected]
        assert len(old) == 1
        assert old[0].new_confidence < old[0].current_confidence

    def test_never_seen_decays_with_high_default(self, decay_project):
        """Patterns never seen should use a high default for days_since."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        never = [a for a in report.actions if a.target_id == "PAT-NEVER-001" and not a.protected]
        assert len(never) == 1
        assert never[0].days_since_activity >= 365


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: PROTECTION
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayProtection:
    def test_security_category_protected(self, decay_project):
        """Security patterns should be protected from decay."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        sec = [a for a in report.actions if a.target_id == "PAT-SEC-001"]
        assert len(sec) == 1
        assert sec[0].protected is True

    def test_critical_incident_protected(self, decay_project):
        """Patterns linked to P0/P1 incidents should be protected."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        crit = [a for a in report.actions if a.target_id == "PAT-CRIT-001"]
        assert len(crit) == 1
        assert crit[0].protected is True

    def test_protected_count_in_report(self, decay_project):
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        assert report.protected_count >= 2  # Security + Critical


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: DECAY CALCULATION
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayCalculation:
    def test_decay_rate_unreinforced(self, decay_project):
        """Unreinforced patterns should decay at the faster rate."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        old = next(a for a in report.actions if a.target_id == "PAT-OLD-001")
        # 9 months inactive, rate 0.05/month, occurrences=0
        expected_decay = DECAY_RATE * (old.days_since_activity / 30.0)
        assert abs(old.decay_amount - expected_decay) < 0.01

    def test_flagged_for_removal_below_threshold(self, decay_project):
        """Items below threshold should be flagged for removal."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        # PAT-NEVER-001: confidence 0.50, 365 days → massive decay → should be flagged
        never = next(a for a in report.actions if a.target_id == "PAT-NEVER-001" and not a.protected)
        assert never.flagged_for_removal is True

    def test_confidence_does_not_go_negative(self, decay_project):
        """Confidence after decay should stay >= 0."""
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        for a in report.actions:
            assert a.new_confidence >= 0.0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: APPLY
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayApply:
    def test_apply_updates_confidence(self, decay_project):
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        engine.apply()

        patterns = _load_yaml(decay_project / "memory" / "semantic" / "defect_patterns.yaml")
        old_pat = next(p for p in patterns["patterns"] if p["id"] == "PAT-OLD-001")
        assert old_pat["confidence"] < 0.70  # Was 0.70

    def test_apply_does_not_touch_protected(self, decay_project):
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        engine.apply()

        patterns = _load_yaml(decay_project / "memory" / "semantic" / "defect_patterns.yaml")
        sec_pat = next(p for p in patterns["patterns"] if p["id"] == "PAT-SEC-001")
        assert sec_pat["confidence"] == 0.60  # Unchanged

    def test_apply_updates_heuristics(self, decay_project):
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        engine.apply()

        heuristics = _load_yaml(decay_project / "memory" / "semantic" / "testing_heuristics.yaml")
        old_h = next(h for h in heuristics["test_selection"] if h["id"] == "HEU-OLD-001")
        assert old_h["confidence"] < 0.60  # Was 0.60


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: REPORT
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayReport:
    def test_report_to_dict(self, decay_project):
        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(decay_project), verbose=False, reference_date=ref_date)
        report = engine.scan()
        d = report.to_dict()
        assert "summary" in d
        assert "actions" in d
        assert "protected" in d
        # JSON serializable
        result = json.dumps(d)
        assert isinstance(result, str)

    def test_healthy_project(self, tmp_path):
        """A project with only fresh items should be HEALTHY."""
        root = tmp_path / "fresh"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-NEW-001",
                "confidence": 0.90,
                "last_seen": days_ago(1),
                "occurrences": 1,
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        ref_date = REFERENCE_DATE
        engine = DecayEngine(str(root), verbose=False, reference_date=ref_date)
        report = engine.scan()
        assert report.to_dict()["summary"]["status"] == "HEALTHY"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: REFERENCE DATE
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayReferenceDate:
    def test_future_date_increases_decay(self, decay_project):
        """A future reference date should increase decay amounts."""
        engine_now = DecayEngine(str(decay_project), verbose=False,
                                 reference_date=REFERENCE_DATE)
        report_now = engine_now.scan()

        engine_future = DecayEngine(str(decay_project), verbose=False,
                                    reference_date=REFERENCE_DATE + timedelta(days=365))
        report_future = engine_future.scan()

        old_now = next((a for a in report_now.actions
                        if a.target_id == "PAT-OLD-001" and not a.protected), None)
        old_future = next((a for a in report_future.actions
                           if a.target_id == "PAT-OLD-001" and not a.protected), None)

        assert old_now is not None
        assert old_future is not None
        assert old_future.decay_amount > old_now.decay_amount


# ═══════════════════════════════════════════════════════════════════════════════
#  P0 — BVA: Boundary Value Analysis for decay thresholds
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayBVAGracePeriod:
    """BVA sur DECAY_GRACE_DAYS = 60 : condition `days_since < 60`."""

    def _make_project(self, tmp_path, last_seen_date: str):
        """Helper: creates a project with a single pattern at given last_seen."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-BVA-001",
                "confidence": 0.80,
                "last_seen": last_seen_date,
                "occurrences": 0,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)
        return root

    @pytest.mark.parametrize("days,should_decay", [
        (59, False),   # 59 < 60 → within grace, no decay
        (60, True),    # 60 == 60, condition `< 60` → decay triggered
        (61, True),    # 61 > 60 → decay triggered
    ], ids=["59d-no-decay", "60d-boundary-decay", "61d-decay"])
    def test_grace_period_boundary(self, tmp_path, days, should_decay):
        """BVA triplet around DECAY_GRACE_DAYS=60."""
        ref = REFERENCE_DATE
        last_seen = (ref - timedelta(days=days)).strftime("%Y-%m-%d")
        root = self._make_project(tmp_path, last_seen)
        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        actions = [a for a in report.actions if a.target_id == "PAT-BVA-001" and not a.protected]
        if should_decay:
            assert len(actions) == 1
            assert actions[0].decay_amount > 0
        else:
            assert len(actions) == 0


class TestDecayBVAThreshold:
    """BVA sur DECAY_THRESHOLD = 0.20 : condition `new_confidence < 0.20`."""

    def _make_project_with_confidence(self, tmp_path, confidence: float, days_since: int):
        """Helper: pattern with given confidence and inactivity."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        last_seen = (ref - timedelta(days=days_since)).strftime("%Y-%m-%d")
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-THR-001",
                "confidence": confidence,
                "last_seen": last_seen,
                "occurrences": 0,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)
        return root, ref

    @pytest.mark.parametrize("confidence,expected_flagged", [
        (0.30, False),  # decay=0.10 → new=0.20, at threshold → NOT flagged (strict <)
        (0.29, True),   # decay=0.10 → new=0.19, below threshold → flagged
        (0.31, False),  # decay=0.10 → new=0.21, above threshold → NOT flagged
    ], ids=["at-threshold-not-flagged", "below-threshold-flagged", "above-threshold-not-flagged"])
    def test_threshold_boundary(self, tmp_path, confidence, expected_flagged):
        """BVA triplet around DECAY_THRESHOLD=0.20 (60 days inactivity)."""
        root, ref = self._make_project_with_confidence(tmp_path, confidence, 60)
        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        action = next(a for a in report.actions if a.target_id == "PAT-THR-001" and not a.protected)
        assert action.flagged_for_removal is expected_flagged


class TestDecayBVAReinforced:
    """BVA sur DECAY_RATE_REINFORCED vs DECAY_RATE : condition `occurrences > 0`."""

    @pytest.mark.parametrize("occurrences,expected_rate", [
        (1, DECAY_RATE_REINFORCED),  # reinforced → slower decay
        (0, DECAY_RATE),             # unreinforced → faster decay
    ], ids=["reinforced-slow", "unreinforced-fast"])
    def test_decay_rate_by_reinforcement(self, tmp_path, occurrences, expected_rate):
        """Reinforced (occurrences>0) vs unreinforced decay rate over 90 days."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        last_seen = (ref - timedelta(days=90)).strftime("%Y-%m-%d")
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-RATE-001",
                "confidence": 0.80,
                "last_seen": last_seen,
                "occurrences": occurrences,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        action = next(a for a in report.actions if a.target_id == "PAT-RATE-001" and not a.protected)
        expected = expected_rate * (90 / 30.0)
        assert action.decay_amount == pytest.approx(expected, abs=0.001)


class TestDecayBVAConfidenceFloor:
    """BVA: confidence should never go below 0.0."""

    def test_massive_decay_floors_at_zero(self, tmp_path):
        """Very old low-confidence pattern → floor at 0.0."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-FLOOR-001",
                "confidence": 0.10,
                "last_seen": None,  # → 365 days default
                "occurrences": 0,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        action = next(a for a in report.actions if a.target_id == "PAT-FLOOR-001" and not a.protected)
        assert action.new_confidence == 0.0
        assert action.new_confidence >= 0.0


# ═══════════════════════════════════════════════════════════════════════════════
#  P0 — Error Guessing: Corrupt YAML and missing files
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayErrorYAML:
    """Tests ISTQB Error Guessing pour la robustesse YAML."""

    def test_corrupt_patterns_file(self, tmp_path):
        """Corrupt defect_patterns.yaml should record errors, not crash."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        (memory / "semantic").mkdir(parents=True, exist_ok=True)
        (memory / "semantic" / "defect_patterns.yaml").write_text(
            "{{INVALID YAML", encoding="utf-8"
        )
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=REFERENCE_DATE)
        report = engine.scan()
        assert isinstance(report, DecayReport)
        assert len(report.errors) >= 1

    def test_missing_semantic_files(self, tmp_path):
        """Missing semantic files → no actions, no crash."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)
        # No semantic dir at all

        engine = DecayEngine(str(root), verbose=False, reference_date=REFERENCE_DATE)
        report = engine.scan()
        assert isinstance(report, DecayReport)
        assert not report.has_actions


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — EP: Missing Partitions (date parsing fallback, empty ID)
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayPartitions:
    """Tests ISTQB EP pour les partitions manquantes."""

    def test_isoformat_date_parsed(self, tmp_path):
        """last_seen in ISO format (with T) should be parseable via fallback."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-ISO-001",
                "confidence": 0.70,
                "last_seen": "2025-06-01T14:30:00",  # ISO format
                "occurrences": 0,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        action = next(a for a in report.actions if a.target_id == "PAT-ISO-001" and not a.protected)
        assert action.days_since_activity > 200  # Should be ~273 days

    def test_unparseable_date_defaults_to_365_days(self, tmp_path):
        """Totally invalid date string should default to 365 days since."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{
                "id": "PAT-BADDATE-001",
                "confidence": 0.70,
                "last_seen": "not-a-real-date",
                "occurrences": 0,
                "category": "logic",
            }],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        action = next(a for a in report.actions if a.target_id == "PAT-BADDATE-001" and not a.protected)
        assert action.days_since_activity >= 365

    def test_pattern_with_empty_id_skipped(self, tmp_path):
        """Pattern with empty/missing ID should be skipped, not crash."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        ref = REFERENCE_DATE
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [
                {"id": "", "confidence": 0.50, "last_seen": "2025-01-01", "occurrences": 0},
                {"id": "PAT-VALID-001", "confidence": 0.50,
                 "last_seen": "2025-01-01", "occurrences": 0, "category": "logic"},
            ],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        engine = DecayEngine(str(root), verbose=False, reference_date=ref)
        report = engine.scan()
        # Empty ID skipped, only PAT-VALID-001 processed
        assert report.patterns_checked == 1


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — CLI: main() exit codes and JSON output
# ═══════════════════════════════════════════════════════════════════════════════

class TestDecayCLI:
    """Tests ISTQB pour l'interface CLI du moteur de decay."""

    def test_cli_scan_exit_0_healthy(self, tmp_path):
        """CLI scan on healthy project exits with code 0."""
        root = tmp_path / "fresh"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [{"id": "PAT-NEW-001", "confidence": 0.90,
                          "last_seen": days_ago(1), "occurrences": 1}],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {"version": "1.0.0"})
        (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)

        result = subprocess.run(
            [sys.executable, "decay.py", "--base-dir", str(root),
             "--quiet", "--reference-date", REFERENCE_DATE.strftime("%Y-%m-%d")],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        assert result.returncode == 0

    def test_cli_scan_exit_1_with_decay(self, decay_project):
        """CLI scan with decay needed exits with code 1."""
        result = subprocess.run(
            [sys.executable, "decay.py", "--base-dir", str(decay_project),
             "--quiet", "--reference-date", REFERENCE_DATE.strftime("%Y-%m-%d")],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        assert result.returncode == 1

    def test_cli_json_output_valid(self, decay_project):
        """CLI --json produces valid parseable JSON."""
        result = subprocess.run(
            [sys.executable, "decay.py", "--base-dir", str(decay_project),
             "--json", "--reference-date", REFERENCE_DATE.strftime("%Y-%m-%d")],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        parsed = json.loads(result.stdout)
        assert "summary" in parsed
        assert "actions" in parsed
