"""Variants: lineage schema, backfill rules, index parent/variants + stricter reuse, search grouping, register end to end.
usage: uv run --with numpy --with pillow python test/test_variants.py   (no network: enrich is switched off)"""
import copy, glob, json, os, shutil, sys, tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
for d in ('lib', 'tools'):
    sys.path.insert(0, os.path.join(ROOT, d))
import card as cardlib  # noqa: E402
import backfill_lineage as bl  # noqa: E402
import build_index as bi  # noqa: E402
import register_variant as rv  # noqa: E402

ok = 0


def check(name, cond):
    global ok
    assert cond, name
    ok += 1


# ---- schema
check('VARIANT_KINDS exists', set(cardlib.VARIANT_KINDS) == {'retexture', 'recolor', 'shape', 'resize', 'rig', 'rerun', 'alternative', 'other'})
check('provenance is outside the release hash', 'provenance' not in cardlib.RUNTIME)
base = json.load(open(glob.glob(os.path.join(ROOT, 'cards/vehicle/car/race/gt-racer/v1.json'))[0]))
check('real card still valid', cardlib.validate(base) == [])
c = copy.deepcopy(base); c['provenance']['parent'] = {'id': base['id'], 'version': 1, 'release': base['release']}; c['provenance']['variant'] = {'kind': 'retexture', 'note': 'x'}
check('same-id parent must be an earlier version', any('earlier' in e for e in cardlib.validate(c)))
c['provenance']['parent'] = {'id': 'a/b', 'version': 1, 'release': 'sha256:x'}; c['provenance']['variant'] = {'kind': 'banana', 'note': 'x'}
check('unknown kind rejected', any('kind' in e for e in cardlib.validate(c)))
c['provenance']['variant'] = {'kind': 'recolor', 'note': 'x'}
check('good lineage accepted, release unchanged', cardlib.validate(c) == [] and cardlib.release_hash(c) == base['release'])

# ---- backfill rules on synthetic cards
def mk(cid, ver, **kw):
    x = copy.deepcopy(base); x['id'] = cid; x['version'] = ver; x['provenance'] = {'author': 'a'}; x['files'] = {'glb': f'sha256:{cid}{ver}'}
    x['release'] = f'sha256:{cid}@{ver}'; x.update(kw); return x
allc = {'vehicle.car.race/gt': [mk('vehicle.car.race/gt', 1)],
        'vehicle.car.race/gt-g4': [mk('vehicle.car.race/gt-g4', 1)],
        'vehicle.space.pod/pod': [mk('vehicle.space.pod/pod', 1), mk('vehicle.space.pod/pod', 2, files={'glb': 'sha256:a', 'painted_glb': 'sha256:b'}), mk('vehicle.space.pod/pod', 3)],
        'vehicle.space.pod/trellis2-raw-pod': [mk('vehicle.space.pod/trellis2-raw-pod', 1)],
        'vehicle.space.pod/trellis2-raw-ghost': [mk('vehicle.space.pod/trellis2-raw-ghost', 1)],
        'prop.x.y/room3-rerun2-lamp': [mk('prop.x.y/room3-rerun2-lamp', 1)]}
links, amb = bl.classify(allc)
check('g4 links to the base as alternative', links[('vehicle.car.race/gt-g4', 1)]['parent']['id'] == 'vehicle.car.race/gt'
      and links[('vehicle.car.race/gt-g4', 1)]['variant']['kind'] == 'alternative')
check('generator A/B copy links to its job', links[('vehicle.space.pod/trellis2-raw-pod', 1)]['parent']['id'] == 'vehicle.space.pod/pod')
check('painted version is a retexture of the previous version', links[('vehicle.space.pod/pod', 2)]['variant']['kind'] == 'retexture'
      and links[('vehicle.space.pod/pod', 2)]['parent']['version'] == 1)
amb_ids = {(i, v) for i, v, _ in amb}
check('unmarked later version is ambiguous, not guessed', ('vehicle.space.pod/pod', 3) in amb_ids and ('vehicle.space.pod/pod', 3) not in links)
check('alternative without a parent is ambiguous', ('vehicle.space.pod/trellis2-raw-ghost', None) in amb_ids)
check('room re-run without its first run is ambiguous', ('prop.x.y/room3-rerun2-lamp', None) in amb_ids)

# ---- index rows
def row(cid, omg=True, parent=None, desc=None):
    return {'id': cid, 'name': cid, 'status': 'approved', 'reuse': {'omg': omg, 'omu': True, 'reason': '' if omg else 'no'},
            'provenance': {'parent': parent, 'variant': {'kind': 'retexture', 'note': 'n'}} if parent else {},
            **({'visual_description': desc} if desc else {})}
P = {'id': 'a.b.c/p', 'version': 1, 'release': 'sha256:p'}
rows = [row('a.b.c/p', omg=False, desc='parent words'), row('a.b.c/p-v', omg=True, parent=P), row('a.b.c/p-w', parent=dict(P, id='a.b.c/p-v'))]
bi.link_variants(rows)
check('parent lists its variants', rows[0]['variants'] == ['a.b.c/p-v'] and rows[1]['variants'] == ['a.b.c/p-w'])
check('variant knows its parent', rows[1]['parent']['id'] == 'a.b.c/p' and rows[1]['parent']['name'] == 'a.b.c/p')
check('reuse can only be stricter than the parent', rows[1]['reuse']['omg'] is False and rows[1]['reuse']['omu'] is True)
check('chain: grandchild inherits the stricter flag', rows[2]['reuse']['omg'] is False)
check('a variant inherits no description', 'visual_description' not in rows[1])
same = [row('s/x'), row('s/x', parent={'id': 's/x', 'version': 1, 'release': 'r'})]
same[0]['id'] = 's/y'; same[1]['id'] = 's/x'; same[1]['provenance']['parent']['id'] = 's/x'
bi.link_variants(same)
check('a newer version of the same id has no parent row', 'parent' not in same[1] and same[0]['variants'] == [])

# ---- search
import search as S  # noqa: E402
with tempfile.TemporaryDirectory() as d:
    idx = {'assets': [dict(r, summary=r['id'], tags=[], size={'w': 1, 'l': 1, 'h': 1}, glb='g' + str(i), game_use=True, release='r' + str(i), style='s') for i, r in enumerate(rows)],
           'aliases': {}, 'tree': {}}
    json.dump(idx, open(f'{d}/index.json', 'w'))
    s = S.Search(d)
    r = s.search(variants_of='a.b.c/p')
    check('--variants-of lists the children', [x['id'] for x in r['results']] == ['a.b.c/p-v'] and r['parent'] is None)
    r = s.search(variants_of='a.b.c/p-w')
    check('--variants-of on a grandchild gives the chain', r['ancestors'] == ['a.b.c/p-v', 'a.b.c/p'] and r['parent']['id'] == 'a.b.c/p-v')
    top = [s.byid['a.b.c/p'], s.byid['a.b.c/p-v']]
    s.group_variants(top)
    check('hit lists its variants under it', top[0]['variants_detail'][0]['id'] == 'a.b.c/p-v' and top[0]['variants_detail'][0]['kind'] == 'retexture')
    check('variant listed with its parent is marked grouped', top[1]['grouped_under'] == 'a.b.c/p')

# ---- register end to end (temp library, no network)
blobs = glob.glob(os.path.expanduser('~/forge-build/forge-assets-search/store/blobs/*.glb'))
if blobs:
    glb_file = min(blobs, key=os.path.getsize)
    with tempfile.TemporaryDirectory() as t:
        for n in ('tools', 'lib', 'taxonomy'):
            os.symlink(os.path.join(ROOT, n), f'{t}/{n}')
        pc = os.path.join(ROOT, 'cards/vehicle/car/race/gt-racer/v1.json')
        dst = f'{t}/cards/vehicle/car/race/gt-racer'
        os.makedirs(dst); shutil.copy(pc, dst)
        thumb = f'{t}/th.jpg'; open(thumb, 'wb').write(b'\xff\xd8fake')
        res = rv.register('vehicle.car.race/gt-racer', glb_file, 'recolor', 'red livery', thumbnail=thumb, enrich=False, root=t)
        check('new id from parent class + slug + kind', res['id'] == 'vehicle.car.race/gt-racer-recolor')
        res2 = rv.register('vehicle.car.race/gt-racer', glb_file, 'recolor', 'blue livery', enrich=False, root=t, no_reuse=True)
        check('second one gets a number', res2['id'] == 'vehicle.car.race/gt-racer-recolor-2')
        check('missing thumbnail => pending, said so', res2['enrich'].startswith('pending'))
        cj = json.load(open(res['card']))
        check('card valid, lineage written', cardlib.validate(cj) == [] and cj['provenance']['parent']['id'] == 'vehicle.car.race/gt-racer'
              and cj['provenance']['variant'] == {'kind': 'recolor', 'note': 'red livery'})
        check('recolor keeps the parent frame', cj['frame'] == json.load(open(pc))['frame'])
        ix = {a['id']: a for a in json.load(open(f'{t}/index.json'))['assets']}
        check('index: parent lists both, variants point back', ix['vehicle.car.race/gt-racer']['variants'] == [res['id'], res2['id']]
              and ix[res['id']]['parent']['id'] == 'vehicle.car.race/gt-racer')
        check('--no-reuse blocks reuse', ix[res2['id']]['reuse']['omg'] is False)
        check('own thumbnail', ix[res['id']]['thumbnail'].startswith('media/') and ix[res['id']]['thumbnail'] != ix['vehicle.car.race/gt-racer']['thumbnail'])
print(f'{ok} checks passed')
