"""Engine side of the library wiring. Run inside forge-engine: pytest tests/test_library_vendor.py"""
import asyncio
import hashlib
import json
from pathlib import Path

from forge_engine.pipeline import library_vendor as lv
from forge_engine.tools import library_search_cli as lib

BLOB = b"glTF-wardrobe" * 100
H = hashlib.sha256(BLOB).hexdigest()


def asset(i, **kw):
    a = {"id": i, "name": i.split("/")[1].replace("-", " "), "summary": "", "tags": i.split("/")[1].split("-"), "status": "approved",
         "game_use": True, "reuse": {"omg": True, "omu": True}, "glb": H, "bytes": len(BLOB), "release": "sha256:x", "licence": "owned",
         "size": {"w": 1, "l": 1, "h": 2}, "tris": 100}
    a.update(kw)
    return a


INDEX = {"assets": [asset("prop.furniture.wardrobe/store-wardrobe"), asset("prop.furniture.sofa/ikea-sofa", reuse={"omg": True, "omu": False}),
                    asset("prop.street.lamp/harbour-lamp", glb="0" * 64)]}


def run(sb, manifest, fetch=None, index=INDEX):
    (sb / ".forge").mkdir()
    (sb / ".forge" / "library-manifest.json").write_text(json.dumps(manifest))

    async def ok(url):
        assert url.endswith(f"/blobs/{H}.glb")
        return BLOB
    return asyncio.run(lv.vendor_library_assets(sb, index=index, fetch_bytes=fetch or ok))


def test_vendors_a_cleared_model_hash_checked(tmp_path):
    r = run(tmp_path, [{"id": "prop.furniture.wardrobe/store-wardrobe", "path": "generated/library/wardrobe.glb"}])
    assert r["vendored"] == 1 and (tmp_path / "public/generated/library/wardrobe.glb").read_bytes() == BLOB
    assert json.loads((tmp_path / ".forge/library-attributions.json").read_text())[0]["id"].endswith("store-wardrobe")


def test_opted_out_unknown_and_escaping_paths_dropped(tmp_path):
    r = run(tmp_path, [{"id": "prop.furniture.sofa/ikea-sofa", "path": "generated/library/s.glb"},
                       {"id": "nope/x", "path": "generated/library/x.glb"},
                       {"id": "prop.furniture.wardrobe/store-wardrobe", "path": "../../etc/w.glb"},
                       {"id": "prop.furniture.wardrobe/store-wardrobe", "path": "generated/hero/w.glb"}])
    assert r["vendored"] == 0 and r["dropped"] == 4 and not (tmp_path / "public").exists()


def test_hash_mismatch_fails_that_entry_only(tmp_path):
    async def bad(url):
        return b"tampered"
    r = run(tmp_path, [{"id": "prop.furniture.wardrobe/store-wardrobe", "path": "generated/library/w.glb"}], fetch=bad)
    assert r["failed"] == 1 and r["vendored"] == 0 and not (tmp_path / "public/generated/library/w.glb").exists()


def test_idempotent_and_no_manifest(tmp_path):
    m = [{"id": "prop.furniture.wardrobe/store-wardrobe", "path": "generated/library/w.glb"}]
    (tmp_path / ".forge").mkdir()
    assert asyncio.run(lv.vendor_library_assets(tmp_path, index=INDEX))["manifest"] is False
    (tmp_path / ".forge" / "library-manifest.json").write_text(json.dumps(m))
    asyncio.run(lv.vendor_library_assets(tmp_path, index=INDEX, fetch_bytes=lambda u: _async(BLOB)))
    r = asyncio.run(lv.vendor_library_assets(tmp_path, index=INDEX, fetch_bytes=lambda u: _async(BLOB)))
    assert r["skipped"] == 1 and r["vendored"] == 0


async def _async(v):
    return v


def test_cli_search_finds_by_name_and_respects_reuse():
    idx = {"assets": INDEX["assets"] + [asset(f"prop.container.crate/box-{i}", tags=["box"]) for i in range(300)]}
    hits = lib.find(idx, "a tall wardrobe")
    assert [a["id"] for _, a in hits] == ["prop.furniture.wardrobe/store-wardrobe"]
    assert lib.find(idx, "sofa") == []          # reuse.omu false never listed
    assert lib.find(idx, "dragon") == []        # floor: a GAP, no lookalike


def test_engine_copy_equals_factory_copy():
    here = Path(lib.__file__).read_text()
    parents = Path(__file__).resolve().parents
    fac = parents[3] / "omg" / "factory" / "library_search.py" if len(parents) > 3 else None
    if fac and fac.exists():  # only inside the forge-assets tree; in the Engine image there is nothing to compare
        assert here.startswith(fac.read_text().rstrip("\n"))


def test_cli_word_map_finds_synonym_and_missing_map_is_bm25():
    wm = {"words": {"closet": [["prop.furniture.wardrobe/store-wardrobe", 0.688], ["prop.furniture.sofa/ikea-sofa", 0.9]]}}
    idx = {"assets": INDEX["assets"] + [asset(f"prop.container.crate/box-{i}", tags=["box"]) for i in range(300)]}
    assert lib.find(idx, "a closet") == []                                   # no shared word: BM25 alone misses
    hits = lib.find(idx, "a closet", word_map=wm)
    assert [a["id"] for _, a in hits] == ["prop.furniture.wardrobe/store-wardrobe"]   # reuse.omu false sofa never listed
    assert lib.load_word_map(fetch=lambda u: (_ for _ in ()).throw(OSError("down")), cache=None) is None


def test_class_named_without_asset_is_gap_not_lookalike():
    """'dragon' names monster.flyer.dragon: with no dragon asset it is a GAP, never the nearest dinosaur (18 Oct live bug)."""
    idx = {"tree": {"monster": {"groups": {"flyer": {"classes": {"dragon": {}}}, "creature": {"classes": {"dinosaur": {}}}}}},
           "assets": [{"id": "monster.creature.dinosaur/omg-dinosaur", "name": "toon dinosaur", "status": "approved", "glb": "x",
                       "reuse": {"omu": True}, "tags": ["dragon-like"], "summary": "dinosaur"}]}
    wm = {"words": {"dragon": [["monster.creature.dinosaur/omg-dinosaur", 0.7]]}}
    assert lib.find(idx, "dragon", word_map=wm) == []
    assert [a["id"] for _, a in lib.find(idx, "dinosaur", word_map=wm)] == ["monster.creature.dinosaur/omg-dinosaur"]
