"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Tests unitaires pour validate_references.py
 Couvre : DefinedId, Reference, OrphanReference, ValidationReport,
          ReferenceValidator (4 phases)
═══════════════════════════════════════════════════════════════════════════════
"""

import pytest
import yaml
from pathlib import Path

from validate_references import (
    DefinedId,
    Reference,
    OrphanReference,
    ValidationReport,
    ReferenceValidator,
    ID_PATTERNS,
)


# ═══════════════════════════════════════════════════════════════════════════════
#  DATA CLASSES
# ═══════════════════════════════════════════════════════════════════════════════


class TestDefinedId:
    def test_basic_creation(self):
        d = DefinedId(id="PAT-NULL-001", id_type="pattern", source_file="f.yaml")
        assert d.id == "PAT-NULL-001"
        assert d.id_type == "pattern"


class TestReference:
    def test_basic_creation(self):
        r = Reference(target_id="PAT-NULL-001", id_type="pattern", source_file="a.yaml", line_number=10)
        assert r.target_id == "PAT-NULL-001"
        assert r.line_number == 10


class TestOrphanReference:
    def test_to_dict(self):
        ref = Reference("PAT-GHOST-001", "pattern", "src.yaml", 5, "some context")
        o = OrphanReference(reference=ref, suggestion="PAT-NULL-001")
        d = o.to_dict()
        assert d["target_id"] == "PAT-GHOST-001"
        assert d["suggestion"] == "PAT-NULL-001"


class TestValidationReport:
    def test_empty_report(self):
        r = ValidationReport()
        assert r.has_orphans is False
        assert r.total_defined == 0

    def test_report_with_orphans(self):
        r = ValidationReport()
        ref = Reference("X", "pattern", "f.yaml", 1)
        r.orphans.append(OrphanReference(reference=ref))
        assert r.has_orphans is True

    def test_to_dict(self):
        r = ValidationReport()
        r.defined_ids["pattern"] = [
            DefinedId("PAT-A-001", "pattern", "f.yaml"),
        ]
        d = r.to_dict()
        assert d["summary"]["total_ids_defined"] == 1
        assert d["summary"]["status"] == "ALL_VALID"


# ═══════════════════════════════════════════════════════════════════════════════
#  ID_PATTERNS — Regex
# ═══════════════════════════════════════════════════════════════════════════════


class TestIdPatterns:
    """Vérifie que les regex reconnaissent les bons formats d'ID."""

    @pytest.mark.parametrize(
        "id_type,valid_id",
        [
            ("pattern", "PAT-NULL-001"),
            ("pattern", "PAT-API-010"),
            ("heuristic", "HEU-SEL-001"),
            ("heuristic", "HEU-ISTQB-007"),
            ("analysis", "ANL-20260215-001"),
            ("decision", "DEC-20260301-001"),
            ("incident", "INC-20260101-003"),
            ("execution", "EXE-20261231-012"),
        ],
    )
    def test_valid_ids_match(self, id_type, valid_id):
        assert ID_PATTERNS[id_type].search(valid_id) is not None

    @pytest.mark.parametrize(
        "id_type,invalid_id",
        [
            ("pattern", "PAT-001"),       # missing category
            ("pattern", "pat-null-001"),   # lowercase
            ("heuristic", "HEU001"),       # missing dashes
            ("analysis", "ANL-2026-001"),  # wrong date format
            ("analysis", "ANL-202602-01"), # incomplete date
        ],
    )
    def test_invalid_ids_no_match(self, id_type, invalid_id):
        assert ID_PATTERNS[id_type].search(invalid_id) is None


# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 1 : Extraction des IDs définis
# ═══════════════════════════════════════════════════════════════════════════════


class TestExtractDefinedIds:
    """Tests pour la Phase 1 du ReferenceValidator."""

    def test_extracts_patterns(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        pattern_ids = {d.id for d in v.report.defined_ids["pattern"]}
        assert "PAT-NULL-001" in pattern_ids
        assert "PAT-ASYNC-001" in pattern_ids

    def test_extracts_heuristics(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        heuristic_ids = {d.id for d in v.report.defined_ids["heuristic"]}
        assert "HEU-SEL-001" in heuristic_ids
        assert "HEU-SEL-002" in heuristic_ids
        assert "HEU-COV-001" in heuristic_ids
        assert len(heuristic_ids) == 3

    def test_extracts_projects(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        project_ids = {d.id for d in v.report.defined_ids["project"]}
        assert "testproj" in project_ids

    def test_excludes_templates(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        project_ids = {d.id for d in v.report.defined_ids["project"]}
        # _template_project.yaml should not appear
        assert "_template_project" not in project_ids

    def test_extracts_episodic_ids(self, tmp_project_with_episodes):
        v = ReferenceValidator(str(tmp_project_with_episodes), verbose=False)
        v.extract_defined_ids()
        analysis_ids = {d.id for d in v.report.defined_ids["analysis"]}
        assert "ANL-20260215-001" in analysis_ids

    def test_extracts_files(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        file_ids = {d.id for d in v.report.defined_ids["file"]}
        # Doit contenir les fichiers sous forme de chemins relatifs
        assert "_index.yaml" in file_ids


# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 2 : Extraction des références
# ═══════════════════════════════════════════════════════════════════════════════


class TestExtractReferences:
    """Tests pour la Phase 2 du ReferenceValidator."""

    def test_finds_references_in_project_field(self, tmp_project_with_episodes):
        """Les champs 'project: testproj' doivent être détectés comme références."""
        v = ReferenceValidator(str(tmp_project_with_episodes), verbose=False)
        v.extract_defined_ids()
        v.extract_references()
        project_refs = [r for r in v.report.references if r.id_type == "project"]
        assert len(project_refs) >= 1

    def test_ignores_comments(self, tmp_project):
        """Les IDs dans les commentaires YAML ne sont pas comptés comme références."""
        commented = tmp_project / "memory" / "semantic" / "commented.yaml"
        commented.write_text(
            "# This is a comment about PAT-NULL-001\ndata: value\n",
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        v.extract_references()
        # PAT-NULL-001 in comment should not count as a reference
        comment_refs = [
            r for r in v.report.references
            if r.target_id == "PAT-NULL-001" and "commented.yaml" in r.source_file
        ]
        assert len(comment_refs) == 0

    def test_ignores_id_definitions_inline(self, tmp_project):
        """Les lignes 'id: PAT-xxx' (hors liste) sont des définitions, pas des références."""
        # Créer un fichier avec un champ id: de premier niveau (pas dans une liste)
        inline_file = tmp_project / "memory" / "semantic" / "inline_id.yaml"
        inline_file.write_text(
            "id: PAT-INLINE-001\nname: test\n", encoding="utf-8"
        )
        # Aussi ajouter cet ID dans defined_ids pour éviter orphelin
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        v.extract_references()
        # "id: PAT-INLINE-001" est filtré par le regex ^\s*id:\s*
        inline_refs = [
            r for r in v.report.references
            if r.target_id == "PAT-INLINE-001"
            and "inline_id.yaml" in r.source_file
        ]
        assert len(inline_refs) == 0

    def test_finds_file_references(self, tmp_project):
        """Les champs 'path:' contenant un .yaml sont détectés comme références de fichier."""
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.extract_defined_ids()
        v.extract_references()
        file_refs = [r for r in v.report.references if r.id_type == "file"]
        assert len(file_refs) >= 1


# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 3 : Validation des références
# ═══════════════════════════════════════════════════════════════════════════════


class TestValidateReferences:
    """Tests pour la Phase 3 du ReferenceValidator."""

    def test_no_orphans_in_clean_project(self, tmp_project):
        """Un projet propre ne doit pas avoir de références orphelines."""
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        assert report.has_orphans is False

    def test_detects_orphan_reference(self, tmp_project):
        """Une référence vers un ID inexistant est détectée comme orpheline."""
        orphan_file = tmp_project / "memory" / "semantic" / "orphan_test.yaml"
        orphan_file.write_text(
            "analysis: ANL-20260215-001\n"
            "related_patterns:\n"
            "  - PAT-GHOST-999\n",
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        orphan_ids = {o.reference.target_id for o in report.orphans}
        assert "PAT-GHOST-999" in orphan_ids

    def test_valid_cross_reference(self, tmp_project):
        """Une référence vers un ID existant n'est pas orpheline."""
        ref_file = tmp_project / "memory" / "semantic" / "ref_test.yaml"
        ref_file.write_text(
            "related_patterns:\n"
            "  - PAT-NULL-001\n"
            "related_heuristics:\n"
            "  - HEU-SEL-001\n",
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        orphan_ids = {o.reference.target_id for o in report.orphans}
        assert "PAT-NULL-001" not in orphan_ids
        assert "HEU-SEL-001" not in orphan_ids

    def test_suggestion_for_near_miss(self, tmp_project):
        """Le validateur suggère un ID proche pour les orphelins."""
        orphan_file = tmp_project / "memory" / "semantic" / "nearmiss.yaml"
        orphan_file.write_text(
            "related_patterns:\n  - PAT-NULL-002\n", encoding="utf-8"
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        orphans_with_suggestion = [o for o in report.orphans if o.suggestion is not None]
        # PAT-NULL-002 is close to PAT-NULL-001 → expect a suggestion
        if report.orphans:
            target_orphan = [o for o in report.orphans if o.reference.target_id == "PAT-NULL-002"]
            if target_orphan:
                assert target_orphan[0].suggestion == "PAT-NULL-001"


# ═══════════════════════════════════════════════════════════════════════════════
#  PHASE 4 : Cohérence de _index.yaml
# ═══════════════════════════════════════════════════════════════════════════════


class TestIndexConsistency:
    """Tests pour la Phase 4 (validate_index_consistency)."""

    def test_consistent_index_no_errors(self, tmp_project):
        """Un index cohérent ne doit pas générer d'erreurs de consistance."""
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        # Filtrer les erreurs de consistance (pas les orphans)
        consistency_errors = [e for e in report.errors if "_index.yaml" in e]
        assert len(consistency_errors) == 0

    def test_wrong_pattern_count_in_index(self, tmp_project):
        """Un compteur de patterns incorrect dans l'index doit être rapporté."""
        index_path = tmp_project / "memory" / "_index.yaml"
        index = yaml.safe_load(index_path.read_text(encoding="utf-8"))
        index["semantic"]["defect_patterns"]["count"] = 999
        index_path.write_text(
            yaml.dump(index, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        assert any("defect_patterns" in e and "999" in e for e in report.errors)

    def test_wrong_episodic_count(self, tmp_project_with_episodes):
        """Un compteur épisodique incorrect doit être détecté."""
        index_path = tmp_project_with_episodes / "memory" / "_index.yaml"
        index = yaml.safe_load(index_path.read_text(encoding="utf-8"))
        index["episodic"]["analyses"]["count"] = 50
        index_path.write_text(
            yaml.dump(index, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project_with_episodes), verbose=False)
        report = v.run()
        assert any("analyses" in e and "50" in e for e in report.errors)

    def test_invalid_path_in_index(self, tmp_project):
        """Un chemin inexistant dans l'index doit être rapporté."""
        index_path = tmp_project / "memory" / "_index.yaml"
        index = yaml.safe_load(index_path.read_text(encoding="utf-8"))
        index["relational"]["path"] = "memory/nonexistent/ghost.yaml"
        index_path.write_text(
            yaml.dump(index, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        assert any("ghost.yaml" in e or "nonexistent" in e for e in report.errors)


# ═══════════════════════════════════════════════════════════════════════════════
#  GRAPHE MERMAID
# ═══════════════════════════════════════════════════════════════════════════════


class TestDependencyGraph:
    """Tests pour generate_dependency_graph()."""

    def test_graph_format(self, tmp_project_with_episodes):
        """Le graphe généré doit être au format Mermaid valide."""
        v = ReferenceValidator(str(tmp_project_with_episodes), verbose=False)
        v.run()
        graph = v.generate_dependency_graph()
        assert "```mermaid" in graph
        assert "graph LR" in graph
        assert "```" in graph

    def test_graph_empty_project(self, tmp_project):
        """Un projet sans références produit un graphe Mermaid minimal."""
        # Supprimer les fichiers qui génèrent des refs pour simplifier
        v = ReferenceValidator(str(tmp_project), verbose=False)
        v.run()
        graph = v.generate_dependency_graph()
        assert "```mermaid" in graph


# ═══════════════════════════════════════════════════════════════════════════════
#  RAPPORT
# ═══════════════════════════════════════════════════════════════════════════════


class TestReferenceReport:
    """Tests pour print_report()."""

    def test_print_report_valid(self, tmp_project, capsys):
        v = ReferenceValidator(str(tmp_project), verbose=True)
        v.run()
        v.print_report()
        captured = capsys.readouterr()
        assert "REFERENCE VALIDATION REPORT" in captured.out

    def test_print_report_with_orphans(self, tmp_project, capsys):
        orphan_file = tmp_project / "memory" / "semantic" / "bad_ref.yaml"
        orphan_file.write_text(
            "related_patterns:\n  - PAT-MISSING-999\n", encoding="utf-8"
        )
        v = ReferenceValidator(str(tmp_project), verbose=True)
        v.run()
        v.print_report()
        captured = capsys.readouterr()
        assert "ORPHAN" in captured.out


# ═══════════════════════════════════════════════════════════════════════════════
#  RUN COMPLET
# ═══════════════════════════════════════════════════════════════════════════════


class TestFullRun:
    """Tests d'intégration pour le run complet."""

    def test_full_run_clean(self, tmp_project):
        v = ReferenceValidator(str(tmp_project), verbose=False)
        report = v.run()
        assert report.files_checked >= 1
        assert report.total_defined >= 5  # 2 patterns + 3 heuristics + projet + fichiers

    def test_full_run_with_episodes(self, tmp_project_with_episodes):
        v = ReferenceValidator(str(tmp_project_with_episodes), verbose=False)
        report = v.run()
        assert report.files_checked >= 2  # Au moins patterns + heuristics + analysis

    def test_normalize_path(self, tmp_project):
        """Les chemins normalisés utilisent des forward slashes."""
        v = ReferenceValidator(str(tmp_project), verbose=False)
        assert v._normalize_path(Path("memory\\semantic\\test.yaml")) == "memory/semantic/test.yaml"
        assert v._normalize_path(Path("memory/semantic/test.yaml")) == "memory/semantic/test.yaml"
