import json
from typing import Any, Dict

import pytest

from py.recipes.parsers.recipe_format import RecipeFormatParser
from py.config import config


class _FakeCache:
    def __init__(self, entries, version_index=None):
        self.raw_data = entries
        self.version_index = version_index or {}


class _FakeLoraScanner:
    def __init__(self, entries, version_index=None, has_hash_result=True):
        self._cache = _FakeCache(entries, version_index)
        self._has_hash_result = has_hash_result

    def has_hash(self, sha256):
        return self._has_hash_result

    async def get_cached_data(self):
        return self._cache


class _FakeRecipeScanner:
    def __init__(self, lora_scanner):
        self._lora_scanner = lora_scanner


async def _noop_metadata_provider():
    class Provider:
        async def get_model_version_info(self, version_id):
            return None, None

    return Provider()


def _parse(monkeypatch, recipe_metadata, recipe_scanner):
    monkeypatch.setattr(
        "py.recipes.parsers.recipe_format.get_default_metadata_provider",
        _noop_metadata_provider,
    )
    parser = RecipeFormatParser()
    metadata_text = f"Recipe metadata: {json.dumps(recipe_metadata)}"
    return parser.parse_metadata(metadata_text, recipe_scanner=recipe_scanner)


@pytest.mark.asyncio
async def test_recipe_format_parser_populates_checkpoint(monkeypatch):
    checkpoint_info = {
        "id": 777111,
        "modelId": 333222,
        "model": {"name": "Z Image", "type": "checkpoint"},
        "name": "Turbo",
        "images": [{"url": "https://image.civitai.com/checkpoints/original=true"}],
        "baseModel": "sdxl",
        "downloadUrl": "https://civitai.com/api/download/checkpoint",
        "files": [
            {
                "type": "Model",
                "primary": True,
                "sizeKB": 2048,
                "name": "Z_Image_Turbo.safetensors",
                "hashes": {"SHA256": "ABC123FF"},
            }
        ],
    }

    async def fake_metadata_provider():
        class Provider:
            async def get_model_version_info(self, version_id):
                assert version_id == "777111"
                return checkpoint_info, None

        return Provider()

    monkeypatch.setattr(
        "py.recipes.parsers.recipe_format.get_default_metadata_provider",
        fake_metadata_provider,
    )

    parser = RecipeFormatParser()

    recipe_metadata = {
        "title": "Z Recipe",
        "base_model": "",
        "loras": [],
        "gen_params": {"steps": 20},
        "tags": ["test"],
        "checkpoint": {
            "modelVersionId": 777111,
            "modelId": 333222,
            "name": "Z Image",
            "version": "Turbo",
        },
    }

    metadata_text = f"Recipe metadata: {json.dumps(recipe_metadata)}"
    result = await parser.parse_metadata(metadata_text)

    checkpoint = result.get("checkpoint")
    assert checkpoint is not None
    assert checkpoint["name"] == "Z Image"
    assert checkpoint["version"] == "Turbo"
    assert checkpoint["hash"] == "abc123ff"
    assert checkpoint["file_name"] == "Z_Image_Turbo"
    assert result["base_model"] == "sdxl"
    assert result["model"] == checkpoint


@pytest.mark.asyncio
async def test_recipe_format_parser_marks_lora_in_library_by_version(monkeypatch):
    async def fake_metadata_provider():
        class Provider:
            async def get_model_version_info(self, version_id):
                assert version_id == 1244133
                return None, None

        return Provider()

    monkeypatch.setattr(
        "py.recipes.parsers.recipe_format.get_default_metadata_provider",
        fake_metadata_provider,
    )

    cached_entry: Dict[str, Any] = {
        "file_path": "/loras/moriimee.safetensors",
        "file_name": "MoriiMee Gothic Niji | LoRA Style",
        "size": 4096,
        "sha256": "abc123",
        "preview_url": "/previews/moriimee.png",
    }

    class FakeCache:
        def __init__(self, entry):
            self.raw_data = [entry]
            self.version_index = {1244133: entry}

    class FakeLoraScanner:
        def __init__(self, entry):
            self._cache = FakeCache(entry)

        def has_hash(self, sha256):
            return False

        async def get_cached_data(self):
            return self._cache

    class FakeRecipeScanner:
        def __init__(self, entry):
            self._lora_scanner = FakeLoraScanner(entry)

    parser = RecipeFormatParser()
    recipe_metadata = {
        "title": "Semi-realism",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 1244133,
                "modelName": "MoriiMee Gothic Niji | LoRA Style",
                "modelVersionName": "V1 Ilustrious",
                "strength": 0.5,
                "hash": "",
            }
        ],
        "gen_params": {"steps": 29},
        "tags": ["woman"],
    }

    metadata_text = f"Recipe metadata: {json.dumps(recipe_metadata)}"
    result = await parser.parse_metadata(
        metadata_text, recipe_scanner=FakeRecipeScanner(cached_entry)
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is True
    assert lora_entry["inLibrary"] is True
    assert lora_entry["localPath"] == cached_entry["file_path"]
    assert lora_entry["file_name"] == cached_entry["file_name"]
    assert lora_entry["hash"] == cached_entry["sha256"]
    assert lora_entry["size"] == cached_entry["size"]
    assert lora_entry["thumbnailUrl"] == config.get_preview_static_url(
        cached_entry["preview_url"]
    )


@pytest.mark.asyncio
async def test_recipe_format_parser_matches_lora_by_autov3_hash(monkeypatch):
    # Cache item is matched by its stored 12-char autov3 hash, even when its
    # full sha256 differs from the recipe hash.
    cached_entry: Dict[str, Any] = {
        "file_path": "/loras/autov3.safetensors",
        "file_name": "AutoV3 LoRA",
        "size": 4096,
        "sha256": "f" * 64,
        "autov3": "AbCdEf123456",
        "preview_url": "/previews/autov3.png",
    }

    recipe_metadata = {
        "title": "Autov3",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 9001,
                "modelName": "AutoV3 LoRA",
                "modelVersionName": "V1",
                "strength": 0.7,
                "hash": "abcdef123456",
            }
        ],
        "gen_params": {"steps": 29},
        "tags": [],
    }

    result = await _parse(
        monkeypatch,
        recipe_metadata,
        recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])),
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is True
    assert lora_entry["localPath"] == cached_entry["file_path"]
    assert lora_entry["thumbnailUrl"] == config.get_preview_static_url(
        cached_entry["preview_url"]
    )


@pytest.mark.asyncio
async def test_recipe_format_parser_matches_lora_by_autov2_prefix(monkeypatch):
    # 10-char autov2 recipe hash matches the sha256[:10] prefix of the cache item.
    sha256 = "abcdef0123456789" + "0" * 48
    cached_entry: Dict[str, Any] = {
        "file_path": "/loras/autov2.safetensors",
        "file_name": "AutoV2 LoRA",
        "size": 8192,
        "sha256": sha256,
        "preview_url": "/previews/autov2.png",
    }

    recipe_metadata = {
        "title": "Autov2",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 9002,
                "modelName": "AutoV2 LoRA",
                "modelVersionName": "V1",
                "strength": 0.5,
                "hash": sha256[:10],
            }
        ],
        "gen_params": {"steps": 20},
        "tags": [],
    }

    result = await _parse(
        monkeypatch,
        recipe_metadata,
        recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])),
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is True
    assert lora_entry["localPath"] == cached_entry["file_path"]


@pytest.mark.asyncio
async def test_recipe_format_parser_matches_lora_by_full_sha256(monkeypatch):
    # Full 64-char sha256 recipe hash matches exactly as before the cascade change.
    sha256 = "0123456789abcdef" * 4
    cached_entry: Dict[str, Any] = {
        "file_path": "/loras/sha256.safetensors",
        "file_name": "Sha256 LoRA",
        "size": 4096,
        "sha256": sha256,
        "preview_url": "/previews/sha256.png",
    }

    recipe_metadata = {
        "title": "Sha256",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 9003,
                "modelName": "Sha256 LoRA",
                "modelVersionName": "V1",
                "strength": 0.9,
                "hash": sha256.upper(),
            }
        ],
        "gen_params": {"steps": 25},
        "tags": [],
    }

    result = await _parse(
        monkeypatch,
        recipe_metadata,
        recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])),
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is True
    assert lora_entry["localPath"] == cached_entry["file_path"]


@pytest.mark.asyncio
async def test_recipe_format_parser_no_hash_match_falls_back_to_version_index(monkeypatch):
    # No hash-form match: falls through to modelVersionId lookup as today.
    version_entry: Dict[str, Any] = {
        "file_path": "/loras/versioned.safetensors",
        "file_name": "Versioned LoRA",
        "size": 4096,
        "sha256": "a" * 64,
        "preview_url": "/previews/versioned.png",
    }
    cache_entry: Dict[str, Any] = {
        "file_path": "/loras/other.safetensors",
        "file_name": "Other LoRA",
        "size": 2048,
        "sha256": "b" * 64,
        "preview_url": "/previews/other.png",
    }

    recipe_metadata = {
        "title": "Versioned",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 9004,
                "modelName": "Versioned LoRA",
                "modelVersionName": "V1",
                "strength": 1.0,
                "hash": "c" * 64,
            }
        ],
        "gen_params": {"steps": 20},
        "tags": [],
    }

    result = await _parse(
        monkeypatch,
        recipe_metadata,
        recipe_scanner=_FakeRecipeScanner(
            _FakeLoraScanner(
                [cache_entry],
                version_index={9004: version_entry},
                has_hash_result=False,
            )
        ),
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is True
    assert lora_entry["localPath"] == version_entry["file_path"]
    assert lora_entry["file_name"] == version_entry["file_name"]
    assert lora_entry["size"] == version_entry["size"]


@pytest.mark.asyncio
async def test_recipe_format_parser_sha256_less_cache_item_no_keyerror(monkeypatch):
    # A cache item without a sha256 field must not raise KeyError in the lookup.
    cache_entry: Dict[str, Any] = {
        "file_path": "/loras/nohash.safetensors",
        "file_name": "NoHash LoRA",
        "size": 4096,
    }

    recipe_metadata = {
        "title": "NoHash",
        "base_model": "Illustrious",
        "loras": [
            {
                "modelVersionId": 9005,
                "modelName": "NoHash LoRA",
                "modelVersionName": "V1",
                "strength": 1.0,
                "hash": "d" * 64,
            }
        ],
        "gen_params": {"steps": 20},
        "tags": [],
    }

    result = await _parse(
        monkeypatch,
        recipe_metadata,
        recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cache_entry])),
    )

    lora_entry = result["loras"][0]
    assert lora_entry["existsLocally"] is False
    assert lora_entry["localPath"] is None
