#!/usr/bin/env python3
"""
tests/test_robustez.py - Tests de robustez frente a entradas raras o
malformadas: NO tienen relacion con la proteccion de distribucion
(bytecode/ofuscacion) -- eso se audita aparte y a proposito no vive en
el repositorio publico. Esto es sobre el intérprete en si: que un
programa .sipi mal escrito, con datos raros o recursion profunda, de
un error claro en vez de colgarse o crashear con una traceback cruda
de Python.

Uso:
    python3 tests/test_robustez.py
"""
import os
import sys
import io
import contextlib
import tempfile
import unittest

AQUI = os.path.dirname(os.path.abspath(__file__))
CARPETA_SRC = os.path.dirname(AQUI)
sys.path.insert(0, CARPETA_SRC)

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


def correr(codigo_sipi):
    with tempfile.TemporaryDirectory(prefix="sipi_test_") as carpeta:
        ruta = os.path.join(carpeta, "programa.sipi")
        with open(ruta, "w", encoding="utf-8") as f:
            f.write(codigo_sipi)
        salida = io.StringIO()
        interprete = motor_sipi.Interprete(ruta)
        with contextlib.redirect_stdout(salida):
            interprete.ejecutar()
        return salida.getvalue()


class TestBloquesSinCerrar(unittest.TestCase):
    def test_si_sin_fin_se_cierra_por_indentacion(self):
        # 'fin' es opcional si la indentacion es consistente (ver
        # docs/SYNTAX.md) -- un 'si' sin 'fin' pero bien indentado es
        # valido, no un error.
        salida = correr('programa "t"\nsi 1 == 1\n    decir "hola"\n')
        self.assertEqual(salida.strip(), "hola")

    def test_bloque_mal_indentado_y_sin_fin_da_sipierror(self):
        # Sin 'fin' Y sin indentacion que lo delimite, el parser no
        # tiene forma de saber donde termina el bloque -- tiene que
        # avisar con un error claro, no comerse la falta de estructura
        # en silencio ni crashear con una excepcion cruda de Python.
        codigo = 'programa "t"\nsi 1 == 1\ndecir "hola"\ndecir "chau"\n'
        try:
            correr(codigo)
        except motor_sipi.SiPiError:
            pass  # esperado: el interprete detecta el problema y avisa
        except Exception as e:
            self.fail(f"Excepcion de Python cruda en vez de SiPiError: {type(e).__name__}: {e}")


class TestVariablesInexistentes(unittest.TestCase):
    def test_variable_no_declarada_no_cuelga(self):
        # No tiene que colgarse ni tirar una excepcion no controlada de
        # Python (KeyError/NameError crudos) -- SiPiError es aceptable,
        # cualquier otra cosa indica un bug de manejo de errores.
        try:
            correr('programa "t"\ndecir variable_que_no_existe\n')
        except motor_sipi.SiPiError:
            pass  # esperado
        except Exception as e:
            self.fail(f"Excepcion de Python cruda en vez de SiPiError: {type(e).__name__}: {e}")


class TestRecursionProfunda(unittest.TestCase):
    def test_recursion_muy_profunda_no_cuelga_el_proceso(self):
        # Recursion sin caso base: tiene que fallar (stack overflow /
        # RecursionError de Python, o un limite propio de SiPi) en vez
        # de colgarse indefinidamente.
        codigo = (
            'programa "t"\nfuncion f(n: entero) -> entero\n'
            '    llamar_valor f(n + 1) -> r\n    devolver r\nfin\n'
            'llamar_valor f(0) -> resultado\n'
        )
        with self.assertRaises((RecursionError, motor_sipi.SiPiError)):
            correr(codigo)


class TestEntradaConCaracteresRaros(unittest.TestCase):
    def test_texto_unicode_no_ascii(self):
        salida = correr('programa "t"\ndecir "Ñandú, café, 日本語, हिन्दी"\n')
        self.assertIn("Ñandú", salida)
        self.assertIn("日本語", salida)

    def test_archivo_vacio_no_crashea(self):
        # Un .sipi vacio (o solo con la declaracion de 'programa') no
        # tiene que crashear con una excepcion no controlada.
        try:
            correr('programa "t"\n')
        except motor_sipi.SiPiError:
            pass
        except Exception as e:
            self.fail(f"Excepcion de Python cruda con archivo casi vacio: {type(e).__name__}: {e}")


class TestDivisionYLimites(unittest.TestCase):
    def test_division_por_cero_da_error_claro(self):
        with self.assertRaises(motor_sipi.SiPiError):
            correr('programa "t"\nvariable x = 10 / 0\ndecir x\n')

    def test_acceso_a_indice_no_crashea_con_error_de_python(self):
        # Independientemente de si esta sintaxis puntual de indexado
        # esta soportada, no tiene que crashear con una excepcion cruda
        # de Python (IndexError/KeyError sin envolver) ni colgarse.
        try:
            correr('programa "t"\nvariable l = [1, 2, 3]\ndecir l[10]\n')
        except motor_sipi.SiPiError:
            pass
        except Exception as e:
            self.fail(f"Excepcion de Python cruda en vez de SiPiError: {type(e).__name__}: {e}")


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