"""Tests de validation de la configuration du projet."""

import re
import tomllib
from pathlib import Path

import yaml
from hypothesis import given, settings
from hypothesis import strategies as st

PYPROJECT_PATH = Path(__file__).resolve().parent.parent / "pyproject.toml"


def _load_pyproject() -> dict:
    """Charge et parse le pyproject.toml."""
    with open(PYPROJECT_PATH, "rb") as f:
        return tomllib.load(f)


def test_pyproject_is_parsable():
    """Vérifie que le pyproject.toml est parsable."""
    data = _load_pyproject()
    assert isinstance(data, dict)


def test_project_name():
    """Vérifie que le nom du package est cezam-lib."""
    data = _load_pyproject()
    assert data["project"]["name"] == "cezam-lib"


def test_project_version():
    """Vérifie que la version est 0.1.7."""
    data = _load_pyproject()
    assert data["project"]["version"] == "0.1.7"


def test_requires_python():
    """Vérifie que requires-python est >= 3.11."""
    data = _load_pyproject()
    assert data["project"]["requires-python"] == ">=3.11"


def test_build_system_uses_hatchling():
    """Vérifie que le build backend est hatchling."""
    data = _load_pyproject()
    bs = data["build-system"]
    assert "hatchling" in bs["requires"]
    assert bs["build-backend"] == "hatchling.build"


def test_wheel_packages_contains_src_cezam_lib():
    """Vérifie que le wheel mappe vers src/cezam_lib."""
    data = _load_pyproject()
    packages = data["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"]
    assert "src/cezam_lib" in packages


def test_runtime_dependencies():
    """Vérifie que toutes les dépendances attendues sont présentes."""
    data = _load_pyproject()
    deps = data["project"]["dependencies"]
    dep_names = [d.split(">")[0].split("=")[0].split("<")[0].strip() for d in deps]

    expected = [
        "minio",
        "pika",
        "opentelemetry-api",
        "opentelemetry-sdk",
        "opentelemetry-exporter-otlp",
        "opentelemetry-instrumentation",
        "pydantic",
    ]
    for name in expected:
        assert name in dep_names, f"Dépendance manquante : {name}"


def test_dev_dependencies():
    """Vérifie que les dépendances de dev incluent pytest, pytest-cov, hypothesis, ruff."""
    data = _load_pyproject()
    dev_deps = data["dependency-groups"]["dev"]
    dev_names = [d.split(">")[0].split("=")[0].split("<")[0].strip() for d in dev_deps]

    expected = ["pytest", "pytest-cov", "hypothesis", "ruff"]
    for name in expected:
        assert name in dev_names, f"Dépendance dev manquante : {name}"


# --- Tests de validation du .gitlab-ci.yml ---

GITLAB_CI_PATH = Path(__file__).resolve().parent.parent / ".gitlab-ci.yml"


def _load_gitlab_ci() -> dict:
    """Charge et parse le .gitlab-ci.yml."""
    with open(GITLAB_CI_PATH) as f:
        return yaml.safe_load(f)


def test_gitlab_ci_is_parsable():
    """Vérifie que le .gitlab-ci.yml est parsable."""
    data = _load_gitlab_ci()
    assert isinstance(data, dict)


def test_gitlab_ci_stages():
    """Vérifie que les stages sont lint, test, build, publish dans cet ordre."""
    data = _load_gitlab_ci()
    assert data["stages"] == ["lint", "test", "build", "publish"]


def test_gitlab_ci_image():
    """Vérifie que l'image de base est python:3.11-slim."""
    data = _load_gitlab_ci()
    assert data["image"] == "python:3.11-slim"


def test_gitlab_ci_cache_config():
    """Vérifie que le cache contient une clé et les paths .venv/ et .uv-cache/."""
    data = _load_gitlab_ci()
    cache = data["cache"]
    assert "key" in cache
    assert ".venv/" in cache["paths"]
    assert ".uv-cache/" in cache["paths"]


def test_gitlab_ci_lint_job():
    """Vérifie le job lint : stage, script et rules."""
    data = _load_gitlab_ci()
    lint = data["lint"]
    assert lint["stage"] == "lint"
    assert any("ruff check" in cmd for cmd in lint["script"])
    assert "rules" in lint
    assert len(lint["rules"]) > 0


def test_gitlab_ci_test_job():
    """Vérifie le job test : stage, script pytest --cov et rules."""
    data = _load_gitlab_ci()
    test = data["test"]
    assert test["stage"] == "test"
    assert any("pytest --cov" in cmd for cmd in test["script"])
    assert "rules" in test
    assert len(test["rules"]) > 0


def test_gitlab_ci_build_job():
    """Vérifie le job build : stage, script uv build, artifacts et rules."""
    data = _load_gitlab_ci()
    build = data["build"]
    assert build["stage"] == "build"
    assert any("uv build" in cmd for cmd in build["script"])
    assert "dist/" in build["artifacts"]["paths"]
    assert "rules" in build
    assert len(build["rules"]) > 0


def test_gitlab_ci_publish_job():
    """Vérifie le job publish : stage, script uv publish, dependencies et rules."""
    data = _load_gitlab_ci()
    publish = data["publish"]
    assert publish["stage"] == "publish"
    assert any("uv publish" in cmd for cmd in publish["script"])
    assert "build" in publish["dependencies"]
    assert "rules" in publish
    assert len(publish["rules"]) > 0


def test_gitlab_ci_before_script():
    """Vérifie que before_script installe uv et exécute uv sync."""
    data = _load_gitlab_ci()
    before = data["before_script"]
    assert any("uv" in cmd for cmd in before)
    assert any("uv sync" in cmd for cmd in before)


# --- Test property-based : cohérence Gitflow ---


GITFLOW_STRATEGY = {
    "feature": {"lint", "test"},
    "hotfix": {"lint", "test"},
    "develop": {"lint", "test", "build"},
    "main": {"lint", "test", "build"},
    "release": {"lint", "test", "build"},
    "tag": {"lint", "test", "build", "publish"},
    "merge_request": {"lint", "test"},
}


def _extract_regex_from_ruby_literal(pattern: str) -> re.Pattern:
    """Extrait et compile une regex depuis un littéral Ruby /.../.

    GitLab CI utilise la syntaxe Ruby : /^(feature|hotfix)\\//
    Le YAML désérialise les backslashes, donc \\/ devient \\/ en Python.
    On extrait le contenu entre le premier et le dernier /.
    """
    # Le premier caractère est '/', le dernier aussi
    # On prend tout entre les deux
    inner = pattern[1:-1]
    # Les \\/ dans le YAML représentent un / littéral échappé en Ruby regex
    # Python re n'a pas besoin d'échapper /, on remplace \\/ par /
    inner = inner.replace("\\/", "/")
    return re.compile(inner)


def _rule_matches(rule_if: str, context: dict) -> bool:
    """Évalue si une rule GitLab CI matche le contexte donné.

    Args:
        rule_if: Expression de la rule (ex: '$CI_COMMIT_BRANCH == "develop"')
        context: Variables CI simulées.
    """
    rule_if = rule_if.strip()

    # $VAR =~ /regex/
    match = re.match(r'^\$(\w+)\s*=~\s*(/.+/)$', rule_if)
    if match:
        var_name = match.group(1)
        pattern = match.group(2)
        var_value = context.get(var_name)
        if var_value is None:
            return False
        regex = _extract_regex_from_ruby_literal(pattern)
        return regex.search(var_value) is not None

    # $VAR == "value"
    match = re.match(r'^\$(\w+)\s*==\s*"([^"]*)"$', rule_if)
    if match:
        var_name = match.group(1)
        expected = match.group(2)
        var_value = context.get(var_name)
        return var_value == expected

    # $VAR (variable is set / truthy)
    match = re.match(r'^\$(\w+)$', rule_if)
    if match:
        var_name = match.group(1)
        return bool(context.get(var_name))

    return False


def _job_would_run(job_config: dict, context: dict) -> bool:
    """Détermine si un job GitLab CI s'exécuterait dans le contexte donné."""
    rules = job_config.get("rules", [])
    for rule in rules:
        if "if" in rule and _rule_matches(rule["if"], context):
            return True
    return False


def _get_executed_jobs(ci_config: dict, context: dict) -> set:
    """Retourne l'ensemble des jobs qui s'exécuteraient pour un contexte CI donné."""
    # Les clés spéciales GitLab CI qui ne sont pas des jobs
    reserved_keys = {"image", "stages", "variables", "cache", "before_script", "after_script", "default"}
    jobs = set()
    for key, value in ci_config.items():
        if key in reserved_keys or not isinstance(value, dict):
            continue
        if _job_would_run(value, context):
            jobs.add(key)
    return jobs


def _build_context(branch_type: str, branch_name: str) -> dict:
    """Construit le contexte de variables CI pour un type de branche donné."""
    if branch_type == "tag":
        return {"CI_COMMIT_TAG": branch_name}
    elif branch_type == "merge_request":
        return {"CI_MERGE_REQUEST_IID": "1"}
    else:
        return {"CI_COMMIT_BRANCH": branch_name}


# Stratégies Hypothesis pour générer des noms de branches
_safe_text = st.text(
    alphabet=st.characters(whitelist_categories=("L", "N"), whitelist_characters="-_"),
    min_size=1,
    max_size=30,
)

branch_strategies = {
    "feature": _safe_text.map(lambda s: f"feature/{s}"),
    "hotfix": _safe_text.map(lambda s: f"hotfix/{s}"),
    "develop": st.just("develop"),
    "main": st.just("main"),
    "release": _safe_text.map(lambda s: f"release/{s}"),
    "tag": st.tuples(
        st.integers(0, 99), st.integers(0, 99), st.integers(0, 99)
    ).map(lambda t: f"v{t[0]}.{t[1]}.{t[2]}"),
    "merge_request": st.just("merge_request"),
}

# Stratégie composite : génère un (branch_type, branch_name)
branch_type_and_name = st.one_of([
    strategy.map(lambda name, bt=bt: (bt, name))
    for bt, strategy in branch_strategies.items()
])


@given(branch_info=branch_type_and_name)
@settings(max_examples=100)
def test_gitflow_branch_executes_correct_stages(branch_info):
    """Vérifie que chaque type de branche Gitflow déclenche les bons stages."""
    branch_type, branch_name = branch_info

    ci_config = _load_gitlab_ci()
    context = _build_context(branch_type, branch_name)
    executed_jobs = _get_executed_jobs(ci_config, context)
    expected_jobs = GITFLOW_STRATEGY[branch_type]

    assert executed_jobs == expected_jobs, (
        f"branch_type={branch_type}, branch_name={branch_name!r}: "
        f"exécutés={executed_jobs}, attendus={expected_jobs}"
    )


# --- Tests de validation du README.md ---

README_PATH = Path(__file__).resolve().parent.parent / "README.md"


def _load_readme() -> str:
    """Charge le contenu du README.md."""
    return README_PATH.read_text(encoding="utf-8")


def test_readme_exists():
    """Vérifie que le README.md existe à la racine du projet."""
    assert README_PATH.exists(), "README.md introuvable à la racine du projet"


def test_readme_contains_introduction():
    """Vérifie que le README contient la description de cezam-lib et les sous-packages."""
    content = _load_readme()
    assert "cezam-lib" in content
    assert "cezam_shared" in content
    assert "pipeline_template" in content


def test_readme_contains_installation():
    """Vérifie que le README contient les commandes d'installation."""
    content = _load_readme()
    assert "uv add cezam-lib" in content
    assert "pip install cezam-lib" in content


def test_readme_contains_cezam_shared_components():
    """Vérifie que le README documente les composants de cezam_shared."""
    content = _load_readme()
    expected = [
        "MinIOClient",
        "DatalakeClient",
        "SourceClient",
        "datalake_paths",
        "RabbitMQPublisher",
        "RabbitMQConsumer",
        "setup_otel",
    ]
    for component in expected:
        assert component in content, f"Composant manquant dans le README : {component}"


def test_readme_contains_pipeline_template_components():
    """Vérifie que le README documente les composants de pipeline_template."""
    content = _load_readme()
    expected = ["BasePipeline", "DataExtractor", "PipelineMessage", "FusionMessage"]
    for component in expected:
        assert component in content, f"Composant manquant dans le README : {component}"


def test_readme_contains_imports():
    """Vérifie que le README montre les imports depuis cezam_lib."""
    content = _load_readme()
    assert "from cezam_lib.cezam_shared import" in content
    assert "from cezam_lib.pipeline_template import" in content


def test_readme_contains_exceptions():
    """Vérifie que le README documente les exceptions disponibles."""
    content = _load_readme()
    expected = ["MinIOError", "RabbitMQError", "RetryableError", "NonRetryableError"]
    for exc in expected:
        assert exc in content, f"Exception manquante dans le README : {exc}"


def test_readme_contains_otel_config():
    """Vérifie que le README documente la configuration OpenTelemetry."""
    content = _load_readme()
    expected = ["setup_otel", "inject_trace_context", "extract_trace_context"]
    for func in expected:
        assert func in content, f"Fonction OTel manquante dans le README : {func}"


def test_readme_contains_dev_commands():
    """Vérifie que le README contient les commandes de développement local."""
    content = _load_readme()
    assert "uv sync" in content
    assert "uv run pytest" in content


def test_readme_contains_python_version():
    """Vérifie que le README indique Python >= 3.11."""
    content = _load_readme()
    assert "3.11" in content
