"""Creature registration and incremental model-call boundaries, offline."""
import copy, hashlib, json, sys
from pathlib import Path
import numpy as np
ROOT=Path(__file__).resolve().parents[1]
sys.path[:0]=[str(ROOT/'lib'),str(ROOT/'tools')]
import card, import_creatures as ingest, enrich_library as enrich, embeddings

def creature_cards():
    return [c for versions in card.load_all(ROOT).values() for c in versions if c.get('omg_char_id')]

def test_catalog_coverage_and_contract():
    entries=json.loads((ROOT/'W-LIBCARDS-evidence/source/catalog.json').read_text())
    cards=creature_cards();byid={c['omg_char_id']:c for c in cards}
    assert len(cards)==len(byid)==len(entries)==30
    assert sum(c['id'].startswith('animal.') for c in cards)==17
    assert sum(c['id'].startswith('monster.') for c in cards)==13
    assert sum(c['style']=='realistic' for c in cards)==7
    tree=json.loads((ROOT/'taxonomy/tree.json').read_text())
    for e in entries:
        c=byid[e['id']];assert not card.validate(c)
        assert c['version']==1 and c['status']=='approved'
        assert c['game_use'] and c['rigged'] and c['reuse']=={'omg':True,'omu':True}
        assert c['rig']['clip_map']==e['clips'];assert c['gameplay']['speeds_mps']==e['speeds']
        assert c['licence']['original']==e['licence'] and c['licence']['credit']==e['credit']
        assert c['provenance']['lane']==('forge-realistic-animal' if e['style']=='realistic' else 'quaternius-cc0')
        kind,group,cls=c['id'].split('/')[0].split('.')
        assert cls in tree[kind]['groups'][group]['classes']
        h=c['files']['glb'].split(':')[1]
        assert hashlib.sha256((ROOT/'store/blobs'/(h+'.glb')).read_bytes()).hexdigest()==h
        assert set(e['clips'].values())<=set(c['animations'])

def test_final_measurements_and_vectors():
    for c in creature_cards():
        assert c['size']['source']=='measured'
        assert c['bounds']['geometry']['source']=='measured'
        assert all(v>0 for v in c['size']['measured_m'].values())
        assert (ROOT/c['thumbnail']).is_file()
        assert c['visual']['description'] and c['visual']['safety']['model']=='jev'
        assert not c['visual']['safety']['blocked']
        a={'id':c['id'],'release':c['release']}
        with np.load(ROOT/'vectors'/embeddings.VECTOR_SET/(enrich.key(a)+'.npz')) as z:
            assert set(z.files)=={'image','text','description'}
            for name in z.files:
                assert z[name].shape==(768,) and np.isfinite(z[name]).all()
                assert abs(np.linalg.norm(z[name])-1)<.002

def test_incremental_scope_and_zero_call_rebuild(tmp_path, monkeypatch):
    import visual_safety
    assets=json.loads((ROOT/'index.json').read_text())['assets'][:2]
    state={'assets':{},'calls':{'vision':0,'multimodal':0,'text':0}}
    for i,a in enumerate(assets):
        c=json.loads((ROOT/a['card']).read_text());p=tmp_path/a['card'];p.parent.mkdir(parents=True,exist_ok=True);p.write_text(json.dumps(c))
        a['thumbnail']=f'media/test-{i}.jpg'
    # The old asset has valid cached vectors but its ignored thumbnail is not on this box.
    old,new=assets;dest=tmp_path/'vectors'/embeddings.VECTOR_SET;dest.mkdir(parents=True)
    state['assets'][enrich.key(old)]={'id':old['id'],'release':old['release'],'description':'old','thumbnail_sha256':'previous'}
    v=np.ones(768,dtype=np.float32)/np.sqrt(768)
    np.savez_compressed(dest/(enrich.key(old)+'.npz'),image=v,text=v,description=v)
    (tmp_path/'media').mkdir();(tmp_path/new['thumbnail']).write_bytes(b'actual new thumbnail fixture')
    (tmp_path/'index.json').write_text(json.dumps({'assets':assets}));(tmp_path/'embeddings.json').write_text(json.dumps(state))
    ids=tmp_path/'ids.json';ids.write_text(json.dumps([new['id']]))
    calls=[]
    monkeypatch.setattr(enrich,'ROOT',tmp_path)
    monkeypatch.setattr(enrich,'vision',lambda batch:(calls.append(('vision',[a['id'] for a in batch])) or [(a,{'description':'new visible animal'}) for a in batch]))
    monkeypatch.setattr(embeddings,'multimodal',lambda *args:(calls.append(('mm',)) or {'imageEmbedding':v,'textEmbedding':v}))
    monkeypatch.setattr(embeddings,'text_vector',lambda *args:(calls.append(('text',)) or v))
    monkeypatch.setattr(visual_safety,'cached',lambda *args:(calls.append(('safety',)) or {'model':'jev','blocked':False}))
    oldbytes=(tmp_path/old['card']).read_bytes()
    monkeypatch.setattr(sys,'argv',['enrich','--ids-file',str(ids),'--workers','1']);enrich.main()
    assert calls==[('vision',[new['id']]),('mm',),('text',),('safety',)]
    assert (tmp_path/old['card']).read_bytes()==oldbytes
    before=(tmp_path/'embeddings.json').read_bytes();calls.clear()
    monkeypatch.setattr(sys,'argv',['enrich','--workers','1']);enrich.main()
    assert calls==[] and (tmp_path/'embeddings.json').read_bytes()==before
    assert (tmp_path/old['card']).read_bytes()==oldbytes

def test_import_replay_preserves_visual_and_rejects_repin(tmp_path):
    import pytest
    entry=next(e for e in json.loads((ROOT/'W-LIBCARDS-evidence/source/catalog.json').read_text()) if e['id']=='enemy-wolf-realistic')
    existing=next(c for c in creature_cards() if c['omg_char_id']==entry['id'])
    source=ROOT/'store/blobs'/(existing['files']['glb'].split(':')[1]+'.glb')
    measured=json.loads((ROOT/'W-LIBCARDS-evidence/measured.json').read_text())
    first=ingest.import_one(entry,source,tmp_path,measured)
    first['visual']={'description':'offline replay fixture'};card.write(tmp_path,first)
    again=ingest.import_one(entry,source,tmp_path,measured)
    assert again==first
    bad=copy.deepcopy(entry);bad['speeds']['run']+=1
    with pytest.raises(ValueError,match='Pinned v1 differs'):ingest.import_one(bad,source,tmp_path,measured)
    bad=copy.deepcopy(entry);bad['licence']='unlicensed'
    with pytest.raises(ValueError,match='Unattested licence'):ingest.import_one(bad,source,tmp_path,measured)

def test_saved_search_proof_matches_current_releases():
    proof=json.loads((ROOT/'W-LIBCARDS-evidence/search-proof.json').read_text())
    assert all(proof['checks'].values())
    current={c['id']:c for c in creature_cards()}
    for result in proof['queries'].values():
        for a in result['results']:assert a['release']==current[a['id']]['release']
