#!/usr/bin/env python3
"""
tests/test_paridad_rust.py - Validacion cruzada de TRES vias (item
runtime alternativo, v41.36 del CHANGELOG): el binario de Rust
(`runtime_rust/`), `ast_sipi.py` (Python) y `Interprete.evaluar_expresion`
del motor de produccion real deben dar EXACTAMENTE el mismo resultado
para el mismo lote de expresiones -- incluyendo el caso de division por
cero.

El CHANGELOG documentaba este archivo desde la v41.36 pero nunca habia
llegado al paquete (ni tampoco `runtime_rust/`, que se agrego junto con
este archivo en la misma pasada). Alcance honesto, igual que
`runtime_rust/README.md`: esto compara evaluacion de EXPRESIONES
sueltas -- no programas .sipi completos con control de flujo.

Requiere tener 'cargo' instalado para compilar el binario de Rust la
primera vez que se corre (`cargo build --release` en `runtime_rust/`).
Si no esta disponible, este archivo se salta solo (no rompe el resto
de la suite) con un aviso explicito -- misma filosofia que el resto
del proyecto: mejor un aviso claro que un fallo confuso en una maquina
que nunca instalo el toolchain de Rust.

Uso:
    python3 tests/test_paridad_rust.py
"""
import json
import os
import shutil
import subprocess
import sys
import unittest

AQUI = os.path.dirname(os.path.abspath(__file__))
CARPETA_SRC = os.path.dirname(AQUI)
RAIZ_REPO = os.path.dirname(CARPETA_SRC)
CARPETA_RUST = os.path.join(RAIZ_REPO, "runtime_rust")

sys.path.insert(0, CARPETA_SRC)
from ast_sipi import evaluar_texto, ErrorEvaluacion  # noqa: E402

try:
    import sipi as motor_sipi
except ImportError:
    import sipi_protegido as motor_sipi  # noqa: F401


def _cargo_disponible():
    return shutil.which("cargo") is not None and os.path.isdir(CARPETA_RUST)


def _compilar_binario_rust():
    subprocess.run(
        ["cargo", "build", "--release", "--quiet"],
        cwd=CARPETA_RUST,
        check=True,
    )
    binario = os.path.join(CARPETA_RUST, "target", "release", "sipi_runtime_rust")
    if os.name == "nt":
        binario += ".exe"
    return binario


def _correr_rust(binario, expresion, variables=None):
    args = [binario, expresion]
    for nombre, valor in (variables or {}).items():
        if isinstance(valor, bool):
            args.append(f"{nombre}={'verdadero' if valor else 'falso'}")
        elif isinstance(valor, str):
            args.append(f'{nombre}="{valor}"')
        elif valor is None:
            args.append(f"{nombre}=nulo")
        else:
            args.append(f"{nombre}={valor}")
    resultado = subprocess.run(args, capture_output=True, text=True)
    return json.loads(resultado.stdout.strip())


def _interprete_de_produccion(variables=None):
    interprete = motor_sipi.Interprete("_paridad_rust_.sipi")
    if variables:
        interprete.entorno.variables.update(variables)
    return interprete


EXPRESIONES = [
    ("2 + 3", None),
    ("6 * 7", None),
    ("20 / 4", None),
    ("2 + 3 * 4", None),
    ("(2 + 3) * 4", None),
    ("5 > 3", None),
    ("5 == 5", None),
    ('"Puntaje: " + 10', None),
    ("x + z", {"x": 2, "z": 3}),
]


@unittest.skipUnless(_cargo_disponible(), "cargo no esta instalado o falta runtime_rust/ -- se salta la paridad de 3 vias")
class TestParidadTresVias(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.binario = _compilar_binario_rust()

    def test_paridad_expresiones(self):
        for expr, variables in EXPRESIONES:
            with self.subTest(expr=expr):
                esperado_rust = _correr_rust(self.binario, expr, variables)
                self.assertTrue(esperado_rust["ok"], f"Rust fallo en {expr!r}: {esperado_rust}")
                valor_rust = esperado_rust["valor"]

                valor_python = evaluar_texto(expr, variables=variables)

                interprete = _interprete_de_produccion(variables)
                valor_produccion = interprete.evaluar_expresion(expr)

                self.assertEqual(valor_rust, valor_python, f"Rust vs Python diverge en: {expr!r}")
                self.assertEqual(valor_python, valor_produccion, f"Python vs produccion diverge en: {expr!r}")

    def test_paridad_division_por_cero(self):
        resultado_rust = _correr_rust(self.binario, "1 / 0")
        self.assertFalse(resultado_rust["ok"])

        with self.assertRaises(ErrorEvaluacion):
            evaluar_texto("1 / 0")

        interprete = _interprete_de_produccion()
        with self.assertRaises(Exception):
            interprete.evaluar_expresion("1 / 0")


if __name__ == "__main__":
    unittest.main(verbosity=2)
