"""Photo ingestion and hybrid search safety tests, including a meaningful opt-out mutant."""
import copy,importlib.util,json,sys,types
from pathlib import Path
import pytest
ROOT=Path(__file__).resolve().parents[1];sys.path[:0]=[str(ROOT/'lib'),str(ROOT/'tools')]
import card,glb,ingest_photo_object as ingest
from search import Search
from lookup import Library

def factory():
    package=types.ModuleType('forge_corp');package.__path__=[];api=types.ModuleType('forge_corp.api');api.__path__=[];routes=types.ModuleType('forge_corp.api.routes');routes.__path__=[]
    for k,v in [('forge_corp',package),('forge_corp.api',api),('forge_corp.api.routes',routes)]:sys.modules.setdefault(k,v)
    for name,attr in [('games_garage','_card_text'),('games_room','_data_url')]:
        m=types.ModuleType('forge_corp.api.routes.'+name);setattr(m,attr, (lambda c,prompt='': ' '.join(str(getattr(c,k,'')) for k in ['title','pitch','genre','core_loop','first_version'])+' '+prompt) if attr=='_card_text' else (lambda mime,data:'data:'+mime+';base64,'+__import__('base64').b64encode(data).decode()));sys.modules.setdefault(m.__name__,m)
    ls=importlib.util.spec_from_file_location('forge_corp.api.routes.library_search',ROOT/'omg/factory/library_search.py');lm=importlib.util.module_from_spec(ls);sys.modules[ls.name]=lm;ls.loader.exec_module(lm);routes.library_search=sys.modules['forge_corp.api.routes'].library_search=lm
    spec=importlib.util.spec_from_file_location('forge_corp.api.routes.games_library',ROOT/'omg/factory/games_library.py');m=importlib.util.module_from_spec(spec);sys.modules[spec.name]=m;spec.loader.exec_module(m);return m

def test_optout_blocks_factory_and_pack():
    gl=factory();base={'id':'prop.furniture.sofa/demo','game_use':True,'status':'legacy','reuse':{'omg':True,'omu':True},'bytes':500,'name':'sofa','summary':'sofa','tags':['sofa'],'size':{'w':2,'l':1,'h':1},'tris':12}
    idx={'assets':[base],'tree':{'prop':{'groups':{'furniture':{'classes':['sofa']}}}},'aliases':{}}
    assert gl._usable(base)
    assert gl.pick(idx,'black sofa')[0]
    base['reuse']['omg']=False
    assert not gl._usable(base)
    assert not gl.pick(idx,'black sofa')[0]
    with pytest.raises(PermissionError):gl.build_pack(ROOT,[base])

def test_optout_mutant_is_caught():
    gl=factory();a={'id':'prop.furniture.sofa/x','game_use':True,'status':'legacy','reuse':{'omg':False}}
    original=gl._usable
    gl._usable=lambda a,modern=True:a['game_use']  # the intentional broken implementation
    with pytest.raises(AssertionError):assert not gl._usable(a)
    gl._usable=original

def test_exact_class_gap_no_vector_calls(monkeypatch):
    s=Search(ROOT);monkeypatch.setattr('embeddings.multimodal',lambda **kw:pytest.fail('GAP made a paid call'))
    r=s.search('dragon');assert r['results']==[] and r['gaps']

def test_class_and_reuse_filters():
    s=Search(ROOT);r=s.search('sofa',lexical_only=True,reuse='omg');assert r['results'];assert all(x['class']=='prop.furniture.sofa' for x in r['results'])
    assert not s.search(asset_class='prop.furniture.sofa',max_size=[.001,.001,.001],lexical_only=True)['results']
    assert all(x['reuse']['omu'] for x in s.search('sofa',reuse='omu',lexical_only=True)['results'])
    assert all(x['class']=='prop.furniture.sofa' for x in s.search('sofa',kind='prop',lexical_only=True)['results'])

def test_ingest_frame_texture_and_optout(tmp_path,monkeypatch):
    from PIL import Image
    import enrich_library
    import visual_safety
    monkeypatch.setattr(visual_safety,'cached',lambda description,root:{'blocked':False,'probabilities':{'person':0,'artwork':0}})
    monkeypatch.setattr(enrich_library,'vision',lambda batch:[(a,{'description':'A grey sofa, furniture size','artwork':False,'person':False}) for a in batch])
    (tmp_path/'taxonomy').mkdir();(tmp_path/'taxonomy/tree.json').write_text((ROOT/'taxonomy/tree.json').read_text())
    original=next((ROOT/'store/blobs').glob('*.glb'))
    doc,binary,_=glb.read(original)
    import io
    buf=io.BytesIO();Image.new('RGB',(2048,2048),'navy').save(buf,format='PNG');blob=buf.getvalue();binary+=b'\0'*(-len(binary)%4);offset=len(binary);binary+=blob
    view=len(doc.setdefault('bufferViews',[]));doc['bufferViews'].append({'buffer':0,'byteOffset':offset,'byteLength':len(blob)})
    image=len(doc.setdefault('images',[]));doc['images'].append({'bufferView':view,'mimeType':'image/png'})
    tex=len(doc.setdefault('textures',[]));doc['textures'].append({'source':image});doc['materials'][0].setdefault('pbrMetallicRoughness',{})['baseColorTexture']={'index':tex}
    doc['nodes'][0].setdefault('extras',{})['rec']={'unwanted_texture_copy':'x'*1000000};doc['buffers'][0]['byteLength']=len(binary)
    src=tmp_path/'input.glb';src.write_bytes(ingest.pack(doc,binary));im=tmp_path/'input.jpg';Image.new('RGB',(1400,1400),'grey').save(im)
    prov=tmp_path/'provenance.json';prov.write_text(json.dumps({'method':'test','models':['test fixture'],'source':'test photograph','date':'2026-10-18','licence_evidence':'owned test fixture'}))
    a=types.SimpleNamespace(root=tmp_path,glb=src,provenance=prov,asset_class='prop.furniture.sofa',slug='test',name='Test',thumbnail=im,photo=[im],metres_per_unit=1,yaw=25,no_reuse=False)
    c=ingest.ingest(a);assert not card.validate(c);assert c['reuse']=={'omg':True,'omu':True,'reason':''};m=glb.measure(tmp_path/'store/blobs'/(c['files']['glb'].split(':')[1]+'.glb'));assert abs(m['bounds']['min'][1])<.002
    assert m['textures'] and all(not t['px'][0] or max(t['px'])<=1024 for t in m['textures'])
    assert not any('rec' in n.get('extras',{}) for n in glb.read(tmp_path/'store/blobs'/(c['files']['glb'].split(':')[1]+'.glb'))[0]['nodes'])
    assert abs(m['bounds']['min'][0]+m['bounds']['max'][0])<.005
    assert abs(m['bounds']['min'][2]+m['bounds']['max'][2])<.005
    assert ingest.ingest(a)['version']==1
    a.no_reuse=True;c=ingest.ingest(a);assert c['version']==2 and not c['reuse']['omg'] and not c['reuse']['omu']
    a.no_reuse=False;d=json.loads(prov.read_text());d['third_party_artwork']=True;prov.write_text(json.dumps(d));c=ingest.ingest(a);assert not c['reuse']['omg'] and 'artwork' in c['reuse']['reason']


def test_real_room_models_fit_the_factory_and_pack(tmp_path):
    import io,tarfile
    gl=factory();idx=json.loads((ROOT/'index.json').read_text());picks=gl.pick(idx,'black leather sofa with a marble coffee table and sideboard')[0]
    expected={'prop.furniture.sofa/room3-rerun2-l-shaped-leather-sectional-sofa','prop.furniture.coffee-table/room3-rerun2-marble-top-wooden-coffee-table'}
    assert expected <= {a['id'] for a in picks}
    assert sum(gl._weight(a) for a in picks)<=gl.MAX_BYTES
    (tmp_path/'repo').symlink_to(ROOT,target_is_directory=True);(tmp_path/'blobs').symlink_to(ROOT/'store/blobs',target_is_directory=True)
    _,files,lock=gl.build_pack(tmp_path,picks)
    with tarfile.open(fileobj=io.BytesIO(files),mode='r:gz') as tf:
        manifest=json.load(tf.extractfile('library/manifest.json'))
    assert expected <= {a['id'] for a in manifest['assets']}
    assert all(a['collider'] for a in manifest['assets'] if a['id'] in expected)


def test_channel_specific_optout_is_not_hardcoded_to_omg():
    s=Search(ROOT);asset=next(a for a in s.assets if 'room3-rerun2-l-shaped' in a['id']);asset['reuse']={'omg':False,'omu':True,'reason':'owner permits apps only'}
    assert not any(a['id']==asset['id'] for a in s.lex.search('sofa',reuse='omg')['hits'])
    assert any(a['id']==asset['id'] for a in s.lex.search('sofa',reuse='omu')['hits'])
    assert any(a['id']==asset['id'] for a in s.search('sofa',reuse='omu',lexical_only=True)['results'])
