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

import pytest
import yaml
import json
import subprocess
import sys
from pathlib import Path
from consolidation import (
    ConsolidationEngine, ConsolidationReport,
    CONFIDENCE_BOOST,
)


# ═══════════════════════════════════════════════════════════════════════════════
#  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 project_with_patterns(tmp_path):
    """Creates a minimal Argus project with patterns and heuristics."""
    root = tmp_path / "argus"
    root.mkdir()
    memory = root / "memory"

    # _index.yaml
    _write_yaml(memory / "_index.yaml", {
        "version": "1.0.0",
        "last_updated": "2026-02-15",
        "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": {
            "totals": {"episodes": 0, "patterns": 2, "heuristics": 1},
            "activity": {"last_consolidation": None},
        },
        "sequences": {"analysis": 0, "decision": 0, "incident": 0, "execution": 0},
    })

    # Semantic: patterns
    _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": "concurrency",
                "confidence": 0.75,
                "occurrences": 0,
                "last_seen": None,
                "source_episodes": [],
            },
        ],
    })

    # Semantic: 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,
            },
        ],
    })

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

    return root


@pytest.fixture
def project_with_incident(project_with_patterns):
    """Adds an incident referencing PAT-NULL-001."""
    root = project_with_patterns
    memory = root / "memory"
    _write_yaml(
        memory / "episodic" / "incidents" / "2026-03-01_inc_INC-20260301-001.yaml",
        {
            "id": "INC-20260301-001",
            "severity": "P2",
            "category": "regression",
            "affected_components": ["authentication"],
            "root_cause": {
                "identified": True,
                "description": "Null pointer on optional user field",
                "matched_patterns": ["PAT-NULL-001"],
                "new_pattern_candidate": False,
            },
        },
    )
    return root


@pytest.fixture
def project_with_analysis(project_with_patterns):
    """Adds an analysis referencing PAT-ASYNC-001."""
    root = project_with_patterns
    memory = root / "memory"
    _write_yaml(
        memory / "episodic" / "analyses" / "2026-03-01_analysis_ANL-20260301-001.yaml",
        {
            "id": "ANL-20260301-001",
            "quick_summary": {"one_liner": "test", "status": "COMPLETED", "next_action": "none"},
            "findings": [
                {
                    "risk_level": "high",
                    "description": "Race condition detected — see PAT-ASYNC-001",
                },
            ],
        },
    )
    return root


@pytest.fixture
def project_with_decision(project_with_patterns):
    """Adds a decision using HEU-SEL-001 and considering PAT-NULL-001."""
    root = project_with_patterns
    memory = root / "memory"
    _write_yaml(
        memory / "episodic" / "decisions" / "2026-03-01_dec_DEC-20260301-001.yaml",
        {
            "id": "DEC-20260301-001",
            "knowledge_applied": {
                "patterns_considered": ["PAT-NULL-001"],
                "heuristics_used": ["HEU-SEL-001"],
                "istqb_techniques": [],
            },
            "decision": {"chosen_option": "OPT-A"},
        },
    )
    return root


@pytest.fixture
def project_already_consolidated(project_with_incident):
    """All episodes are already consolidated."""
    root = project_with_incident
    inc_path = root / "memory" / "episodic" / "incidents" / "2026-03-01_inc_INC-20260301-001.yaml"
    data = _load_yaml(inc_path)
    data["consolidation"] = {"consolidated": True, "consolidated_at": "2026-03-01T00:00:00"}
    _write_yaml(inc_path, data)
    return root


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: SCAN — No episodes
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationScanEmpty:
    def test_no_episodes(self, project_with_patterns):
        engine = ConsolidationEngine(str(project_with_patterns), verbose=False)
        report = engine.scan()
        assert report.episodes_scanned == 0
        assert not report.has_actions

    def test_all_consolidated(self, project_already_consolidated):
        engine = ConsolidationEngine(str(project_already_consolidated), verbose=False)
        report = engine.scan()
        assert report.episodes_scanned == 1
        assert report.episodes_unconsolidated == 0
        assert not report.has_actions


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: SCAN — Incidents
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationScanIncident:
    def test_incident_pattern_reinforcement(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        report = engine.scan()
        assert report.episodes_unconsolidated == 1
        pat_reinforcements = [r for r in report.reinforcements if r.target_id == "PAT-NULL-001"]
        assert len(pat_reinforcements) >= 1
        assert pat_reinforcements[0].new_occurrence is True

    def test_incident_association_discovery(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        report = engine.scan()
        assert len(report.associations_found) >= 1
        assert report.associations_found[0]["type"] == "component_to_defect"

    def test_report_to_dict(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        report = engine.scan()
        d = report.to_dict()
        assert d["summary"]["status"] == "ACTIONS_NEEDED"
        assert d["summary"]["reinforcements"] >= 1


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: SCAN — Analyses
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationScanAnalysis:
    def test_analysis_pattern_reference(self, project_with_analysis):
        engine = ConsolidationEngine(str(project_with_analysis), verbose=False)
        report = engine.scan()
        async_refs = [r for r in report.reinforcements if r.target_id == "PAT-ASYNC-001"]
        assert len(async_refs) >= 1

    def test_unknown_pattern_ignored(self, project_with_patterns):
        """References to unknown pattern IDs should not create reinforcements."""
        root = project_with_patterns
        _write_yaml(
            root / "memory" / "episodic" / "analyses" / "test.yaml",
            {
                "id": "ANL-20260301-099",
                "quick_summary": {"one_liner": "test", "status": "COMPLETED", "next_action": "none"},
                "findings": [{"description": "See PAT-UNKNOWN-999"}],
            },
        )
        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        unknown_refs = [r for r in report.reinforcements if r.target_id == "PAT-UNKNOWN-999"]
        assert len(unknown_refs) == 0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: SCAN — Decisions
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationScanDecision:
    def test_decision_knowledge_applied(self, project_with_decision):
        engine = ConsolidationEngine(str(project_with_decision), verbose=False)
        report = engine.scan()
        pat_refs = [r for r in report.reinforcements if r.target_id == "PAT-NULL-001"]
        heu_refs = [r for r in report.reinforcements if r.target_id == "HEU-SEL-001"]
        assert len(pat_refs) >= 1
        assert len(heu_refs) >= 1


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

class TestConsolidationApply:
    def test_apply_reinforces_confidence(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        report = engine.apply()

        # Check pattern was reinforced
        patterns = _load_yaml(
            project_with_incident / "memory" / "semantic" / "defect_patterns.yaml"
        )
        pat = next(p for p in patterns["patterns"] if p["id"] == "PAT-NULL-001")
        assert pat["confidence"] > 0.80  # Was 0.80, should have increased
        assert pat["occurrences"] >= 1
        assert "INC-20260301-001" in pat["source_episodes"]
        assert pat["last_seen"] is not None

    def test_apply_marks_episode_consolidated(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        engine.apply()

        inc_path = (project_with_incident / "memory" / "episodic" / "incidents" /
                    "2026-03-01_inc_INC-20260301-001.yaml")
        inc = _load_yaml(inc_path)
        assert inc["consolidation"]["consolidated"] is True
        assert inc["consolidation"]["consolidated_at"] is not None

    def test_apply_updates_index(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        engine.apply()

        index = _load_yaml(project_with_incident / "memory" / "_index.yaml")
        assert index["statistics"]["activity"]["last_consolidation"] is not None

    def test_apply_idempotent(self, project_with_incident):
        """Running apply twice should not double-reinforce."""
        engine1 = ConsolidationEngine(str(project_with_incident), verbose=False)
        engine1.apply()

        patterns1 = _load_yaml(
            project_with_incident / "memory" / "semantic" / "defect_patterns.yaml"
        )
        conf1 = next(p for p in patterns1["patterns"] if p["id"] == "PAT-NULL-001")["confidence"]

        engine2 = ConsolidationEngine(str(project_with_incident), verbose=False)
        report2 = engine2.apply()
        assert not report2.has_actions  # Episode already consolidated

        patterns2 = _load_yaml(
            project_with_incident / "memory" / "semantic" / "defect_patterns.yaml"
        )
        conf2 = next(p for p in patterns2["patterns"] if p["id"] == "PAT-NULL-001")["confidence"]
        assert conf1 == conf2

    def test_confidence_capped_at_1(self, project_with_patterns):
        """Confidence should not exceed 1.0."""
        root = project_with_patterns
        # Set pattern confidence to 0.99
        patterns_path = root / "memory" / "semantic" / "defect_patterns.yaml"
        data = _load_yaml(patterns_path)
        data["patterns"][0]["confidence"] = 0.99
        _write_yaml(patterns_path, data)

        # Add incident
        _write_yaml(
            root / "memory" / "episodic" / "incidents" / "test_inc.yaml",
            {
                "id": "INC-20260301-099",
                "root_cause": {"matched_patterns": ["PAT-NULL-001"]},
            },
        )

        engine = ConsolidationEngine(str(root), verbose=False)
        engine.apply()

        updated = _load_yaml(patterns_path)
        conf = next(p for p in updated["patterns"] if p["id"] == "PAT-NULL-001")["confidence"]
        assert conf <= 1.0


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: NEW PATTERN CANDIDATES
# ═══════════════════════════════════════════════════════════════════════════════

class TestNewPatternCandidates:
    def test_new_pattern_candidate_detected(self, project_with_patterns):
        root = project_with_patterns
        _write_yaml(
            root / "memory" / "episodic" / "incidents" / "new_pattern.yaml",
            {
                "id": "INC-20260301-002",
                "root_cause": {
                    "identified": True,
                    "description": "A completely new type of bug in API pagination",
                    "matched_patterns": [],
                    "new_pattern_candidate": True,
                    "contributing_factors": [
                        {"factor": "Off-by-one in cursor", "category": "api"},
                    ],
                },
            },
        )

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        assert len(report.new_pattern_candidates) == 1
        assert report.new_pattern_candidates[0].category_suggestion == "api"


# ═══════════════════════════════════════════════════════════════════════════════
#  TEST: JSON OUTPUT
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationJSON:
    def test_json_serializable(self, project_with_incident):
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        report = engine.scan()
        result = json.dumps(report.to_dict())
        assert isinstance(result, str)
        parsed = json.loads(result)
        assert "summary" in parsed
        assert "reinforcements" in parsed


# ═══════════════════════════════════════════════════════════════════════════════
#  P0 — BVA: Boundary Value Analysis for confidence boosts
# ═══════════════════════════════════════════════════════════════════════════════

class TestConsolidationBVA:
    """Tests ISTQB BVA pour les seuils de consolidation."""

    def test_incident_boost_exact_value(self, project_with_incident):
        """Incident match should boost by exactly CONFIDENCE_BOOST['incident_match'] = 0.03."""
        engine = ConsolidationEngine(str(project_with_incident), verbose=False)
        engine.apply()

        patterns = _load_yaml(project_with_incident / "memory" / "semantic" / "defect_patterns.yaml")
        pat = next(p for p in patterns["patterns"] if p["id"] == "PAT-NULL-001")
        assert pat["confidence"] == pytest.approx(0.80 + CONFIDENCE_BOOST["incident_match"], abs=1e-4)

    def test_analysis_boost_exact_value(self, project_with_analysis):
        """Analysis finding should boost by exactly CONFIDENCE_BOOST['analysis_finding'] = 0.02."""
        engine = ConsolidationEngine(str(project_with_analysis), verbose=False)
        engine.apply()

        patterns = _load_yaml(project_with_analysis / "memory" / "semantic" / "defect_patterns.yaml")
        pat = next(p for p in patterns["patterns"] if p["id"] == "PAT-ASYNC-001")
        assert pat["confidence"] == pytest.approx(0.75 + CONFIDENCE_BOOST["analysis_finding"], abs=1e-4)

    def test_heuristic_boost_exact_value(self, project_with_decision):
        """Heuristic usage should boost by exactly CONFIDENCE_BOOST['heuristic_applied'] = 0.01."""
        engine = ConsolidationEngine(str(project_with_decision), verbose=False)
        engine.apply()

        heuristics = _load_yaml(project_with_decision / "memory" / "semantic" / "testing_heuristics.yaml")
        heu = next(h for h in heuristics["test_selection"] if h["id"] == "HEU-SEL-001")
        assert heu["confidence"] == pytest.approx(0.90 + CONFIDENCE_BOOST["heuristic_applied"], abs=1e-4)

    def test_confidence_cap_exact_boundary(self, project_with_patterns):
        """Confidence 0.99 + 0.03 boost should cap at exactly 1.0, not 1.02."""
        root = project_with_patterns
        patterns_path = root / "memory" / "semantic" / "defect_patterns.yaml"
        data = _load_yaml(patterns_path)
        data["patterns"][0]["confidence"] = 0.99
        _write_yaml(patterns_path, data)

        _write_yaml(root / "memory" / "episodic" / "incidents" / "cap_test.yaml", {
            "id": "INC-20260301-CAP",
            "root_cause": {"matched_patterns": ["PAT-NULL-001"]},
        })

        engine = ConsolidationEngine(str(root), verbose=False)
        engine.apply()

        updated = _load_yaml(patterns_path)
        conf = next(p for p in updated["patterns"] if p["id"] == "PAT-NULL-001")["confidence"]
        assert conf == 1.0  # Exact cap, not 1.02

    def test_confidence_at_1_0_no_overshoot(self, project_with_patterns):
        """Already at 1.0: should remain exactly 1.0 after boost."""
        root = project_with_patterns
        patterns_path = root / "memory" / "semantic" / "defect_patterns.yaml"
        data = _load_yaml(patterns_path)
        data["patterns"][0]["confidence"] = 1.0
        _write_yaml(patterns_path, data)

        _write_yaml(root / "memory" / "episodic" / "incidents" / "max_test.yaml", {
            "id": "INC-20260301-MAX",
            "root_cause": {"matched_patterns": ["PAT-NULL-001"]},
        })

        engine = ConsolidationEngine(str(root), verbose=False)
        engine.apply()

        updated = _load_yaml(patterns_path)
        conf = next(p for p in updated["patterns"] if p["id"] == "PAT-NULL-001")["confidence"]
        assert conf == 1.0


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

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

    def test_corrupt_episode_file_skipped(self, project_with_patterns):
        """A corrupt YAML episode file should be skipped without crashing."""
        root = project_with_patterns
        corrupt = root / "memory" / "episodic" / "incidents" / "corrupt.yaml"
        corrupt.parent.mkdir(parents=True, exist_ok=True)
        corrupt.write_text("{{INVALID YAML: [unterminated", encoding="utf-8")

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        assert len(report.errors) >= 1  # Error recorded

    def test_corrupt_semantic_file_does_not_crash(self, project_with_patterns):
        """Corrupt defect_patterns.yaml should not crash the engine."""
        root = project_with_patterns
        patterns_path = root / "memory" / "semantic" / "defect_patterns.yaml"
        patterns_path.write_text("{{NOT VALID YAML}}", encoding="utf-8")

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        # Engine should still complete, with errors
        assert isinstance(report, ConsolidationReport)

    def test_missing_semantic_dir(self, tmp_path):
        """Project with no semantic directory should not crash."""
        root = tmp_path / "argus"
        root.mkdir()
        memory = root / "memory"
        _write_yaml(memory / "_index.yaml", {"version": "1.0.0"})
        for sub in ["analyses", "incidents", "decisions", "executions"]:
            (memory / "episodic" / sub).mkdir(parents=True, exist_ok=True)

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        assert isinstance(report, ConsolidationReport)
        assert report.episodes_scanned == 0

    def test_empty_yaml_episode_skipped(self, project_with_patterns):
        """An empty YAML file (parses as None) should be skipped."""
        root = project_with_patterns
        empty = root / "memory" / "episodic" / "incidents" / "empty.yaml"
        empty.write_text("", encoding="utf-8")

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        # Should not crash, empty file is just skipped
        assert isinstance(report, ConsolidationReport)


# ═══════════════════════════════════════════════════════════════════════════════
#  P1 — EP: Missing Partitions (mixed episodes, execution episodes)
# ═══════════════════════════════════════════════════════════════════════════════

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

    def test_mixed_episode_types_combined(self, project_with_patterns):
        """Project with incident + analysis + decision at once."""
        root = project_with_patterns
        memory = root / "memory"

        # Incident referencing PAT-NULL-001
        _write_yaml(memory / "episodic" / "incidents" / "inc.yaml", {
            "id": "INC-20260301-001",
            "severity": "P2",
            "affected_components": ["auth"],
            "root_cause": {"matched_patterns": ["PAT-NULL-001"]},
        })
        # Analysis referencing PAT-ASYNC-001
        _write_yaml(memory / "episodic" / "analyses" / "anl.yaml", {
            "id": "ANL-20260301-001",
            "quick_summary": {"one_liner": "t", "status": "COMPLETED", "next_action": "n"},
            "findings": [{"description": "Race condition PAT-ASYNC-001"}],
        })
        # Decision using HEU-SEL-001
        _write_yaml(memory / "episodic" / "decisions" / "dec.yaml", {
            "id": "DEC-20260301-001",
            "knowledge_applied": {
                "patterns_considered": [],
                "heuristics_used": ["HEU-SEL-001"],
                "istqb_techniques": [],
            },
        })

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        assert report.episodes_unconsolidated == 3
        target_ids = {r.target_id for r in report.reinforcements}
        assert "PAT-NULL-001" in target_ids
        assert "PAT-ASYNC-001" in target_ids
        assert "HEU-SEL-001" in target_ids

    def test_execution_episode_scanned_but_no_reinforcement(self, project_with_patterns):
        """Execution episodes are scanned but produce no reinforcements (no handler)."""
        root = project_with_patterns
        _write_yaml(root / "memory" / "episodic" / "executions" / "exe.yaml", {
            "id": "EXE-20260301-001",
            "result": "PASS",
            "coverage": 85,
        })

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        assert report.episodes_scanned == 1
        assert report.episodes_unconsolidated == 1
        assert len(report.reinforcements) == 0  # No handler for executions

    def test_text_reference_extraction(self, project_with_patterns):
        """Pattern IDs embedded in free text should be extracted via regex."""
        root = project_with_patterns
        _write_yaml(root / "memory" / "episodic" / "analyses" / "text_ref.yaml", {
            "id": "ANL-20260301-002",
            "quick_summary": {"one_liner": "t", "status": "COMPLETED", "next_action": "n"},
            "findings": [
                {"description": "The root cause relates to PAT-NULL-001 in the auth module"},
            ],
        })

        engine = ConsolidationEngine(str(root), verbose=False)
        report = engine.scan()
        pat_refs = [r for r in report.reinforcements if r.target_id == "PAT-NULL-001"]
        assert len(pat_refs) >= 1


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

class TestConsolidationCLI:
    """Tests ISTQB pour l'interface CLI du moteur de consolidation."""

    def test_cli_scan_exit_0_no_actions(self, project_with_patterns):
        """CLI scan on clean project exits with code 0."""
        result = subprocess.run(
            [sys.executable, "consolidation.py", "--base-dir", str(project_with_patterns), "--quiet"],
            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_incident):
        """CLI scan with pending work exits with code 1."""
        result = subprocess.run(
            [sys.executable, "consolidation.py", "--base-dir", str(project_with_incident), "--quiet"],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        assert result.returncode == 1

    def test_cli_json_output_valid(self, project_with_incident):
        """CLI --json produces valid parseable JSON."""
        result = subprocess.run(
            [sys.executable, "consolidation.py", "--base-dir", str(project_with_incident), "--json"],
            capture_output=True, text=True, cwd=str(Path(__file__).parent),
        )
        parsed = json.loads(result.stdout)
        assert "summary" in parsed
        assert "reinforcements" in parsed
