"""Tests del dispatcher multi-lang (issue 0033 fase A).

Verifica que el parser del manifest lee `lang`/`exec` correctamente
y que el wire protocol (stdin JSON / stdout JSON / exit code)
funciona identico para enrichers bash y python.

No probamos `lang: go` aqui — eso vive en los tests Go nativos del
issue 0034. Esta suite cubre el dispatcher como tal: que la
ramificacion de argv funciona y el contrato es estable.
"""
from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

from conftest import (
    PYTHON_BIN, base_ctx, list_entities, make_node,
)


def _write_dummy_enricher(tmp_path: Path, *, eid: str, lang: str,
                            exec_basename: str = "run",
                            applies_to: str = "[text]",
                            body: str = "") -> Path:
    """Crea un enricher dummy aislado en tmp_path/enrichers/<eid>/."""
    enr_dir = tmp_path / "enrichers" / eid
    enr_dir.mkdir(parents=True, exist_ok=True)

    manifest = (
        f"id: {eid}\n"
        f"name: \"{eid}\"\n"
        f"description: \"dummy {lang} enricher para tests\"\n"
        f"applies_to: {applies_to}\n"
        f"lang: {lang}\n"
        f"exec: {exec_basename}\n"
    )
    (enr_dir / "manifest.yaml").write_text(manifest, encoding="utf-8")

    if lang == "python":
        ext = ".py"
        full_body = body or (
            "import json, sys\n"
            "ctx = json.loads(sys.stdin.read())\n"
            "sys.stderr.write('PROGRESS:1.0 done\\n')\n"
            "print(json.dumps({'ok': True, 'lang': 'python', "
            "'node_id': ctx.get('node_id', '')}))\n"
        )
    elif lang == "bash":
        ext = ".sh"
        full_body = body or (
            "#!/usr/bin/env bash\n"
            "ctx=$(cat)\n"
            "echo 'PROGRESS:1.0 done' >&2\n"
            'echo "{\\"ok\\": true, \\"lang\\": \\"bash\\"}"\n'
        )
    elif lang == "go":
        # Para tests del loader que verifican el caso "binario
        # ausente" — solo escribimos el manifest, sin script ni
        # binario.
        return enr_dir
    else:
        raise ValueError(f"lang {lang} no soportado en este test")

    script = enr_dir / f"{exec_basename}{ext}"
    script.write_text(full_body, encoding="utf-8")
    if lang == "bash":
        os.chmod(script, 0o755)
    return enr_dir


# ---------------------------------------------------------------------------
# Parser del manifest — verifica que lang/exec se reconocen
# ---------------------------------------------------------------------------

def test_parser_default_lang_is_python_when_omitted(tmp_path):
    """Manifest sin `lang` se considera `python` por retrocompat."""
    enr = _write_dummy_enricher(tmp_path, eid="legacy", lang="python")
    # Quitamos las lineas lang/exec del manifest para emular un manifest viejo.
    manifest = enr / "manifest.yaml"
    text = manifest.read_text()
    text = "\n".join(l for l in text.splitlines()
                     if not l.startswith("lang:") and not l.startswith("exec:"))
    manifest.write_text(text + "\n", encoding="utf-8")

    # Reusamos el binario graph_explorer indirectamente via un test
    # caja-blanca: parseamos con yq + verificamos comportamiento via
    # subprocess de run.py. El parser C++ no es directamente
    # accesible desde pytest, por eso lo testeamos transitivamente:
    # corremos el dummy python y verificamos que su run.py se
    # encuentra. El loader C++ solo deja el spec con run_path no
    # vacio si encuentra el archivo.
    py_script = enr / "run.py"
    assert py_script.exists()


def test_parser_reads_lang_bash(tmp_path):
    enr = _write_dummy_enricher(tmp_path, eid="dummy_bash", lang="bash")
    manifest = (enr / "manifest.yaml").read_text()
    assert "lang: bash" in manifest


# ---------------------------------------------------------------------------
# Wire protocol — Python (regresion del comportamiento existente)
# ---------------------------------------------------------------------------

def test_python_dummy_enricher_obeys_wire_protocol(tmp_path):
    enr = _write_dummy_enricher(tmp_path, eid="dummy_py", lang="python")

    ctx = json.dumps({
        "node_id": "n1", "node_name": "x", "node_type": "text",
        "metadata": {}, "ops_db_path": "", "app_dir": str(tmp_path),
        "cache_dir": str(tmp_path / "cache"),
        "registry_root": "", "params": {},
    })
    proc = subprocess.run(
        [str(PYTHON_BIN), str(enr / "run.py")],
        input=ctx, capture_output=True, text=True, timeout=10,
    )
    assert proc.returncode == 0, proc.stderr
    assert "PROGRESS:1.0" in proc.stderr
    out = json.loads(proc.stdout.strip().splitlines()[-1])
    assert out == {"ok": True, "lang": "python", "node_id": "n1"}


# ---------------------------------------------------------------------------
# Wire protocol — Bash (la ruta nueva)
# ---------------------------------------------------------------------------

@pytest.mark.skipif(
    sys.platform.startswith("win") or not shutil.which("bash"),
    reason="test bash-only — saltado en Windows (el bash de WSL no acepta "
           "rutas Windows nativas) y en sistemas sin bash",
)
def test_bash_dummy_enricher_obeys_wire_protocol(tmp_path):
    enr = _write_dummy_enricher(tmp_path, eid="dummy_sh", lang="bash")

    ctx = json.dumps({"node_id": "n1", "ops_db_path": "", "params": {}})
    proc = subprocess.run(
        [shutil.which("bash"), str(enr / "run.sh")],
        input=ctx, capture_output=True, text=True, timeout=10,
    )
    assert proc.returncode == 0, proc.stderr
    assert "PROGRESS:1.0" in proc.stderr
    out = json.loads(proc.stdout.strip().splitlines()[-1])
    assert out == {"ok": True, "lang": "bash"}


# ---------------------------------------------------------------------------
# Comportamiento del loader: enricher Go sin binario queda disabled
# ---------------------------------------------------------------------------

def test_go_enricher_without_binary_is_disabled_in_load(tmp_path):
    """Un manifest con `lang: go` pero sin binario compilado debe
    quedar disabled. El test es indirecto — solo confirmamos que el
    layout esperado (manifest sin binario) es el caso real.

    El loader C++ marcara el spec como disabled. Esto se valida en
    integracion (smoke test del binario) pero no aqui — pytest no
    ejecuta el loader C++ directamente.
    """
    enr = _write_dummy_enricher(tmp_path, eid="dummy_go", lang="go")
    # Un enricher Go necesita <run> (Linux) o <run>.exe (Windows).
    # Como el dummy_go solo tiene manifest, no hay binario.
    files = sorted(p.name for p in enr.iterdir())
    assert files == ["manifest.yaml"], files
    # Si en el futuro alguien anade el binario, este test debera
    # actualizarse para verificar el flujo enabled tambien.


# ---------------------------------------------------------------------------
# Manifests con `lang: python` explicito leen igual que los implicitos
# ---------------------------------------------------------------------------

def test_existing_enrichers_keep_working_after_dispatcher(ops_db, app_dir,
                                                            registry_root):
    """Regresion: un enricher real del proyecto (extract_domain,
    `lang: python` por default) sigue funcionando con el flujo
    estandar del wire protocol."""
    make_node(ops_db, node_id="u1", name="ex", type_ref="Url",
              metadata={"url": "https://www.test.example/x"})
    from conftest import run_enricher
    ctx = base_ctx(ops_db=ops_db, app_dir=app_dir, registry_root=registry_root,
                   node_id="u1", node_name="ex", node_type="Url",
                   metadata={"url": "https://www.test.example/x"})
    rc, out, err = run_enricher("extract_domain", ctx)
    assert rc == 0, err
    domains = list_entities(ops_db, type_ref="Domain")
    assert any(d["name"] == "www.test.example" for d in domains)
