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

import pytest
import yaml
import json
import subprocess
import sys
from pathlib import Path
from datetime import datetime, timedelta
from reflection import (
    ReflectionEngine, ReflectionReport, HealthCheck,
)
from conftest import REFERENCE_DATE, days_ago


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

def _write_yaml(path: Path, data: dict):
    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 healthy_project(tmp_path):
    """Creates a well-formed Argus project with consistent data."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    # _index.yaml with correct counts
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "statistics": {
            "totals": {"episodes": 1, "patterns": 2, "heuristics": 1, "projects": 1},
            "activity": {"last_consolidation": "2026-02-28"},
        },
        "sequences": {"analysis": 1, "decision": 0, "incident": 0, "execution": 0},
    })

    # Patterns
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "patterns": [
            {"id": "PAT-NULL-001", "category": "null_safety", "confidence": 0.90,
             "occurrences": 1, "last_seen": days_ago(1), "source_episodes": ["ANL-20260228-001"]},
            {"id": "PAT-ASYNC-001", "category": "concurrency", "confidence": 0.85,
             "occurrences": 0, "last_seen": None, "source_episodes": []},
        ],
    })

    # Heuristics
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
        "test_selection": [
            {"id": "HEU-SEL-001", "confidence": 0.95},
        ],
    })

    # One analysis episode
    analyses = memory / "episodic" / "analyses"
    analyses.mkdir(parents=True, exist_ok=True)
    _write_yaml(analyses / "2026-02-28_analysis_ANL-20260228-001.yaml", {
        "id": "ANL-20260228-001",
        "quick_summary": {"one_liner": "test analysis", "status": "COMPLETED", "next_action": "none"},
        "consolidation": {"consolidated": True, "consolidated_at": "2026-02-28T12:00:00"},
    })

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

    # Relational
    _write_yaml(memory / "relational" / "associations.yaml", {
        "component_coupling": {
            "couplings": [
                {"component_a": "schemas", "component_b": "yaml_files"},
                {"component_a": "index", "component_b": "analyses"},
                {"component_a": "tests", "component_b": "schemas"},
            ],
        },
    })

    # Project
    _write_yaml(memory / "projects" / "argus.yaml", {
        "identity": {"project_id": "argus", "name": "Argus"},
    })

    return root


@pytest.fixture
def unhealthy_project(tmp_path):
    """Creates an Argus project with various issues."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    # _index.yaml with WRONG counts
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "statistics": {
            "totals": {"episodes": 99, "patterns": 5, "heuristics": 10},
            "activity": {"last_consolidation": None},
        },
    })

    # Patterns — low confidence, never seen
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "patterns": [
            {"id": "PAT-LOW-001", "category": "null_safety", "confidence": 0.15,
             "occurrences": 0, "last_seen": None, "source_episodes": ["INC-ORPHAN-001"]},
            {"id": "PAT-LOW-002", "category": "null_safety", "confidence": 0.10,
             "occurrences": 0, "last_seen": None, "source_episodes": []},
        ],
    })

    # Heuristics - low confidence
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
        "test_selection": [
            {"id": "HEU-LOW-001", "confidence": 0.30},
        ],
    })

    # 3 unconsolidated analyses
    analyses = memory / "episodic" / "analyses"
    analyses.mkdir(parents=True, exist_ok=True)
    for i in range(6):
        _write_yaml(analyses / f"analysis_{i}.yaml", {
            "id": f"ANL-20260228-{i:03d}",
            "quick_summary": {"one_liner": f"test {i}", "status": "COMPLETED", "next_action": "none"},
        })

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

    # Empty associations
    _write_yaml(memory / "relational" / "associations.yaml", {
        "file_defects": {"entries": []},
    })

    return root


@pytest.fixture
def empty_project(tmp_path):
    """Creates a minimal empty Argus project."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "statistics": {"totals": {"episodes": 0, "patterns": 0, "heuristics": 0}},
    })
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {"patterns": []})
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {})
    _write_yaml(memory / "relational" / "associations.yaml", {})
    for sub in ["analyses", "decisions", "incidents", "executions"]:
        (memory / "episodic" / sub).mkdir(parents=True, exist_ok=True)
    return root


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: HEALTHY PROJECT
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionHealthy:
    def test_healthy_overall_status(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False,
                                  reference_date=REFERENCE_DATE)
        report = engine.reflect()
        d = report.to_dict()
        # May have warnings (e.g., empty episode types) but no errors
        assert d["summary"]["errors"] == 0

    def test_coherent_index_counts(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False,
                                  reference_date=REFERENCE_DATE)
        report = engine.reflect()
        coherence_checks = [c for c in report.checks if c.category == "coherence"]
        count_checks = [c for c in coherence_checks if "count" in c.name]
        ok_checks = [c for c in count_checks if c.status == "ok"]
        assert len(ok_checks) == len(count_checks)  # All count checks should pass

    def test_stats_populated(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        assert report.stats.patterns_count == 2
        assert report.stats.heuristics_count == 1
        assert report.stats.episodes_total == 1
        assert report.stats.projects_count == 1
        assert report.stats.avg_pattern_confidence > 0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: UNHEALTHY PROJECT
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionUnhealthy:
    def test_detects_index_desync(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        coherence_warnings = [c for c in report.checks
                              if c.category == "coherence" and c.status == "warning"]
        assert len(coherence_warnings) >= 1  # At least the counts are wrong

    def test_detects_low_confidence(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        assert report.stats.low_confidence_patterns == 2  # PAT-LOW-001 and PAT-LOW-002
        low_conf_check = next((c for c in report.checks if c.name == "low_confidence_items"), None)
        assert low_conf_check is not None
        assert low_conf_check.status == "warning"

    def test_detects_no_consolidation(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        consol_check = next((c for c in report.checks if c.name == "consolidation_recency"), None)
        assert consol_check is not None
        assert consol_check.status == "warning"

    def test_detects_consolidation_backlog(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        backlog_check = next((c for c in report.checks if c.name == "consolidation_backlog"), None)
        assert backlog_check is not None
        assert backlog_check.status == "warning"
        assert report.stats.episodes_unconsolidated >= 5

    def test_detects_empty_relational(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        rel_check = next((c for c in report.checks if c.name == "relational_memory_population"), None)
        assert rel_check is not None
        assert rel_check.status == "warning"

    def test_detects_all_theoretical_patterns(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        field_check = next(
            (c for c in report.checks if c.name == "patterns_field_validation"), None
        )
        assert field_check is not None
        assert field_check.status == "warning"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: EMPTY PROJECT
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionEmpty:
    def test_empty_project_no_errors(self, empty_project):
        engine = ReflectionEngine(str(empty_project), verbose=False)
        report = engine.reflect()
        assert report.error_count == 0

    def test_empty_project_stats(self, empty_project):
        engine = ReflectionEngine(str(empty_project), verbose=False)
        report = engine.reflect()
        assert report.stats.patterns_count == 0
        assert report.stats.episodes_total == 0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: COMPLETENESS CHECKS
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionCompleteness:
    def test_detects_empty_episode_types(self, healthy_project):
        """Healthy project has analyses but no decisions/incidents/executions."""
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        type_check = next(
            (c for c in report.checks if c.name == "episode_type_coverage"), None
        )
        assert type_check is not None
        assert type_check.status == "warning"
        assert "decisions" in type_check.message or "incidents" in type_check.message

    def test_all_types_present(self, tmp_path):
        """Project with all 4 episode types should pass."""
        root = tmp_path / "full"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {
            "version": "1.0.0",
            "statistics": {"totals": {"episodes": 4, "patterns": 0, "heuristics": 0}},
        })
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {"patterns": []})
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {})
        _write_yaml(memory / "relational" / "associations.yaml", {})
        for sub, prefix in [("analyses", "ANL"), ("decisions", "DEC"),
                            ("incidents", "INC"), ("executions", "EXE")]:
            d = memory / "episodic" / sub
            d.mkdir(parents=True, exist_ok=True)
            _write_yaml(d / f"test_{sub}.yaml", {
                "id": f"{prefix}-20260301-001",
                "quick_summary": {"one_liner": "t", "status": "ok", "next_action": "n"},
            })

        engine = ReflectionEngine(str(root), verbose=False)
        report = engine.reflect()
        type_check = next(
            (c for c in report.checks if c.name == "episode_type_coverage"), None
        )
        assert type_check is not None
        assert type_check.status == "ok"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: BALANCE CHECKS
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionBalance:
    def test_knowledge_experience_ratio(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        ratio_check = next(
            (c for c in report.checks if c.name == "knowledge_experience_ratio"), None
        )
        assert ratio_check is not None
        assert ratio_check.status == "ok"

    def test_pattern_category_diversity(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        div_check = next(
            (c for c in report.checks if c.name == "pattern_category_diversity"), None
        )
        assert div_check is not None
        # 2 categories for 2 patterns is fine
        assert div_check.status == "ok"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: CROSS-REFERENCES
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionCrossReferences:
    def test_orphan_source_episodes_detected(self, unhealthy_project):
        """PAT-LOW-001 references INC-ORPHAN-001 which doesn't exist."""
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        xref_check = next(
            (c for c in report.checks if c.name == "cross_reference_validity"), None
        )
        assert xref_check is not None
        assert xref_check.status == "warning"

    def test_valid_references_pass(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        xref_check = next(
            (c for c in report.checks if c.name == "cross_reference_validity"), None
        )
        assert xref_check is not None
        assert xref_check.status == "ok"


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

class TestReflectionJSON:
    def test_json_serializable(self, healthy_project):
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        result = json.dumps(report.to_dict())
        parsed = json.loads(result)
        assert "summary" in parsed
        assert "stats" in parsed
        assert "checks" in parsed

    def test_check_has_recommendation(self, unhealthy_project):
        engine = ReflectionEngine(str(unhealthy_project), verbose=False)
        report = engine.reflect()
        warnings = [c for c in report.checks if c.status == "warning"]
        assert all(c.recommendation for c in warnings)


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: FRESHNESS
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionFreshness:
    def test_old_consolidation_warns(self, tmp_path):
        """Consolidation > 30 days ago should trigger warning."""
        root = tmp_path / "old_consol"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {
            "version": "1.0.0",
            "statistics": {
                "totals": {"episodes": 1, "patterns": 0, "heuristics": 0},
                "activity": {"last_consolidation": "2025-01-01"},
            },
        })
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {"patterns": []})
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {})
        _write_yaml(memory / "relational" / "associations.yaml", {})
        analyses = memory / "episodic" / "analyses"
        analyses.mkdir(parents=True, exist_ok=True)
        _write_yaml(analyses / "test.yaml", {"id": "ANL-20260301-001"})
        for sub in ["decisions", "incidents", "executions"]:
            (memory / "episodic" / sub).mkdir(parents=True, exist_ok=True)

        engine = ReflectionEngine(str(root), verbose=False,
                                  reference_date=REFERENCE_DATE)
        report = engine.reflect()
        consol_check = next((c for c in report.checks if c.name == "consolidation_recency"), None)
        assert consol_check is not None
        assert consol_check.status == "warning"


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

def _make_reflection_project(tmp_path, patterns=None, heuristics=None,
                              episodes_consolidated=0, episodes_total=0,
                              last_consolidation=None, associations_count=0,
                              episode_files=None, projects_count=0):
    """Helper to build a custom project for precise BVA."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "statistics": {
            "totals": {"episodes": episodes_total, "patterns": len(patterns or []),
                       "heuristics": len(heuristics or [])},
            "activity": {"last_consolidation": last_consolidation},
        },
    })
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "patterns": patterns or [],
    })
    if heuristics:
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
            "test_selection": heuristics,
        })
    else:
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {})

    _write_yaml(memory / "relational" / "associations.yaml", {
        "component_coupling": {"couplings": [{"component_a": "a", "component_b": "b"}] * associations_count}
    } if associations_count > 0 else {})

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

    if episode_files:
        for sub, filename, data in episode_files:
            _write_yaml(memory / "episodic" / sub / filename, data)

    return root


class TestReflectionBVALowConfidence:
    """BVA sur LOW_CONFIDENCE = 0.40 : condition `c < 0.40`."""

    @pytest.mark.parametrize("confidence,expected_low_count", [
        (0.39, 1),  # 0.39 < 0.40 → counted as low
        (0.40, 0),  # 0.40 == 0.40 → NOT counted (strict <)
        (0.41, 0),  # 0.41 > 0.40 → NOT counted
    ], ids=["0.39-low", "0.40-boundary-not-low", "0.41-not-low"])
    def test_low_confidence_boundary(self, tmp_path, confidence, expected_low_count):
        """BVA triplet around LOW_CONFIDENCE=0.40."""
        root = _make_reflection_project(tmp_path, patterns=[
            {"id": f"PAT-{int(confidence*100)}", "confidence": confidence,
             "last_seen": days_ago(1), "source_episodes": [],
             "category": "logic", "occurrences": 0},
        ])
        engine = ReflectionEngine(str(root), verbose=False, reference_date=REFERENCE_DATE)
        report = engine.reflect()
        assert report.stats.low_confidence_patterns == expected_low_count


class TestReflectionBVAConsolidationLag:
    """BVA sur CONSOLIDATION_LAG_THRESHOLD = 5 : condition `>= 5`."""

    @pytest.mark.parametrize("count,expected_status", [
        (4, "ok"),       # 4 < 5 → no backlog warning
        (5, "warning"),  # 5 >= 5 → backlog warning triggered
        (6, "warning"),  # 6 >= 5 → backlog warning triggered
    ], ids=["4-ok", "5-boundary-warning", "6-warning"])
    def test_consolidation_lag_boundary(self, tmp_path, count, expected_status):
        """BVA triplet around CONSOLIDATION_LAG_THRESHOLD=5."""
        episodes = []
        for i in range(count):
            episodes.append(("analyses", f"anl_{i}.yaml", {
                "id": f"ANL-20260301-{i:03d}",
                "quick_summary": {"one_liner": f"test {i}", "status": "ok", "next_action": "n"},
            }))
        root = _make_reflection_project(
            tmp_path, episodes_total=count, episode_files=episodes,
        )
        engine = ReflectionEngine(str(root), verbose=False)
        report = engine.reflect()
        backlog = next((c for c in report.checks if c.name == "consolidation_backlog"), None)
        assert backlog is not None
        assert backlog.status == expected_status


class TestReflectionBVAStaleness:
    """BVA sur stale consolidation : condition `days_ago > 30`."""

    @pytest.mark.parametrize("days,expected_status", [
        (29, "ok"),       # 29 <= 30 → not stale
        (30, "ok"),       # 30 == 30, strict > 30 → not stale
        (31, "warning"),  # 31 > 30 → stale
    ], ids=["29d-ok", "30d-boundary-ok", "31d-stale"])
    def test_staleness_boundary(self, tmp_path, days, expected_status):
        """BVA triplet around consolidation staleness threshold (>30 days)."""
        ref = REFERENCE_DATE
        consol_date = (ref - timedelta(days=days)).strftime("%Y-%m-%d")
        root = _make_reflection_project(
            tmp_path,
            last_consolidation=consol_date,
            episodes_total=1,
            episode_files=[("analyses", "a.yaml", {"id": "ANL-001"})],
        )
        engine = ReflectionEngine(str(root), verbose=False, reference_date=ref)
        report = engine.reflect()
        check = next((c for c in report.checks if c.name == "consolidation_recency"), None)
        assert check is not None
        assert check.status == expected_status


# ═══════════════════════════════════════════════════════════════════════════════
#  P0 — Error Guessing: Corrupt YAML and edge cases
# ═══════════════════════════════════════════════════════════════════════════════

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

    def test_corrupt_patterns_file_no_crash(self, tmp_path):
        """Corrupt defect_patterns.yaml should not crash."""
        root = _make_reflection_project(tmp_path)
        (root / "memory" / "semantic" / "defect_patterns.yaml").write_text(
            "{{INVALID", encoding="utf-8"
        )
        engine = ReflectionEngine(str(root), verbose=False)
        report = engine.reflect()
        assert isinstance(report, ReflectionReport)

    def test_unparseable_consolidation_date(self, tmp_path):
        """Invalid consolidation date string triggers warning."""
        root = _make_reflection_project(
            tmp_path,
            last_consolidation="not-a-date",
            episodes_total=1,
            episode_files=[("analyses", "a.yaml", {"id": "ANL-001"})],
        )
        engine = ReflectionEngine(str(root), verbose=False, reference_date=REFERENCE_DATE)
        report = engine.reflect()
        check = next((c for c in report.checks if c.name == "consolidation_recency"), None)
        assert check is not None
        assert check.status == "warning"
        assert "Cannot parse" in check.message


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — EP: UNHEALTHY status (error_count > 0)
# ═══════════════════════════════════════════════════════════════════════════════

class TestReflectionUnhealthyStatus:
    """Test ISTQB EP: the UNHEALTHY state (error_count > 0) partition."""

    def test_error_check_produces_unhealthy(self, healthy_project):
        """Injecting an error-level HealthCheck should produce UNHEALTHY."""
        engine = ReflectionEngine(str(healthy_project), verbose=False)
        report = engine.reflect()
        # Manually inject an error-level check to verify UNHEALTHY path
        report.checks.append(HealthCheck(
            name="injected_error",
            category="test",
            status="error",
            message="Simulated error for testing",
        ))
        d = report.to_dict()
        assert d["summary"]["health"] == "UNHEALTHY"
        assert d["summary"]["errors"] == 1


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

class TestReflectionCLI:
    """Tests ISTQB pour l'interface CLI du moteur de réflexion."""

    def test_cli_exit_0_healthy(self, tmp_path):
        """CLI on healthy project exits with code 0."""
        # Build a project with all episode types and recent consolidation
        root = _make_reflection_project(
            tmp_path,
            patterns=[
                {"id": "PAT-A", "confidence": 0.90, "last_seen": days_ago(1),
                 "source_episodes": ["ANL-001"], "category": "a", "occurrences": 1},
                {"id": "PAT-B", "confidence": 0.85, "last_seen": days_ago(1),
                 "source_episodes": [], "category": "b", "occurrences": 0},
                {"id": "PAT-C", "confidence": 0.80, "last_seen": days_ago(1),
                 "source_episodes": [], "category": "c", "occurrences": 0},
            ],
            heuristics=[{"id": "HEU-001", "confidence": 0.90}],
            episodes_total=4,
            last_consolidation="2026-02-28",
            associations_count=3,
            episode_files=[
                ("analyses", "a.yaml", {"id": "ANL-001", "consolidation": {"consolidated": True}}),
                ("incidents", "i.yaml", {"id": "INC-001", "consolidation": {"consolidated": True}}),
                ("decisions", "d.yaml", {"id": "DEC-001", "consolidation": {"consolidated": True}}),
                ("executions", "e.yaml", {"id": "EXE-001", "consolidation": {"consolidated": True}}),
            ],
        )
        result = subprocess.run(
            [sys.executable, "reflection.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_exit_1_with_issues(self, unhealthy_project):
        """CLI on unhealthy project exits with code 1."""
        result = subprocess.run(
            [sys.executable, "reflection.py", "--base-dir", str(unhealthy_project),
             "--quiet"],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        assert result.returncode == 1

    def test_cli_json_output_valid(self, healthy_project):
        """CLI --json produces valid parseable JSON."""
        result = subprocess.run(
            [sys.executable, "reflection.py", "--base-dir", str(healthy_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 "stats" in parsed
        assert "checks" in parsed
