"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Tests unitaires pour sync_schemas.py
 Couvre : EnumDelta, CrossSchemaDesync, SyncReport, SchemaSynchronizer
═══════════════════════════════════════════════════════════════════════════════
"""

import json
import pytest
import yaml
from pathlib import Path

from sync_schemas import (
    EnumDelta,
    CrossSchemaDesync,
    SyncReport,
    SchemaSynchronizer,
    get_enum_at_path,
    set_enum_at_path,
    extract_values_from_yaml,
    extract_heuristic_section_names,
    collect_data_values,
)


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


class TestEnumDelta:
    """Tests pour la dataclass EnumDelta."""

    def test_is_desync_true(self):
        d = EnumDelta(
            group="test", schema_file="s.json", json_path="a.b.enum",
            missing_in_schema=["new_val"], missing_in_data=[],
            current_enum=["a"], data_values=["a", "new_val"],
        )
        assert d.is_desync is True

    def test_is_desync_false(self):
        d = EnumDelta(
            group="test", schema_file="s.json", json_path="a.b.enum",
            missing_in_schema=[], missing_in_data=["unused"],
            current_enum=["a", "unused"], data_values=["a"],
        )
        assert d.is_desync is False

    def test_to_dict_contains_all_fields(self):
        d = EnumDelta(
            group="g", schema_file="f.json", json_path="x.enum",
            missing_in_schema=["v1"], missing_in_data=["v2"],
            current_enum=["a", "v2"], data_values=["a", "v1"],
            severity="high",
        )
        result = d.to_dict()
        assert result["group"] == "g"
        assert result["schema_file"] == "f.json"
        assert result["missing_in_schema"] == ["v1"]
        assert result["missing_in_data"] == ["v2"]
        assert result["severity"] == "high"
        assert result["desync"] is True


class TestCrossSchemaDesync:
    """Tests pour la dataclass CrossSchemaDesync."""

    def test_is_desync_missing(self):
        d = CrossSchemaDesync(
            group="g", source_schema="src.json", source_path="a.enum",
            target_schema="tgt.json", target_path="b.enum",
            missing_in_target=["val"], extra_in_target=[],
        )
        assert d.is_desync is True

    def test_is_desync_extra(self):
        d = CrossSchemaDesync(
            group="g", source_schema="src.json", source_path="a.enum",
            target_schema="tgt.json", target_path="b.enum",
            missing_in_target=[], extra_in_target=["old_val"],
        )
        assert d.is_desync is True

    def test_is_desync_false(self):
        d = CrossSchemaDesync(
            group="g", source_schema="src.json", source_path="a.enum",
            target_schema="tgt.json", target_path="b.enum",
            missing_in_target=[], extra_in_target=[],
        )
        assert d.is_desync is False

    def test_to_dict(self):
        d = CrossSchemaDesync(
            group="g", source_schema="src.json", source_path="a",
            target_schema="tgt.json", target_path="b",
            missing_in_target=["x"], extra_in_target=["y"],
        )
        result = d.to_dict()
        assert result["source_schema"] == "src.json"
        assert result["target_schema"] == "tgt.json"
        assert result["missing_in_target"] == ["x"]
        assert result["extra_in_target"] == ["y"]


class TestSyncReport:
    """Tests pour la dataclass SyncReport."""

    def test_empty_report(self):
        r = SyncReport()
        assert r.has_desyncs is False
        assert r.desync_count == 0

    def test_report_with_enum_desync(self):
        r = SyncReport()
        r.enum_deltas.append(EnumDelta(
            group="g", schema_file="f.json", json_path="p",
            missing_in_schema=["v"], missing_in_data=[],
            current_enum=["a"], data_values=["a", "v"],
        ))
        assert r.has_desyncs is True
        assert r.desync_count == 1

    def test_report_with_cross_desync(self):
        r = SyncReport()
        r.cross_schema_desyncs.append(CrossSchemaDesync(
            group="g", source_schema="s.json", source_path="p",
            target_schema="t.json", target_path="q",
            missing_in_target=["v"], extra_in_target=[],
        ))
        assert r.has_desyncs is True
        assert r.desync_count == 1

    def test_report_to_dict_status(self):
        r = SyncReport()
        assert r.to_dict()["summary"]["status"] == "SYNC"

        r.enum_deltas.append(EnumDelta(
            group="g", schema_file="f.json", json_path="p",
            missing_in_schema=["v"], missing_in_data=[],
            current_enum=[], data_values=["v"],
        ))
        assert r.to_dict()["summary"]["status"] == "DESYNC"


# ═══════════════════════════════════════════════════════════════════════════════
#  HELPERS — get_enum_at_path / set_enum_at_path
# ═══════════════════════════════════════════════════════════════════════════════


class TestGetEnumAtPath:
    """Tests pour get_enum_at_path."""

    def test_simple_path(self):
        schema = {"properties": {"category": {"enum": ["a", "b"]}}}
        result = get_enum_at_path(schema, ["properties", "category", "enum"])
        assert result == ["a", "b"]

    def test_nested_path(self):
        schema = {
            "properties": {
                "items": {
                    "properties": {
                        "type": {"enum": ["x", "y", "z"]}
                    }
                }
            }
        }
        result = get_enum_at_path(
            schema, ["properties", "items", "properties", "type", "enum"]
        )
        assert result == ["x", "y", "z"]

    def test_missing_path(self):
        schema = {"properties": {"name": {"type": "string"}}}
        result = get_enum_at_path(schema, ["properties", "category", "enum"])
        assert result is None

    def test_filters_nulls(self):
        schema = {"properties": {"status": {"enum": ["a", None, "b"]}}}
        result = get_enum_at_path(schema, ["properties", "status", "enum"])
        assert result == ["a", "b"]


class TestSetEnumAtPath:
    """Tests pour set_enum_at_path."""

    def test_simple_set(self):
        schema = {"properties": {"category": {"enum": ["a"]}}}
        ok = set_enum_at_path(schema, ["properties", "category", "enum"], ["a", "b"])
        assert ok is True
        assert schema["properties"]["category"]["enum"] == ["a", "b"]

    def test_preserves_nulls(self):
        schema = {"properties": {"status": {"enum": ["a", None]}}}
        ok = set_enum_at_path(schema, ["properties", "status", "enum"], ["a", "b"])
        assert ok is True
        assert schema["properties"]["status"]["enum"] == ["a", "b", None]

    def test_missing_path_returns_false(self):
        schema = {"properties": {}}
        ok = set_enum_at_path(schema, ["properties", "missing", "enum"], ["a"])
        assert ok is False


# ═══════════════════════════════════════════════════════════════════════════════
#  EXTRACTION — extract_values_from_yaml
# ═══════════════════════════════════════════════════════════════════════════════


class TestExtractValues:
    """Tests pour extract_values_from_yaml."""

    def test_extract_from_list(self, tmp_path):
        data = {
            "patterns": [
                {"id": "P1", "category": "null_safety"},
                {"id": "P2", "category": "async"},
                {"id": "P3", "category": "null_safety"},
            ]
        }
        f = tmp_path / "test.yaml"
        f.write_text(yaml.dump(data), encoding="utf-8")

        result = extract_values_from_yaml(f, ["patterns", "*", "category"])
        assert result == {"null_safety", "async"}

    def test_extract_single_value(self, tmp_path):
        data = {"category": "bug"}
        f = tmp_path / "test.yaml"
        f.write_text(yaml.dump(data), encoding="utf-8")

        result = extract_values_from_yaml(f, ["category"])
        assert result == {"bug"}

    def test_missing_file(self, tmp_path):
        result = extract_values_from_yaml(tmp_path / "nope.yaml", ["category"])
        assert result == set()

    def test_empty_file(self, tmp_path):
        f = tmp_path / "empty.yaml"
        f.write_text("", encoding="utf-8")
        result = extract_values_from_yaml(f, ["category"])
        assert result == set()

    def test_double_star_extraction(self, tmp_path):
        """Test ** wildcard that iterates over dict values containing lists."""
        data = {
            "section_a": [
                {"id": "H1", "category": "selection"},
                {"id": "H2", "category": "coverage"},
            ],
            "section_b": [
                {"id": "H3", "category": "api_testing"},
            ],
        }
        f = tmp_path / "test.yaml"
        f.write_text(yaml.dump(data), encoding="utf-8")

        result = extract_values_from_yaml(f, ["**", "*", "category"])
        assert result == {"selection", "coverage", "api_testing"}


class TestExtractHeuristicSectionNames:
    """Tests pour extract_heuristic_section_names."""

    def test_extracts_sections(self, tmp_path):
        data = {
            "version": "1.0.0",
            "last_updated": "2026-02-21",
            "test_selection": [{"id": "H1"}],
            "coverage": [{"id": "H2"}],
            "metadata": {"total": 2},
        }
        f = tmp_path / "heuristics.yaml"
        f.write_text(yaml.dump(data), encoding="utf-8")

        result = extract_heuristic_section_names(f)
        assert result == {"test_selection", "coverage"}

    def test_skips_metadata_keys(self, tmp_path):
        data = {
            "version": "1.0.0",
            "last_updated": "2026-02-21",
            "metadata": {"x": 1},
        }
        f = tmp_path / "heuristics.yaml"
        f.write_text(yaml.dump(data), encoding="utf-8")

        result = extract_heuristic_section_names(f)
        assert result == set()


# ═══════════════════════════════════════════════════════════════════════════════
#  SYNCHRONIZER — Tests d'intégration
# ═══════════════════════════════════════════════════════════════════════════════


@pytest.fixture
def sync_project(tmp_path):
    """Crée un mini-projet Argus avec schemas et données pour tester sync."""
    root = tmp_path / "argus_test"
    root.mkdir()

    # Schemas directory
    schemas = root / "schemas"
    schemas.mkdir()

    # Memory structure
    memory = root / "memory"
    (memory / "semantic").mkdir(parents=True)
    (memory / "episodic" / "analyses").mkdir(parents=True)
    (memory / "episodic" / "incidents").mkdir(parents=True)
    (memory / "episodic" / "executions").mkdir(parents=True)
    (memory / "projects").mkdir(parents=True)

    # defect_patterns.schema.json — with only 2 categories
    dp_schema = {
        "properties": {
            "patterns": {
                "items": {
                    "properties": {
                        "category": {
                            "type": "string",
                            "enum": ["null_safety", "async"],
                        }
                    }
                }
            }
        }
    }
    _write_json(schemas / "defect_patterns.schema.json", dp_schema)

    # incident.schema.json — subcategories subset
    inc_schema = {
        "properties": {
            "category": {"type": "string", "enum": ["regression", "bug"]},
            "root_cause": {
                "properties": {
                    "contributing_factors": {
                        "items": {
                            "properties": {
                                "category": {
                                    "type": "string",
                                    "enum": ["code", "config"],
                                }
                            }
                        }
                    }
                }
            },
            "argus_improvements": {
                "properties": {
                    "new_patterns_to_learn": {
                        "items": {
                            "properties": {
                                "category": {
                                    "type": "string",
                                    "enum": ["null_safety"],
                                }
                            }
                        }
                    },
                    "heuristics_to_add": {
                        "items": {
                            "properties": {
                                "category": {
                                    "type": "string",
                                    "enum": ["selection"],
                                }
                            }
                        }
                    },
                }
            },
        }
    }
    _write_json(schemas / "incident.schema.json", inc_schema)

    # testing_heuristics.schema.json
    th_schema = {
        "definitions": {
            "heuristic_list": {
                "items": {
                    "properties": {
                        "category": {
                            "type": "string",
                            "enum": ["selection", "coverage", "api_testing"],
                        }
                    }
                }
            }
        }
    }
    _write_json(schemas / "testing_heuristics.schema.json", th_schema)

    # analysis.schema.json
    an_schema = {
        "properties": {
            "findings": {
                "items": {
                    "properties": {
                        "category": {
                            "type": "string",
                            "enum": ["regression", "bug", "security"],
                        }
                    }
                }
            }
        }
    }
    _write_json(schemas / "analysis.schema.json", an_schema)

    # execution.schema.json
    ex_schema = {
        "properties": {
            "environment": {
                "properties": {
                    "runner": {
                        "type": "string",
                        "enum": ["github-actions", "local"],
                    }
                }
            }
        }
    }
    _write_json(schemas / "execution.schema.json", ex_schema)

    # project.schema.json
    proj_schema = {
        "properties": {
            "architecture": {
                "properties": {
                    "structure": {
                        "properties": {
                            "type": {
                                "type": "string",
                                "enum": ["monolith", "microservices"],
                            }
                        }
                    }
                }
            }
        }
    }
    _write_json(schemas / "project.schema.json", proj_schema)

    # ── YAML data ──

    # defect_patterns.yaml — has 'security' which is NOT in schema
    patterns = {
        "version": "1.0.0",
        "last_updated": "2026-02-21",
        "patterns": [
            {"id": "PAT-NULL-001", "category": "null_safety"},
            {"id": "PAT-ASYNC-001", "category": "async"},
            {"id": "PAT-SEC-001", "category": "security"},
        ],
    }
    _write_yaml(memory / "semantic" / "defect_patterns.yaml", patterns)

    # testing_heuristics.yaml
    heuristics = {
        "version": "1.0.0",
        "last_updated": "2026-02-21",
        "selection": [
            {"id": "HEU-SEL-001", "category": "selection"},
        ],
        "coverage": [
            {"id": "HEU-COV-001", "category": "coverage"},
        ],
        "api_testing": [
            {"id": "HEU-API-001", "category": "api_testing"},
        ],
    }
    _write_yaml(memory / "semantic" / "testing_heuristics.yaml", heuristics)

    return root


def _write_json(path, data):
    path.write_text(json.dumps(data, indent=2), encoding="utf-8")


def _write_yaml(path, data):
    path.write_text(
        yaml.dump(data, default_flow_style=False, allow_unicode=True),
        encoding="utf-8",
    )


class TestSchemaSynchronizerCheck:
    """Tests pour SchemaSynchronizer.check()."""

    def test_detects_missing_in_schema(self, sync_project):
        """'security' est dans les données mais pas dans defect_patterns.schema.json."""
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()

        dp_deltas = [
            d for d in report.enum_deltas
            if d.group == "defect_pattern_categories"
            and d.schema_file == "defect_patterns.schema.json"
        ]
        assert len(dp_deltas) == 1
        assert "security" in dp_deltas[0].missing_in_schema

    def test_detects_cross_schema_desync(self, sync_project):
        """incident.schema new_patterns_to_learn manque 'async' et 'security'."""
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()

        cross = [
            d for d in report.cross_schema_desyncs
            if d.group == "defect_pattern_categories"
        ]
        assert len(cross) == 1
        assert "async" in cross[0].missing_in_target

    def test_no_desync_when_aligned(self, sync_project):
        """Si on aligne le schema, plus de désync."""
        # Patch the schema to include 'security'
        schema_path = sync_project / "schemas" / "defect_patterns.schema.json"
        schema = json.loads(schema_path.read_text(encoding="utf-8"))
        schema["properties"]["patterns"]["items"]["properties"]["category"]["enum"] = [
            "async", "null_safety", "security",
        ]
        _write_json(schema_path, schema)

        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()

        dp_deltas = [
            d for d in report.enum_deltas
            if d.group == "defect_pattern_categories"
            and d.schema_file == "defect_patterns.schema.json"
        ]
        assert len(dp_deltas) == 1
        assert dp_deltas[0].is_desync is False

    def test_report_has_desyncs(self, sync_project):
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()
        assert report.has_desyncs is True
        assert report.desync_count > 0


class TestSchemaSynchronizerFix:
    """Tests pour SchemaSynchronizer.fix()."""

    def test_fix_adds_missing_values(self, sync_project):
        """--fix ajoute les valeurs manquantes dans les schemas."""
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.fix()

        # Vérifier que des corrections ont été appliquées
        assert len(report.fixes_applied) > 0

        # Recharger le schema et vérifier que 'security' est présent
        schema = json.loads(
            (sync_project / "schemas" / "defect_patterns.schema.json")
            .read_text(encoding="utf-8")
        )
        categories = schema["properties"]["patterns"]["items"]["properties"]["category"]["enum"]
        assert "security" in categories

    def test_fix_synchronizes_cross_schemas(self, sync_project):
        """--fix synchronise l'incident schema avec le defect_patterns schema."""
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        sync.fix()

        incident_schema = json.loads(
            (sync_project / "schemas" / "incident.schema.json")
            .read_text(encoding="utf-8")
        )
        inc_categories = (
            incident_schema["properties"]["argus_improvements"]["properties"]
            ["new_patterns_to_learn"]["items"]["properties"]["category"]["enum"]
        )
        # Doit contenir toutes les valeurs de defect_patterns
        assert "async" in inc_categories
        assert "null_safety" in inc_categories
        assert "security" in inc_categories

    def test_fix_then_check_is_sync(self, sync_project):
        """Après un fix, un check doit retourner SYNC."""
        sync_fix = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        sync_fix.fix()

        sync_check = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync_check.check()
        assert report.has_desyncs is False

    def test_fix_idempotent(self, sync_project):
        """Deux fix successifs donnent le même résultat."""
        for _ in range(2):
            sync = SchemaSynchronizer(
                base_dir=sync_project,
                schemas_dir=sync_project / "schemas",
                verbose=False,
            )
            sync.fix()

        sync_check = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync_check.check()
        assert report.has_desyncs is False


class TestCollectDataValues:
    """Tests pour collect_data_values avec des données réelles."""

    def test_collect_defect_patterns(self, sync_project):
        values = collect_data_values("defect_pattern_categories", sync_project)
        assert values == {"null_safety", "async", "security"}

    def test_collect_heuristic_categories(self, sync_project):
        values = collect_data_values("heuristic_categories", sync_project)
        assert "selection" in values
        assert "coverage" in values
        assert "api_testing" in values

    def test_collect_unknown_group(self, sync_project):
        values = collect_data_values("nonexistent_group", sync_project)
        assert values == set()

    def test_collect_empty_directory(self, sync_project):
        """Pas de fichiers d'incidents → ensemble vide."""
        values = collect_data_values("incident_categories", sync_project)
        assert values == set()


class TestSyncReportJson:
    """Tests pour la sortie JSON du rapport."""

    def test_json_output_structure(self, sync_project):
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()
        output = report.to_dict()

        assert "summary" in output
        assert "enum_deltas" in output
        assert "cross_schema_desyncs" in output
        assert "errors" in output
        assert output["summary"]["status"] in ("SYNC", "DESYNC")

    def test_json_serializable(self, sync_project):
        sync = SchemaSynchronizer(
            base_dir=sync_project,
            schemas_dir=sync_project / "schemas",
            verbose=False,
        )
        report = sync.check()
        # Doit être sérialisable sans erreur
        result = json.dumps(report.to_dict(), ensure_ascii=False)
        assert isinstance(result, str)
