"""Tests de roundtrip JSON para genconfig_save_json y genconfig_load_json."""

import json
import os
import sys
import tempfile
import unittest

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

from ml.genconfig_save_json import genconfig_save_json
from ml.genconfig_load_json import genconfig_load_json
from ml.generation_config import GenerationConfig


def _make_cfg(**overrides):
    """Construye un GenerationConfig sintetico usando model_validate para evitar
    problemas de identidad de clase entre modulos pydantic separados."""
    defaults = dict(
        prompt="a forest at dusk",
        negative_prompt="blurry, low quality",
        seed=123,
        steps=25,
        cfg_scale=7.5,
        sampler="euler",
        width=512,
        height=512,
        model={"name": "runwayml/stable-diffusion-v1-5", "model_type": "sd15"},
        loras=[{"path": "/loras/detail.safetensors", "weight": 0.7}],
    )
    defaults.update(overrides)
    try:
        return GenerationConfig.model_validate(defaults)
    except AttributeError:
        from ml.model_ref import ModelRef
        from ml.lora_ref import LoraRef
        m = defaults.pop("model")
        if isinstance(m, dict):
            m = ModelRef(**m)
        loras = defaults.pop("loras", [])
        built = [LoraRef(**lr) if isinstance(lr, dict) else lr for lr in loras]
        return GenerationConfig(model=m, loras=tuple(built), **defaults)


class TestGenconfigJsonRoundtrip(unittest.TestCase):

    def test_save_escribe_archivo_json_valido_en_la_ruta_indicada(self):
        """save escribe archivo JSON valido en la ruta indicada"""
        cfg = _make_cfg()
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            saved = genconfig_save_json(cfg, path)
            self.assertTrue(os.path.isfile(saved))
            with open(saved, "r", encoding="utf-8") as f:
                data = json.load(f)
            self.assertIsInstance(data, dict)
            self.assertEqual(data["prompt"], "a forest at dusk")

    def test_save_crea_directorios_padre_si_no_existen(self):
        """save crea directorios padre si no existen"""
        cfg = _make_cfg()
        with tempfile.TemporaryDirectory() as tmpdir:
            nested = os.path.join(tmpdir, "a", "b", "c", "config.json")
            saved = genconfig_save_json(cfg, nested)
            self.assertTrue(os.path.isfile(saved))

    def test_json_contiene_claves_en_snake_case(self):
        """json contiene claves en snake_case"""
        cfg = _make_cfg()
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            genconfig_save_json(cfg, path)
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            # Claves deben ser snake_case (interoperabilidad con Go)
            expected_keys = {
                "prompt", "negative_prompt", "seed", "steps",
                "cfg_scale", "sampler", "width", "height", "model",
            }
            for key in expected_keys:
                self.assertIn(key, data, f"Clave snake_case faltante: {key}")
            # No debe haber camelCase
            self.assertNotIn("negativePrompt", data)
            self.assertNotIn("cfgScale", data)
            self.assertNotIn("numInferenceSteps", data)

    def test_roundtrip_preserva_campos_escalares(self):
        """roundtrip save→load preserva campos escalares"""
        cfg = _make_cfg()
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            genconfig_save_json(cfg, path)
            loaded = genconfig_load_json(path)
        self.assertEqual(loaded.prompt, cfg.prompt)
        self.assertEqual(loaded.negative_prompt, cfg.negative_prompt)
        self.assertEqual(loaded.seed, cfg.seed)
        self.assertEqual(loaded.steps, cfg.steps)
        self.assertAlmostEqual(loaded.cfg_scale, cfg.cfg_scale)
        self.assertEqual(loaded.sampler, cfg.sampler)
        self.assertEqual(loaded.width, cfg.width)
        self.assertEqual(loaded.height, cfg.height)

    def test_roundtrip_preserva_model_ref(self):
        """roundtrip preserva ModelRef"""
        cfg = _make_cfg(
            model={
                "name": "stabilityai/sdxl-base-1.0",
                "model_type": "sdxl",
                "quantization": "fp16",
                "path": "/models/sdxl.safetensors",
            }
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            genconfig_save_json(cfg, path)
            loaded = genconfig_load_json(path)
        self.assertEqual(loaded.model.name, "stabilityai/sdxl-base-1.0")
        self.assertEqual(loaded.model.model_type, "sdxl")
        self.assertEqual(loaded.model.quantization, "fp16")
        self.assertEqual(loaded.model.path, "/models/sdxl.safetensors")

    def test_roundtrip_preserva_loras(self):
        """roundtrip preserva lista de LoraRef"""
        cfg = _make_cfg(
            loras=[
                {"path": "/loras/a.safetensors", "weight": 0.8},
                {"path": "/loras/b.safetensors", "weight": 0.5},
            ]
        )
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            genconfig_save_json(cfg, path)
            loaded = genconfig_load_json(path)
        loaded_loras = list(loaded.loras)
        self.assertEqual(len(loaded_loras), 2)
        paths = [lr.path for lr in loaded_loras]
        self.assertIn("/loras/a.safetensors", paths)
        self.assertIn("/loras/b.safetensors", paths)

    def test_roundtrip_negative_prompt_none(self):
        """roundtrip con negative_prompt=None"""
        cfg = _make_cfg(negative_prompt=None)
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "config.json")
            genconfig_save_json(cfg, path)
            loaded = genconfig_load_json(path)
        self.assertIsNone(loaded.negative_prompt)

    def test_load_falla_con_file_not_found(self):
        """load lanza FileNotFoundError si el archivo no existe"""
        with self.assertRaises(FileNotFoundError):
            genconfig_load_json("/tmp/nonexistent_fn_registry_test_12345.json")

    def test_save_retorna_path_absoluto(self):
        """save retorna path absoluto aunque se pase path relativo"""
        cfg = _make_cfg()
        with tempfile.TemporaryDirectory() as tmpdir:
            abs_path = os.path.join(tmpdir, "cfg.json")
            result = genconfig_save_json(cfg, abs_path)
            self.assertTrue(os.path.isabs(result))


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