"""Tests para el backend sdcpp_python: sdcpp_python_load y sdcpp_python_generate."""

from __future__ import annotations

import sys
import os

import pytest

# Ajustar path para importar desde python/functions/ml/
_ML_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, _ML_PATH)

# Importacion lazy — salta todos los tests si el package no esta instalado.
pytest.importorskip(
    "stable_diffusion_cpp",
    reason="stable_diffusion_cpp no instalado — skip tests sdcpp_python backend",
)

# El paquete usa modulos hermanos sin prefijo (model_ref, generation_config...).
# Para evitar el double-import problem, mapeamos los aliases antes de importar
# las funciones bajo test.
import ml.model_ref as _mref_module
import ml.generation_config as _gcfg_module
import ml.image_gen_result as _igr_module

for _alias, _mod in [
    ("model_ref", _mref_module),
    ("generation_config", _gcfg_module),
    ("image_gen_result", _igr_module),
]:
    if _alias not in sys.modules:
        sys.modules[_alias] = _mod  # type: ignore[assignment]

from ml.model_ref import ModelRef
from ml.generation_config import GenerationConfig
from ml.image_gen_result import ImageGenResult
from ml.sdcpp_python_load import sdcpp_python_load, _clear_sd_cache
from ml.sdcpp_python_generate import sdcpp_python_generate


# ---------------------------------------------------------------------------
# Constantes
# ---------------------------------------------------------------------------

SD_TURBO_SAFETENSORS = (
    "/home/lucas/vaults/imagegen_models/diffusers/sd-turbo/sd_turbo.safetensors"
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session")
def sd_turbo_model() -> ModelRef:
    """ModelRef apuntando al safetensors de SD Turbo en local."""
    if not os.path.isfile(SD_TURBO_SAFETENSORS):
        pytest.skip(
            f"Modelo SD Turbo no encontrado en {SD_TURBO_SAFETENSORS}"
        )
    return ModelRef(
        name="sd-turbo",
        model_type="sd15",
        quantization="fp16",
        path=SD_TURBO_SAFETENSORS,
    )


@pytest.fixture(scope="session")
def loaded_sd(sd_turbo_model: ModelRef):
    """StableDiffusion cargado una sola vez para toda la sesion de tests."""
    _clear_sd_cache()
    sd = sdcpp_python_load(sd_turbo_model, n_threads=-1, wtype="default")
    yield sd
    _clear_sd_cache()


@pytest.fixture(scope="session")
def sd_turbo_cfg(sd_turbo_model: ModelRef) -> GenerationConfig:
    """GenerationConfig para SD Turbo: 512x512, 4 steps, euler_a, seed=42."""
    return GenerationConfig(
        prompt="a simple red apple on a white table",
        negative_prompt=None,
        seed=42,
        steps=4,
        cfg_scale=1.0,
        sampler="euler_a",
        width=512,
        height=512,
        model=sd_turbo_model,
        loras=[],
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


def test_load_retorna_objeto(loaded_sd) -> None:
    """load retorna objeto StableDiffusion"""
    from stable_diffusion_cpp import StableDiffusion

    assert isinstance(loaded_sd, StableDiffusion), (
        f"Se esperaba StableDiffusion, se obtuvo {type(loaded_sd)}"
    )


def test_load_caches(sd_turbo_model: ModelRef, loaded_sd) -> None:
    """segunda llamada retorna instancia cacheada"""
    import time

    t0 = time.perf_counter()
    sd2 = sdcpp_python_load(sd_turbo_model, n_threads=-1, wtype="default")
    elapsed = time.perf_counter() - t0

    assert sd2 is loaded_sd, "Segunda llamada debe retornar la misma instancia cacheada"
    assert elapsed < 0.5, (
        f"Segunda llamada tardo {elapsed:.3f}s — deberia ser inmediata (cache hit)"
    )


def test_generate_retorna_image_gen_result(
    loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
    """generate retorna ImageGenResult valido"""
    result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)

    assert isinstance(result, ImageGenResult), (
        f"Se esperaba ImageGenResult, se obtuvo {type(result)}"
    )
    assert result.image is not None, "result.image no debe ser None"


def test_duration_ms_mayor_que_cero(
    loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
    """duration_ms mayor que cero"""
    result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
    assert result.duration_ms > 0, (
        f"duration_ms debe ser > 0, se obtuvo {result.duration_ms}"
    )


def test_meta_backend_es_sdcpp_python(
    loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
    """meta backend es sdcpp_python"""
    result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
    assert result.meta.get("backend") == "sdcpp_python", (
        f"meta['backend'] debe ser 'sdcpp_python', se obtuvo {result.meta.get('backend')!r}"
    )
    assert result.meta.get("model") == sd_turbo_cfg.model.name
    assert result.meta.get("sampler") == sd_turbo_cfg.sampler
    assert result.meta.get("seed") == sd_turbo_cfg.seed


def test_vram_peak_mb_es_none(
    loaded_sd, sd_turbo_cfg: GenerationConfig
) -> None:
    """vram_peak_mb es None — sdcpp no expone medicion de VRAM"""
    result = sdcpp_python_generate(loaded_sd, sd_turbo_cfg)
    assert result.vram_peak_mb is None, (
        f"vram_peak_mb debe ser None, se obtuvo {result.vram_peak_mb}"
    )
