#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Tests pour l'Orchestrateur de Cycle Cognitif
═══════════════════════════════════════════════════════════════════════════════
"""

import pytest
import yaml
import json
import subprocess
import sys
from pathlib import Path
from datetime import datetime
from conftest import REFERENCE_DATE, days_ago
from unittest.mock import patch
from cognitive_cycle import (
    CognitiveCycle, CycleReport, PhaseResult,
)


# ═══════════════════════════════════════════════════════════════════════════════
#  HELPERS
# ═══════════════════════════════════════════════════════════════════════════════

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"))


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

@pytest.fixture
def healthy_project(tmp_path):
    """Argus project in healthy state — no consolidation needed, no decay, good reflection."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    # _index.yaml
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "last_updated": "2026-02-28",
        "episodic": {
            "analyses": {"count": 1, "latest": "ANL-20260228-001", "path": "memory/episodic/analyses/"},
            "decisions": {"count": 0, "latest": None, "path": "memory/episodic/decisions/"},
            "incidents": {"count": 0, "latest": None, "path": "memory/episodic/incidents/"},
            "executions": {"count": 0, "latest": None, "path": "memory/episodic/executions/"},
        },
        "semantic": {
            "defect_patterns": {"count": 2, "path": "memory/semantic/defect_patterns.yaml", "categories": ["null_safety", "async"]},
            "testing_heuristics": {"count": 1, "path": "memory/semantic/testing_heuristics.yaml", "categories": ["selection"]},
        },
        "projects": {
            "count": 1,
            "path": "memory/projects/",
            "registered": [{"id": "argus", "name": "Argus", "file": "memory/projects/argus.yaml"}],
        },
        "relational": {
            "path": "memory/relational/associations.yaml",
            "total_associations": 1,
            "relation_types": ["file_defects"],
        },
        "statistics": {
            "memory_initialized": "2026-02-15",
            "totals": {"episodes": 1, "patterns": 2, "heuristics": 1, "projects": 1, "associations": 1},
            "activity": {
                "last_consolidation": "2026-02-28",
                "last_episode_recorded": "2026-02-28",
            },
        },
        "sequences": {"analysis": 1, "decision": 0, "incident": 0, "execution": 0, "pattern": 2, "heuristic": 1},
        "next_ids": {
            "analysis": "ANL-20260301-001",
            "decision": "DEC-20260301-001",
            "incident": "INC-20260301-001",
            "execution": "EXE-20260301-001",
        },
    })

    # Patterns (all recent, good confidence)
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "version": "1.0.0",
        "patterns": [
            {
                "id": "PAT-NULL-001",
                "name": "Null Reference",
                "category": "null_safety",
                "confidence": 0.85,
                "occurrences": 2,
                "last_seen": days_ago(1),
                "source_episodes": ["ANL-20260228-001"],
            },
            {
                "id": "PAT-ASYNC-001",
                "name": "Race Condition",
                "category": "async",
                "confidence": 0.80,
                "occurrences": 1,
                "last_seen": days_ago(9),
                "source_episodes": [],
            },
        ],
    })

    # Heuristics
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
        "version": "1.0.0",
        "test_selection": [
            {"id": "HEU-SEL-001", "name": "Test What Changed", "confidence": 0.90,
             "last_seen": days_ago(4), "occurrences": 3},
        ],
    })

    # One consolidated episode
    _write_yaml(memory / "episodic" / "analyses" / "2026-02-28_10-00_analysis_ANL-20260228-001.yaml", {
        "metadata": {
            "id": "ANL-20260228-001",
            "type": "analysis",
            "created_at": "2026-02-28T10:00:00Z",
        },
        "findings": [
            {"description": "Null check missing", "severity": "medium"},
        ],
        "consolidation": {"consolidated": True, "consolidated_at": "2026-02-28"},
    })

    # Relational memory
    _write_yaml(memory / "relational" / "associations.yaml", {
        "version": "1.0.0",
        "associations": [
            {"type": "file_defects", "source": "src/auth.ts", "target": "PAT-NULL-001"},
        ],
    })

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

    # Working dir
    (memory / "working").mkdir(parents=True, exist_ok=True)

    return root


@pytest.fixture
def project_with_pending_work(tmp_path):
    """Project with unconsolidated episodes, old patterns, and issues."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "last_updated": "2026-01-15",
        "episodic": {
            "analyses": {"count": 2, "latest": "ANL-20260215-002", "path": "memory/episodic/analyses/"},
            "decisions": {"count": 0, "latest": None, "path": "memory/episodic/decisions/"},
            "incidents": {"count": 1, "latest": "INC-20260215-001", "path": "memory/episodic/incidents/"},
            "executions": {"count": 0, "latest": None, "path": "memory/episodic/executions/"},
        },
        "semantic": {
            "defect_patterns": {"count": 2, "path": "memory/semantic/defect_patterns.yaml", "categories": ["null_safety", "async"]},
            "testing_heuristics": {"count": 1, "path": "memory/semantic/testing_heuristics.yaml", "categories": ["selection"]},
        },
        "projects": {
            "count": 1,
            "path": "memory/projects/",
            "registered": [{"id": "argus", "name": "Argus", "file": "memory/projects/argus.yaml"}],
        },
        "relational": {
            "path": "memory/relational/associations.yaml",
            "total_associations": 0,
            "relation_types": [],
        },
        "statistics": {
            "memory_initialized": "2026-01-01",
            "totals": {"episodes": 3, "patterns": 2, "heuristics": 1, "projects": 1, "associations": 0},
            "activity": {
                "last_consolidation": None,
                "last_episode_recorded": "2026-02-15",
            },
        },
        "sequences": {"analysis": 2, "decision": 0, "incident": 1, "execution": 0, "pattern": 2, "heuristic": 1},
        "next_ids": {
            "analysis": "ANL-20260301-001",
            "decision": "DEC-20260301-001",
            "incident": "INC-20260301-001",
            "execution": "EXE-20260301-001",
        },
    })

    # Patterns with an old one
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
        "version": "1.0.0",
        "patterns": [
            {
                "id": "PAT-NULL-001",
                "name": "Null Reference",
                "category": "null_safety",
                "confidence": 0.80,
                "occurrences": 0,
                "last_seen": None,
                "source_episodes": [],
            },
            {
                "id": "PAT-ASYNC-001",
                "name": "Race Condition",
                "category": "async",
                "confidence": 0.50,
                "occurrences": 0,
                "last_seen": days_ago(425),
                "source_episodes": [],
            },
        ],
    })

    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
        "version": "1.0.0",
        "test_selection": [
            {"id": "HEU-SEL-001", "name": "Test What Changed", "confidence": 0.90},
        ],
    })

    # Unconsolidated analysis referencing PAT-NULL-001
    _write_yaml(memory / "episodic" / "analyses" / "2026-02-15_14-30_analysis_ANL-20260215-001.yaml", {
        "metadata": {
            "id": "ANL-20260215-001",
            "type": "analysis",
            "created_at": "2026-02-15T14:30:00Z",
        },
        "findings": [
            {"description": "Found PAT-NULL-001 in auth module", "severity": "high"},
        ],
        "consolidation": {"consolidated": False},
    })

    # Unconsolidated incident matching a pattern
    _write_yaml(memory / "episodic" / "incidents" / "2026-02-15_16-00_incident_INC-20260215-001.yaml", {
        "metadata": {
            "id": "INC-20260215-001",
            "type": "incident",
            "created_at": "2026-02-15T16:00:00Z",
            "severity": "P2",
        },
        "matched_patterns": ["PAT-NULL-001"],
        "consolidation": {"consolidated": False},
    })

    # Relational empty
    _write_yaml(memory / "relational" / "associations.yaml", {
        "version": "1.0.0",
        "associations": [],
    })

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

    (memory / "working").mkdir(parents=True, exist_ok=True)

    return root


@pytest.fixture
def empty_project(tmp_path):
    """Bare minimum Argus project — no episodes, no patterns."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "last_updated": "2026-03-01",
        "episodic": {
            "analyses": {"count": 0, "latest": None, "path": "memory/episodic/analyses/"},
            "decisions": {"count": 0, "latest": None, "path": "memory/episodic/decisions/"},
            "incidents": {"count": 0, "latest": None, "path": "memory/episodic/incidents/"},
            "executions": {"count": 0, "latest": None, "path": "memory/episodic/executions/"},
        },
        "statistics": {
            "memory_initialized": "2026-03-01",
            "totals": {"episodes": 0, "patterns": 0, "heuristics": 0, "projects": 0, "associations": 0},
            "activity": {"last_consolidation": None},
        },
        "sequences": {"analysis": 0, "decision": 0, "incident": 0, "execution": 0, "pattern": 0, "heuristic": 0},
        "next_ids": {
            "analysis": "ANL-20260301-001",
            "decision": "DEC-20260301-001",
            "incident": "INC-20260301-001",
            "execution": "EXE-20260301-001",
        },
    })

    (memory / "semantic").mkdir(parents=True, exist_ok=True)
    (memory / "episodic" / "analyses").mkdir(parents=True, exist_ok=True)
    (memory / "episodic" / "incidents").mkdir(parents=True, exist_ok=True)
    (memory / "episodic" / "decisions").mkdir(parents=True, exist_ok=True)
    (memory / "episodic" / "executions").mkdir(parents=True, exist_ok=True)
    (memory / "relational").mkdir(parents=True, exist_ok=True)
    (memory / "projects").mkdir(parents=True, exist_ok=True)
    (memory / "working").mkdir(parents=True, exist_ok=True)

    return root


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Full Cycle on Healthy Project
# ═══════════════════════════════════════════════════════════════════════════════

class TestFullCycleHealthy:

    def test_full_cycle_no_actions(self, healthy_project):
        """A healthy project with consolidated episodes should have no consolidation/decay actions."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()

        assert len(report.phases_executed) == 3
        assert report.phases_requested == ["consolidation", "decay", "reflection"]

        # Consolidation and decay should be clean
        assert report.phases_executed[0].status == "ok"  # consolidation
        assert report.phases_executed[1].status == "ok"  # decay
        # Reflection may find completeness warnings (sparse associations, missing episode types)
        # which is correct behavior for a minimal project
        assert report.phases_executed[2].status in ("ok", "issues_found")

    def test_all_phases_executed_in_order(self, healthy_project):
        """Phases must execute in correct order."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()

        phases = [p.phase for p in report.phases_executed]
        assert phases == ["consolidation", "decay", "reflection"]

    def test_mode_is_scan_by_default(self, healthy_project):
        """Default mode should be scan."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run()
        assert report.mode == "scan"

    def test_mode_is_apply_when_requested(self, healthy_project):
        """Apply mode should be recorded."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run(apply=True)
        assert report.mode == "apply"

    def test_duration_is_positive(self, healthy_project):
        """Total duration should be a positive number."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run()
        assert report.total_duration_ms > 0
        for p in report.phases_executed:
            assert p.duration_ms >= 0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Full Cycle Detects Pending Work
# ═══════════════════════════════════════════════════════════════════════════════

class TestFullCycleWithWork:

    def test_detects_consolidation_needed(self, project_with_pending_work):
        """Cycle should detect unconsolidated episodes."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()

        consolidation = report.phases_executed[0]
        assert consolidation.phase == "consolidation"
        assert consolidation.status == "actions_needed"
        assert report.consolidation_report is not None
        assert report.consolidation_report.has_actions

    def test_detects_decay_needed(self, project_with_pending_work):
        """Cycle should detect patterns needing decay."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()

        decay = report.phases_executed[1]
        assert decay.phase == "decay"
        assert decay.status == "actions_needed"
        assert report.decay_report is not None

    def test_detects_reflection_issues(self, project_with_pending_work):
        """Reflection should find warnings on an unhealthy project."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()

        reflection = report.phases_executed[2]
        assert reflection.phase == "reflection"
        assert reflection.status == "issues_found"

    def test_overall_status_not_healthy(self, project_with_pending_work):
        """Overall status should reflect pending work."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run()
        assert report.overall_status != "HEALTHY"
        assert report.has_actions


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Single Phase Execution
# ═══════════════════════════════════════════════════════════════════════════════

class TestSinglePhase:

    def test_run_consolidation_only(self, healthy_project):
        """Running a single phase should only execute that phase."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run(phases=["consolidation"])

        assert len(report.phases_executed) == 1
        assert report.phases_executed[0].phase == "consolidation"
        assert report.consolidation_report is not None
        assert report.decay_report is None
        assert report.reflection_report is None

    def test_run_decay_only(self, project_with_pending_work):
        """Decay-only should skip consolidation and reflection."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run(phases=["decay"])

        assert len(report.phases_executed) == 1
        assert report.phases_executed[0].phase == "decay"
        assert report.consolidation_report is None
        assert report.reflection_report is None

    def test_run_reflection_only(self, healthy_project):
        """Reflection-only mode."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run(phases=["reflection"])

        assert len(report.phases_executed) == 1
        assert report.phases_executed[0].phase == "reflection"
        assert report.reflection_report is not None

    def test_invalid_phase_caught(self, healthy_project):
        """Invalid phase name should be caught."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run(phases=["invalid_phase"])

        assert len(report.errors) > 0
        assert "Unknown phase" in report.errors[0]

    def test_custom_phase_order(self, healthy_project):
        """Phases should be executed in the order requested."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run(phases=["reflection", "consolidation"])

        phases = [p.phase for p in report.phases_executed]
        assert phases == ["reflection", "consolidation"]


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Apply Mode
# ═══════════════════════════════════════════════════════════════════════════════

class TestApplyMode:

    def test_apply_consolidation_modifies_patterns(self, project_with_pending_work):
        """Apply mode should actually reinforce pattern confidence."""
        root = project_with_pending_work
        cycle = CognitiveCycle(str(root), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run(apply=True)

        assert report.mode == "apply"

        # Check that patterns were actually modified
        patterns = _load_yaml(root / "memory" / "semantic" / "defect_patterns.yaml")
        pat_null = next(p for p in patterns["patterns"] if p["id"] == "PAT-NULL-001")
        # Confidence should have been boosted from 0.80
        assert pat_null["confidence"] > 0.80

    def test_apply_then_rescan_shows_clean(self, project_with_pending_work):
        """After apply, a rescan of consolidation should show no pending work."""
        root = project_with_pending_work
        cycle1 = CognitiveCycle(str(root), verbose=False,
                                reference_date=datetime(2026, 3, 1))
        cycle1.run(phases=["consolidation"], apply=True)

        # Second scan should find nothing to consolidate
        cycle2 = CognitiveCycle(str(root), verbose=False,
                                reference_date=datetime(2026, 3, 1))
        report2 = cycle2.run(phases=["consolidation"])

        consolidation = report2.phases_executed[0]
        assert consolidation.status == "ok"

    def test_apply_decay_modifies_confidence(self, project_with_pending_work):
        """Apply decay should lower old pattern confidence."""
        root = project_with_pending_work
        cycle = CognitiveCycle(str(root), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        report = cycle.run(phases=["decay"], apply=True)

        patterns = _load_yaml(root / "memory" / "semantic" / "defect_patterns.yaml")
        pat_async = next(p for p in patterns["patterns"] if p["id"] == "PAT-ASYNC-001")
        # Old pattern should have been decayed from 0.50
        assert pat_async["confidence"] < 0.50


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Empty Project
# ═══════════════════════════════════════════════════════════════════════════════

class TestEmptyProject:

    def test_empty_project_completes(self, empty_project):
        """Empty project should complete without errors."""
        cycle = CognitiveCycle(str(empty_project), verbose=False)
        report = cycle.run()

        assert len(report.phases_executed) == 3
        for p in report.phases_executed:
            assert p.status != "error", f"{p.phase} errored: {p.error}"

    def test_empty_project_no_crash(self, empty_project):
        """Apply on empty project should not crash."""
        cycle = CognitiveCycle(str(empty_project), verbose=False)
        report = cycle.run(apply=True)

        assert len(report.phases_executed) == 3
        for p in report.phases_executed:
            assert p.status != "error", f"{p.phase} errored: {p.error}"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Working Session Integration
# ═══════════════════════════════════════════════════════════════════════════════

class TestSessionIntegration:

    def test_create_session_record(self, healthy_project):
        """Session record should be a valid dict."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        cycle.run()

        record = cycle.create_session_record("SES-20260301-001")
        assert record["session_id"] == "SES-20260301-001"
        assert record["status"] == "active"
        assert record["context"]["trigger"] == "scheduled"
        assert isinstance(record["knowledge_activated"]["patterns_consulted"], list)
        assert isinstance(record["archival"]["consolidation_triggers"], list)

    def test_save_session_creates_file(self, healthy_project):
        """save_session should create a YAML file in memory/working/."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        cycle.run()

        filepath = cycle.save_session("SES-20260301-001")
        assert filepath.exists()
        assert filepath.name == "ses_20260301_001.yaml"
        assert filepath.parent.name == "working"

        data = _load_yaml(filepath)
        assert data["session_id"] == "SES-20260301-001"

    def test_session_captures_reinforcements(self, project_with_pending_work):
        """Session record should list patterns consulted during consolidation."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        cycle.run()

        record = cycle.create_session_record()
        # PAT-NULL-001 was reinforced by the incident and analysis
        assert len(record["knowledge_activated"]["patterns_consulted"]) > 0

    def test_session_captures_new_pattern_candidates(self, project_with_pending_work):
        """If new pattern candidates exist, they should appear in archival."""
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        cycle.run()

        record = cycle.create_session_record()
        # archival.patterns_observed should exist as a list
        assert isinstance(record["archival"]["patterns_observed"], list)


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: JSON Serialization
# ═══════════════════════════════════════════════════════════════════════════════

class TestJSON:

    def test_report_to_dict(self, healthy_project):
        """Report should be JSON-serializable."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run()

        d = report.to_dict()
        serialized = json.dumps(d, indent=2, ensure_ascii=False)
        reloaded = json.loads(serialized)
        assert reloaded["overall_status"] in ("HEALTHY", "ISSUES_FOUND")
        assert len(reloaded["phases"]) == 3

    def test_report_includes_detail_sections(self, healthy_project):
        """Full cycle report should include detail sections."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run()

        d = report.to_dict()
        assert "consolidation_detail" in d
        assert "decay_detail" in d
        assert "reflection_detail" in d

    def test_single_phase_json_omits_others(self, healthy_project):
        """Single-phase report should only include that phase's details."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        report = cycle.run(phases=["consolidation"])

        d = report.to_dict()
        assert "consolidation_detail" in d
        assert "decay_detail" not in d
        assert "reflection_detail" not in d


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: Reference Date Propagation
# ═══════════════════════════════════════════════════════════════════════════════

class TestReferenceDate:

    def test_reference_date_propagated_to_decay(self, project_with_pending_work):
        """Reference date should propagate to the decay engine."""
        ref = datetime(2027, 6, 1)  # Far in the future
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=ref)
        report = cycle.run(phases=["decay"])

        assert report.decay_report is not None
        assert report.decay_report.reference_date == "2027-06-01"

    def test_reference_date_propagated_to_reflection(self, project_with_pending_work):
        """Reference date should propagate to the reflection engine."""
        ref = datetime(2027, 6, 1)
        cycle = CognitiveCycle(str(project_with_pending_work), verbose=False,
                               reference_date=ref)
        report = cycle.run(phases=["reflection"])

        assert report.reflection_report is not None


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: PhaseResult Dataclass
# ═══════════════════════════════════════════════════════════════════════════════

class TestPhaseResult:

    def test_to_dict_basic(self):
        """PhaseResult.to_dict() should produce correct structure."""
        pr = PhaseResult(phase="consolidation", status="ok", duration_ms=42.5)
        d = pr.to_dict()
        assert d["phase"] == "consolidation"
        assert d["status"] == "ok"
        assert d["duration_ms"] == 42.5
        assert "error" not in d

    def test_to_dict_with_error(self):
        """Error field should be included when present."""
        pr = PhaseResult(phase="decay", status="error", error="Something broke")
        d = pr.to_dict()
        assert d["error"] == "Something broke"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: CycleReport Dataclass
# ═══════════════════════════════════════════════════════════════════════════════

class TestCycleReport:

    @pytest.mark.parametrize("statuses,expected_overall,expected_has_actions", [
        (["ok", "ok", "ok"], "HEALTHY", False),
        (["actions_needed", "ok", "ok"], "ACTIONS_NEEDED", True),
        (["actions_needed", "ok", "issues_found"], "ISSUES_FOUND", True),
        (["error", "ok", "issues_found"], "ERROR", True),
    ], ids=["all-ok-healthy", "actions-needed", "issues-found-precedence", "error-precedence"])
    def test_overall_status(self, statuses, expected_overall, expected_has_actions):
        """Verify status precedence: ERROR > ISSUES_FOUND > ACTIONS_NEEDED > HEALTHY."""
        report = CycleReport()
        report.phases_executed = [
            PhaseResult(phase=phase, status=status)
            for phase, status in zip(["consolidation", "decay", "reflection"], statuses)
        ]
        assert report.overall_status == expected_overall
        assert report.has_actions == expected_has_actions

    def test_total_duration(self):
        """Total duration is sum of phases."""
        report = CycleReport()
        report.phases_executed = [
            PhaseResult(phase="consolidation", status="ok", duration_ms=10.0),
            PhaseResult(phase="decay", status="ok", duration_ms=20.0),
            PhaseResult(phase="reflection", status="ok", duration_ms=30.0),
        ]
        assert report.total_duration_ms == 60.0


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — EP: Engine exception → status="error"
# ═══════════════════════════════════════════════════════════════════════════════

class TestEngineException:
    """Tests ISTQB EP: when an engine crashes, the phase captures the error."""

    @pytest.mark.parametrize("engine_class,phase_name,error_msg", [
        ("ConsolidationEngine", "consolidation", "Engine exploded"),
        ("DecayEngine", "decay", "Decay crashed"),
        ("ReflectionEngine", "reflection", "Reflect failed"),
    ], ids=["consolidation-crash", "decay-crash", "reflection-crash"])
    def test_engine_exception_captured(self, healthy_project, engine_class, phase_name, error_msg):
        """If an engine raises RuntimeError, phase status should be 'error'."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        with patch(f"cognitive_cycle.{engine_class}") as mock_cls:
            mock_cls.side_effect = RuntimeError(error_msg)
            report = cycle.run(phases=[phase_name])

        assert report.phases_executed[0].status == "error"
        assert error_msg in report.phases_executed[0].error

    def test_phase1_error_phase2_still_runs(self, healthy_project):
        """If consolidation errors, decay and reflection should still execute."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False,
                               reference_date=datetime(2026, 3, 1))
        with patch("cognitive_cycle.ConsolidationEngine") as mock_cls:
            mock_cls.side_effect = RuntimeError("Boom")
            report = cycle.run()

        phases = [p.phase for p in report.phases_executed]
        assert phases == ["consolidation", "decay", "reflection"]
        assert report.phases_executed[0].status == "error"
        assert report.phases_executed[1].status != "error"
        assert report.phases_executed[2].status != "error"


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — EP: --dry-run override and save_session failure
# ═══════════════════════════════════════════════════════════════════════════════

class TestDryRunAndSessionFailure:

    def test_save_session_failure_no_crash(self, healthy_project):
        """save_session on non-writable dir should raise, CLI catches it."""
        cycle = CognitiveCycle(str(healthy_project), verbose=False)
        cycle.run()
        # Make working dir a file to prevent directory creation
        working = healthy_project / "memory" / "working"
        if working.exists():
            import shutil
            shutil.rmtree(working)
        working.parent.mkdir(parents=True, exist_ok=True)
        working.write_text("blocker", encoding="utf-8")  # file, not dir

        # save_session should raise (can't mkdir on a file)
        with pytest.raises(Exception):
            cycle.save_session()


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — CLI: main() exit codes, --dry-run, JSON output
# ═══════════════════════════════════════════════════════════════════════════════

class TestCognitiveCycleCLI:
    """Tests ISTQB pour l'interface CLI de l'orchestrateur."""

    def test_cli_scan_exit_0_healthy(self, tmp_path):
        """CLI scan on truly healthy project (all episode types) exits code 0."""
        # The healthy_project fixture has missing episode types which triggers
        # reflection warnings. Build a complete project here.
        root = tmp_path / "argus_cli"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {
            "version": "1.0.0",
            "statistics": {
                "totals": {"episodes": 4, "patterns": 3, "heuristics": 1},
                "activity": {"last_consolidation": "2026-02-28"},
            },
        })
        _write_yaml(memory / "semantic" / "defect_patterns.yaml", {
            "patterns": [
                {"id": "PAT-A", "category": "a", "confidence": 0.90,
                 "occurrences": 1, "last_seen": days_ago(1), "source_episodes": ["ANL-001"]},
                {"id": "PAT-B", "category": "b", "confidence": 0.85,
                 "occurrences": 0, "last_seen": days_ago(1), "source_episodes": []},
                {"id": "PAT-C", "category": "c", "confidence": 0.80,
                 "occurrences": 0, "last_seen": days_ago(1), "source_episodes": []},
            ],
        })
        _write_yaml(memory / "semantic" / "testing_heuristics.yaml", {
            "test_selection": [{"id": "HEU-001", "confidence": 0.90,
                                "last_seen": days_ago(4), "occurrences": 1}],
        })
        _write_yaml(memory / "relational" / "associations.yaml", {
            "component_coupling": {"couplings": [
                {"component_a": "a", "component_b": "b"},
                {"component_a": "c", "component_b": "d"},
                {"component_a": "e", "component_b": "f"},
            ]},
        })
        for sub, prefix in [("analyses", "ANL"), ("incidents", "INC"),
                            ("decisions", "DEC"), ("executions", "EXE")]:
            d = memory / "episodic" / sub
            d.mkdir(parents=True, exist_ok=True)
            _write_yaml(d / f"ep_{sub}.yaml", {
                "id": f"{prefix}-001",
                "consolidation": {"consolidated": True},
            })
        (memory / "working").mkdir(parents=True, exist_ok=True)

        result = subprocess.run(
            [sys.executable, "cognitive_cycle.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_actions(self, project_with_pending_work):
        """CLI scan with pending work exits with code 1."""
        result = subprocess.run(
            [sys.executable, "cognitive_cycle.py",
             "--base-dir", str(project_with_pending_work), "--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, healthy_project):
        """CLI --json produces valid parseable JSON."""
        result = subprocess.run(
            [sys.executable, "cognitive_cycle.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 "overall_status" in parsed
        assert "phases" in parsed

    def test_cli_dry_run_overrides_apply(self, project_with_pending_work):
        """--apply --dry-run should NOT modify files (dry-run wins)."""
        root = project_with_pending_work
        patterns_before = _load_yaml(root / "memory" / "semantic" / "defect_patterns.yaml")
        conf_before = patterns_before["patterns"][0]["confidence"]

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

        patterns_after = _load_yaml(root / "memory" / "semantic" / "defect_patterns.yaml")
        conf_after = patterns_after["patterns"][0]["confidence"]
        assert conf_before == conf_after  # Files untouched

    def test_cli_single_phase(self, healthy_project):
        """CLI --phase consolidation runs only one phase."""
        result = subprocess.run(
            [sys.executable, "cognitive_cycle.py",
             "--base-dir", str(healthy_project), "--json",
             "--phase", "consolidation"],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        parsed = json.loads(result.stdout)
        assert len(parsed["phases"]) == 1
        assert parsed["phases"][0]["phase"] == "consolidation"
