"""Tests para metabase_mbql_from_source_card."""

from __future__ import annotations

import sys
import os
import re

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from metabase.metabase_mbql_from_source_card import metabase_mbql_from_source_card


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

UUID_RE = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
    re.IGNORECASE,
)


def _collect_dicts(obj: object) -> list[dict]:
    """Recorre recursivamente obj y devuelve todos los dicts encontrados."""
    result: list[dict] = []
    if isinstance(obj, dict):
        result.append(obj)
        for v in obj.values():
            result.extend(_collect_dicts(v))
    elif isinstance(obj, list):
        for item in obj:
            result.extend(_collect_dicts(item))
    return result


def _all_meta_dicts_have_uuid(obj: object) -> tuple[bool, list[dict]]:
    """Verifica que todos los dicts de metadatos MBQL tienen lib/uuid valido.

    Los dicts de metadatos son aquellos que tienen exactamente el rol de
    opciones/meta en MBQL: dicts que NO son stages (no tienen lib/type con
    valor mbql.stage/*) y tampoco son el objeto raiz. En la practica son los
    dicts de posicion 2 en nodos [op, meta, ...] y similares.

    Devuelve (ok, lista_de_dicts_sin_uuid).
    """
    # Dicts que se consideran estructurales y no necesitan lib/uuid
    _STRUCTURAL_LIB_TYPES = {
        "mbql/query",
        "mbql.stage/mbql",
        "mbql/join",
    }

    missing: list[dict] = []
    for d in _collect_dicts(obj):
        lib_type = d.get("lib/type")
        # Omitir dicts estructurales (stages, joins, query raiz)
        if lib_type in _STRUCTURAL_LIB_TYPES:
            continue
        # Omitir dicts vacios (pueden aparecer en filtros raw antes de inyectar)
        if not d:
            continue
        # Cualquier otro dict MBQL (meta de field, de operacion, de condition...)
        # debe tener lib/uuid
        uid = d.get("lib/uuid")
        if uid is None or not UUID_RE.match(str(uid)):
            missing.append(d)
    return (len(missing) == 0, missing)


# ---------------------------------------------------------------------------
# Test 1: source-card y database presentes en el output
# ---------------------------------------------------------------------------


def test_source_card_y_database_presentes():
    """source-card y database estan presentes en el output."""
    q = metabase_mbql_from_source_card(
        database_id=6,
        source_card_id=5305,
        aggregations=[{"op": "count"}],
    )
    assert q["database"] == 6, f"database esperado 6, got {q['database']}"
    assert q["lib/type"] == "mbql/query"
    stage0 = q["stages"][0]
    assert stage0["source-card"] == 5305, (
        f"source-card esperado 5305, got {stage0.get('source-card')}"
    )


# ---------------------------------------------------------------------------
# Test 2: todos los dicts anidados tienen lib/uuid
# ---------------------------------------------------------------------------


def test_todos_los_dicts_tienen_lib_uuid():
    """todos los dicts anidados tienen lib/uuid."""
    q = metabase_mbql_from_source_card(
        database_id=6,
        source_card_id=5305,
        aggregations=[
            {"op": "sum", "field": "PrecioVenta", "base_type": "type/Decimal"},
            {"op": "sum", "field": "PrecioCompra", "base_type": "type/Decimal"},
        ],
        joins=[
            {
                "alias": "Centros - idCentro",
                "source_card_id": 4076,
                "fields": "none",
                "local_field": "idCentro",
                "local_base_type": "type/Text",
                "foreign_field_id": 17316,
                "foreign_base_type": "type/Text",
            },
        ],
        filters=[
            [
                "not-empty",
                {},
                [
                    "field",
                    {"base-type": "type/Text"},
                    "Centros - idCentro__Companies__name",
                ],
            ]
        ],
        expressions=[
            {
                "name": "Margen",
                "expr": {
                    "op": "coalesce",
                    "args": [
                        {
                            "op": "/",
                            "args": [
                                {
                                    "op": "-",
                                    "args": [
                                        {"field": "sum"},
                                        {"field": "sum_2"},
                                    ],
                                },
                                {"field": "sum"},
                            ],
                        },
                        0,
                    ],
                },
            }
        ],
    )

    # Verificar que todos los dicts de metadatos MBQL tienen lib/uuid
    ok, missing = _all_meta_dicts_have_uuid(q["stages"])
    assert ok, (
        f"Hay {len(missing)} dicts de meta sin lib/uuid valido. Primeros 3: {missing[:3]}"
    )


# ---------------------------------------------------------------------------
# Test 3: agregaciones producen forma correcta op+meta+field
# ---------------------------------------------------------------------------


def test_agregaciones_producen_forma_correcta():
    """agregaciones producen forma correcta op+meta+field."""
    q = metabase_mbql_from_source_card(
        database_id=6,
        source_card_id=100,
        aggregations=[
            {"op": "sum", "field": "Ventas", "base_type": "type/Decimal"},
            {"op": "count"},
            {"op": "avg", "field": "Precio"},
        ],
    )
    aggs = q["stages"][0]["aggregation"]
    assert len(aggs) == 3

    # sum: ["sum", {lib/uuid}, ["field", {base-type, lib/uuid}, "Ventas"]]
    sum_agg = aggs[0]
    assert sum_agg[0] == "sum", f"Esperaba 'sum', got {sum_agg[0]}"
    assert isinstance(sum_agg[1], dict) and "lib/uuid" in sum_agg[1]
    assert isinstance(sum_agg[2], list) and sum_agg[2][0] == "field"
    assert sum_agg[2][2] == "Ventas"
    assert sum_agg[2][1].get("base-type") == "type/Decimal"

    # count sin campo: ["count", {lib/uuid}]
    count_agg = aggs[1]
    assert count_agg[0] == "count"
    assert len(count_agg) == 2, f"count no deberia tener campo, got {count_agg}"

    # avg con campo
    avg_agg = aggs[2]
    assert avg_agg[0] == "avg"
    assert avg_agg[2][2] == "Precio"


# ---------------------------------------------------------------------------
# Test 4: segunda stage existe solo cuando se pasan expressions
# ---------------------------------------------------------------------------


def test_segunda_stage_solo_con_expressions():
    """segunda stage existe solo cuando se pasan expressions."""
    # Sin expressions: una sola stage
    q_sin = metabase_mbql_from_source_card(
        database_id=6,
        source_card_id=100,
        aggregations=[{"op": "count"}],
    )
    assert len(q_sin["stages"]) == 1, (
        f"Sin expressions esperaba 1 stage, got {len(q_sin['stages'])}"
    )

    # Con expressions: dos stages
    q_con = metabase_mbql_from_source_card(
        database_id=6,
        source_card_id=100,
        aggregations=[{"op": "count"}],
        expressions=[
            {
                "name": "DobleConteo",
                "expr": {"op": "*", "args": [{"field": "count"}, 2]},
            }
        ],
    )
    assert len(q_con["stages"]) == 2, (
        f"Con expressions esperaba 2 stages, got {len(q_con['stages'])}"
    )
    stage1 = q_con["stages"][1]
    assert "expressions" in stage1, "stage1 debe tener key 'expressions'"
    expr = stage1["expressions"][0]
    assert expr[0] == "*", f"Esperaba op '*', got {expr[0]}"
    assert expr[1].get("lib/expression-name") == "DobleConteo"
