"""The --self-test runner over assets/regression-snapshot.json."""
import json
import os
from .context import Ctx
from .inputs import load_json, parse_export, parse_tagcov, read_ui
from .report import summary_line
from .scoring import run_rules, score, trust

def merge(a, b):
    """Fixture patch: dicts merge key by key, anything else replaces."""
    if isinstance(a, dict) and isinstance(b, dict):
        return {**a, **{k: merge(a.get(k), v) for k, v in b.items()}}
    return b

def case_inputs(case, base, named):
    """A fixture case's inputs. A case with "base" copies an earlier case, then applies patch, ui_set and drop."""
    if 'base' in case:
        inp = json.loads(json.dumps(named[case['base']]))
    else:
        fl = {k: os.path.join(base, v) for k, v in (case.get('files') or {}).items()}
        inp = {k: (load_json(fl[k]) if k in fl else case.get(k, {} if k in ('snapshot', 'config') else None))
               for k in ('snapshot', 'config', 'bq', 'ads')}
        inp['ui'] = read_ui(fl['ui']) if 'ui' in fl else read_ui(rows=case.get('ui', []))
        inp['exports'], inp['tag_coverage'] = case.get('exports') or {}, case.get('tag_coverage')
    for k, v in (case.get('patch') or {}).items(): inp[k] = merge(inp.get(k), v)
    inp['ui'].update(read_ui(rows=case.get('ui_set', [])))
    for path in case.get('drop', []):
        if path.startswith('ui:'):
            pre = path[3:]
            for k in [k for k in inp['ui'] if (k.startswith(pre[:-1]) if pre.endswith('*') else k == pre)]: del inp['ui'][k]
            continue
        *parents, last = path.split('.')
        node = inp
        for p in parents: node = node.get(p) or {}
        if parents: node.pop(last, None)
        else: inp[last] = None
    return inp

def self_test(path):
    data, base, bad, named = load_json(path), os.path.dirname(path), 0, {}
    for case in data['cases']:
        inp = case_inputs(case, base, named)
        named[case['name']] = inp
        exports = {}
        for text in (inp['exports'] or {}).values(): exports.update(parse_export(text))
        tc = parse_tagcov(inp['tag_coverage']) if inp.get('tag_coverage') else None
        cfg = inp['config'] or {}
        c = Ctx(inp['snapshot'], cfg, inp['ui'], inp['bq'], inp['ads'], exports, tc, cfg.get('as_of'))
        fs = run_rules(c)
        sc, got = score(fs), {f.id: f.status for f in fs}
        exp = case['_expected']
        mism = [f'{r}: expected {exp[r]}, got {got.get(r)}' for r in sorted(exp) if got.get(r) != exp[r]]
        for key, val in (('_expected_score', sc['score']), ('_expected_grade', sc['grade']),
                         ('_expected_loss', bool(sc['banner'])), ('_expected_risk', bool(sc['risk']))):
            if key in case and case[key] != val: mism.append(f'{key[10:]}: expected {case[key]}, got {val}')
        labels = {t['family']: t['label'] for t in trust(c, fs)}
        mism += [f'trust {k}: expected {v}, got {labels.get(k)}' for k, v in (case.get('_expected_trust') or {}).items() if labels.get(k) != v]
        texts = [t for f in fs for _, t in f.subs] + [f.ok_text for f in fs] + [f.reason for f in fs] + [p for f in fs for p in f.partial]
        mism += [f'no finding text contains "{w}"' for w in case.get('_expected_text') or [] if not any(w in t for t in texts)]
        mism += [f'probe value "{w}" leaked into the output' for w in case.get('_forbidden_text') or [] if any(w in t for t in texts)]
        print(('ok    ' if not mism else 'FAIL  ') + case['name'] + ': ' + summary_line(sc))
        for m in mism: print('      ' + m)
        bad += bool(mism)
    print(f'{len(data["cases"]) - bad} of {len(data["cases"])} cases match')
    return 1 if bad else 0
