/*
 * RunKit.java — Java port of run_vectors.js: runs the official AER-1
 * conformance corpus against Aer1Verify.
 *
 *   java -cp out RunKit [kit-root]        # default /tmp/zambo
 *
 * A vector PASSes when the verifier's verdict matches the corpus
 * expectation, and — where a chain vector pins expected_failure or a merkle
 * vector pins rejected_by — some failure reason names that check.
 *
 * Output format mirrors run_vectors.js exactly (PASS/FAIL lines, per-group
 * "  => group: pass/n", SUMMARY block, and the final
 * "vector checks: N passed, M failed" line). Exit code 0 iff zero failures.
 */
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public final class RunKit {

    /* Sentinel standing in for JS `undefined` (vs Json.NULL == JS null). */
    private static final Object UNDEFINED = new Object();

    private static String KIT;
    private static final List<String[]> summary = new ArrayList<>(); // {group, pass, n}
    private static int totalPass = 0, totalFail = 0;
    private static Map<String, Map<String, Object>> indexById = new LinkedHashMap<>();

    public static void main(String[] args) {
        KIT = args.length > 0 ? args[0] : "/tmp/zambo";

        // ---- index.json verdicts (kit manifest; labels only) ----
        try {
            Object idx = loadJson(new File(KIT, "aer1-implementations/vectors/index.json"));
            for (Object vo : asList(getField(idx, "vectors"))) {
                Object id = getField(vo, "id");
                if (id instanceof String && vo instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> vm = (Map<String, Object>) vo;
                    indexById.put((String) id, vm);
                }
            }
        } catch (Exception e) { /* optional */ }

        File TV = new File(new File(KIT, "aer-1"), "test-vectors");
        System.out.println("== ARION AER-1 conformance run == kit: " + KIT + " \n");

        // ---- 1. receipt corpora ----
        runReceiptDir("valid", new File(TV, "valid"), false, "valid-dir");
        runReceiptDir("invalid", new File(TV, "invalid"), false, "invalid-dir");
        runReceiptDir("invalid-profile", new File(TV, "invalid-profile"), false, "profile-boundary");
        runReceiptDir("anchored", new File(TV, "anchored"), false, "anchored");

        // ---- 2. chain corpora ----
        runChainFile("chain-v07 (-07/-08)", new File(KIT, "aer-1/chain-vectors-v07.json"), "v07");

        // ---- 2b. commitment corpus (Section 7.3 artifact, draft >= -09) ----
        // The JS runner preprocesses "entry_count": <d+.d+> into a
        // {"__float__": N} marker because JSON.parse erases the lexical
        // distinction; our parser keeps the raw lexeme, so
        // verifyChainCommitment detects it via Json.Num.isMarkedFloat().
        runCommitmentFile("commitment", new File(KIT, "aer-1/commitment-vectors.json"), false);

        // Historical -06 corpus, two lenses: under its own construction each
        // vector should produce its labeled verdict; under -07/-08 every
        // vector MUST be rejected (Section 7.4).
        {
            File file = new File(KIT, "aer-1/chain-vectors.json");
            runChainFile("chain-legacy (-06 construction)", file, "v06");
            Object corpus = null;
            try { corpus = loadJson(file); } catch (Exception e) { /* handled below */ }
            List<?> vectors = corpus instanceof Map ? asList(getField(corpus, "vectors"))
                                                  : List.of();
            int pass = 0;
            for (Object vo : vectors) {
                Object tl = timelineOf(vo);
                List<String> fails = Aer1Verify.verifyChainV07(tl);
                boolean ok = !fails.isEmpty();
                record("chain-legacy under -07/-08", str(getField(vo, "name")), ok,
                       !fails.isEmpty() ? fails.get(0) : "verified (BAD)");
                if (ok) pass++;
            }
            groupDone("chain-legacy under -07/-08", pass, vectors.size());
        }

        // ---- 3. merkle vectors ----
        runMerkleFile("merkle", new File(KIT, "aer-1/merkle-vectors.json"));

        // ---- 4. frozen corpora ----
        runReceiptDir("frozen-v1.3.0", new File(KIT, "aer-1/frozen-vectors/v1.3.0"), false, "index");
        runFrozenV1("frozen-v1", new File(KIT, "aer-1/frozen-vectors/v1"));

        runReceiptDir("adversarial (strict)",
            new File(KIT, "aer1-implementations/vectors/adversarial-2026-09-27"), true, "index");
        runReceiptDir("certifier v1.3",
            new File(KIT, "aer1-ecosystem/certifier/vectors/v1.3/valid"), false, "valid-dir");
        runReceiptDir("certifier v1.3-invalid",
            new File(KIT, "aer1-ecosystem/certifier/vectors/v1.3/invalid"), false, "invalid-dir");
        runReceiptDir("certifier v1.3-profile",
            new File(KIT, "aer1-ecosystem/certifier/vectors/v1.3/invalid-profile"), false, "profile-boundary");
        runReceiptDir("certifier v1.3-anchored",
            new File(KIT, "aer1-ecosystem/certifier/vectors/v1.3/anchored"), false, "anchored");
        runReceiptDir("certifier adversarial (strict)",
            new File(KIT, "aer1-ecosystem/certifier/vectors/adversarial-2026-09-27"), true, "index");
        runReceiptDir("audit-bundle samples",
            new File(KIT, "aer1-audit-bundle/samples"), false, "valid-dir");

        // ---- 5. fuzz corpus (unlabeled) ----
        runFuzz("fuzz-corpus (strict, unlabeled)",
            new File(KIT, "aer1-ecosystem/certifier/fuzz-corpus"));

        System.out.println("\n== SUMMARY ==");
        for (String[] s : summary) System.out.println("  " + s[0] + ": " + s[1] + "/" + s[2]);
        System.out.println("vector checks: " + totalPass + " passed, " + totalFail + " failed");
        System.exit(totalFail != 0 ? 1 : 0);
    }

    /* ------------------------------------------------------------------ */

    private static void record(String group, String name, boolean ok, String note) {
        System.out.println((ok ? "PASS" : "FAIL") + " [" + group + "] " + name +
                           (note != null && !note.isEmpty() ? " \u2014 " + note : ""));
        if (ok) totalPass++; else totalFail++;
    }

    private static void groupDone(String group, int pass, int n) {
        summary.add(new String[]{group, Integer.toString(pass), Integer.toString(n)});
        System.out.println("  => " + group + ": " + pass + "/" + n);
    }

    private static Object loadJson(File f) throws IOException, Json.ParseException {
        // JS readFileSync(path,"utf8") is lenient (U+FFFD on malformed
        // bytes); new String(bytes, UTF_8) matches that.
        return Json.parse(new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8));
    }

    private static List<File> listJson(File dir) {
        String[] names = dir.list();
        if (names == null) return List.of();
        List<String> filtered = new ArrayList<>();
        for (String n : names) {
            if (n.endsWith(".json") && !n.startsWith(".")) filtered.add(n);
        }
        Collections.sort(filtered);
        List<File> out = new ArrayList<>(filtered.size());
        for (String n : filtered) out.add(new File(dir, n));
        return out;
    }

    /* safe member access on a JSON-object-shaped value */
    private static Object getField(Object o, String k) {
        return (o instanceof Map) ? Json.get(o, k) : null;
    }

    private static List<?> asList(Object v) {
        return (v instanceof List) ? (List<?>) v : List.of();
    }

    private static String str(Object v) { return v instanceof String ? (String) v : String.valueOf(v); }

    /* v.timeline !== undefined && !== null ? v.timeline : v.entries */
    private static Object timelineOf(Object v) {
        Object t = getField(v, "timeline");
        if (t != null && t != Json.NULL) return t;
        return getField(v, "entries");
    }

    // ---- 1. receipt corpora ----------------------------------------------
    private static void runReceiptDir(String group, File dir, boolean strict, String mode) {
        Aer1Verify.Opts opts = new Aer1Verify.Opts(strict);
        List<File> files = listJson(dir);
        int pass = 0;
        for (File f : files) {
            String name = f.getName();
            name = name.substring(0, name.length() - ".json".length());
            Object doc = null;
            try { doc = loadJson(f); } catch (Exception e) { /* parse error */ }
            List<String> core = Aer1Verify.truthy(doc)
                ? Aer1Verify.checkReceipt(doc, opts)
                : List.of("not valid JSON");
            String id = name;
            // JS: let expectedVerdict = null  -> Json.NULL (not UNDEFINED);
            // an index hit without the field assigns JS undefined -> UNDEFINED.
            Object expectedVerdict = Json.NULL;
            String effMode = mode;
            Map<String, Object> idx = indexById.get(id);
            if (idx != null) {
                Object ev = idx.containsKey("expected_verdict")
                            ? idx.get("expected_verdict") : UNDEFINED;
                expectedVerdict = ev;
                Object g = idx.get("vector_group");
                if ("anchored".equals(g)) effMode = "anchored";
                else if ("invalid-profile".equals(g)) effMode = "profile-boundary";
            }
            if ("valid-dir".equals(mode)) expectedVerdict = Boolean.TRUE;
            else if ("invalid-dir".equals(mode)) expectedVerdict = Boolean.FALSE;
            boolean ok;
            if ("profile-boundary".equals(effMode)) {
                List<String> prof = Aer1Verify.truthy(doc)
                    ? Aer1Verify.checkReferenceProfile(doc, opts)
                    : List.of("n/a");
                ok = core.isEmpty() && !prof.isEmpty();
                record(group, name, ok, ok ? "" :
                    "core " + (!core.isEmpty() ? "unexpectedly failed: " + core.get(0) : "ok") +
                    ", profile " + (!prof.isEmpty() ? "rejected: " + prof.get(0) : "accepted!"));
            } else if ("anchored".equals(effMode)) {
                List<String> tier = Aer1Verify.truthy(doc)
                    ? Aer1Verify.checkDisinterestedTier(doc, opts)
                    : List.of("n/a");
                boolean expectTierPass = Boolean.TRUE.equals(expectedVerdict);
                ok = core.isEmpty() && (expectTierPass ? tier.isEmpty() : !tier.isEmpty());
                record(group, name, ok, !tier.isEmpty() ? "tier: " + tier.get(0) :
                    (expectTierPass ? "tier ok" : "tier accepted!"));
            } else {
                boolean actual = core.isEmpty();
                // JS: expectedVerdict === null ? actual : (actual === expectedVerdict)
                if (expectedVerdict == Json.NULL) ok = actual;
                else ok = Objects.equals(Boolean.valueOf(actual), expectedVerdict);
                record(group, name, ok, !core.isEmpty() ? "rejected: " + core.get(0) :
                    (Boolean.FALSE.equals(expectedVerdict) ? "accepted!" : ""));
            }
            if (ok) pass++;
        }
        groupDone(group, pass, files.size());
    }

    // ---- 2. chain corpora --------------------------------------------------
    private static void runChainFile(String group, File file, String construction) {
        Object corpus = null;
        try { corpus = loadJson(file); } catch (Exception e) { /* empty corpus */ }
        List<?> vectors = corpus instanceof Map ? asList(getField(corpus, "vectors")) : List.of();
        int pass = 0;
        for (Object vo : vectors) {
            Object tl = timelineOf(vo);
            List<String> fails = "v06".equals(construction)
                ? Aer1Verify.verifyChainV06(tl)
                : Aer1Verify.verifyChainV07(tl);
            String actual = !fails.isEmpty() ? "invalid" : "valid";
            Object expVerdict = getField(vo, "expected_verdict");
            boolean ok = actual.equals(expVerdict); // JS: actual === v.expected_verdict
            Object expFail = getField(vo, "expected_failure");
            if (ok && "invalid".equals(expVerdict) &&
                expFail instanceof String && !((String) expFail).isEmpty()) {
                String pinned = (String) expFail;
                if ("v07".equals(construction)) {
                    // -07/-08 vectors pin the failing check verbatim.
                    boolean hit = false;
                    for (String f0 : fails) if (f0.contains(pinned)) { hit = true; break; }
                    ok = hit;
                } else {
                    // -06 corpus words failures loosely; require the named
                    // check family to appear.
                    String[] kws = {"prev_digest", "genesis", "close", "base64",
                                    "seq", "job", "tool", "provenance"};
                    String low = pinned.toLowerCase();
                    List<String> want = new ArrayList<>();
                    for (String k : kws) if (low.contains(k)) want.add(k);
                    boolean hit = false;
                    for (String f0 : fails) {
                        String fl = f0.toLowerCase();
                        for (String k : want) if (fl.contains(k)) { hit = true; break; }
                        if (hit) break;
                    }
                    ok = hit;
                }
            }
            record(group, str(getField(vo, "name")), ok, !fails.isEmpty() ? fails.get(0) :
                ("invalid".equals(expVerdict) ? "accepted!" : ""));
            if (ok) pass++;
        }
        groupDone(group, pass, vectors.size());
    }

    // ---- 2b. commitment corpus --------------------------------------------
    private static void runCommitmentFile(String group, File file, boolean strict) {
        Aer1Verify.Opts opts = new Aer1Verify.Opts(strict);
        Object corpus = null;
        try { corpus = loadJson(file); } catch (Exception e) { /* empty corpus */ }
        List<?> vectors = corpus instanceof Map ? asList(getField(corpus, "vectors")) : List.of();
        int pass = 0;
        for (Object vo : vectors) {
            List<String> fails = Aer1Verify.verifyChainCommitment(
                getField(vo, "timeline"), getField(vo, "commitment"), opts);
            String actual = !fails.isEmpty() ? "invalid" : "valid";
            Object expVerdict = getField(vo, "expected_verdict");
            boolean ok = actual.equals(expVerdict);
            record(group, str(getField(vo, "name")), ok, !fails.isEmpty() ? fails.get(0) :
                ("invalid".equals(expVerdict) ? "accepted!" : ""));
            if (ok) pass++;
        }
        groupDone(group, pass, vectors.size());
    }

    // ---- 3. merkle vectors -------------------------------------------------
    private static void runMerkleFile(String group, File file) {
        Object corpus = null;
        try { corpus = loadJson(file); } catch (Exception e) { /* empty corpus */ }
        List<?> vectors = corpus instanceof Map ? asList(getField(corpus, "vectors")) : List.of();
        int pass = 0;
        for (Object vo : vectors) {
            boolean ok = true;
            String note = "";
            if (vo instanceof Map && Json.has(vo, "receipt_ids")) {
                List<String> ids = new ArrayList<>();
                for (Object r : asList(getField(vo, "receipt_ids"))) ids.add((String) r);
                String got = Aer1Verify.merkleRoot(ids);
                if (!got.equals(getField(vo, "expected_root"))) {
                    ok = false; note = "root " + got + " != " + getField(vo, "expected_root");
                }
            } else if (vo instanceof Map && Json.has(vo, "leaves")) {
                // Legacy construction: leaf = SHA-256(utf8(preimage)), tree
                // per the same pairing rule; must reproduce the historical
                // root and MUST NOT equal the canonical root.
                List<Object> leaves = new ArrayList<>(asList(getField(vo, "leaves")));
                leaves.sort(Comparator.comparingDouble(l -> seqOf(l)));
                List<byte[]> lvl = new ArrayList<>();
                for (Object l : leaves) {
                    Object pre = getField(l, "preimage");
                    lvl.add(sha256bytes(pre instanceof String ? (String) pre : String.valueOf(pre)));
                }
                while (lvl.size() > 1) {
                    if (lvl.size() % 2 == 1) lvl.add(lvl.get(lvl.size() - 1));
                    List<byte[]> next = new ArrayList<>();
                    for (int i = 0; i < lvl.size(); i += 2) {
                        byte[] a = lvl.get(i), b = lvl.get(i + 1);
                        byte[] c = new byte[a.length + b.length];
                        System.arraycopy(a, 0, c, 0, a.length);
                        System.arraycopy(b, 0, c, a.length, b.length);
                        next.add(sha256bytes(c));
                    }
                    lvl = next;
                }
                String got = !lvl.isEmpty() ? hexStr(lvl.get(0))
                                            : Aer1Verify.merkleRoot(List.of());
                Object mne = getField(vo, "must_not_equal");
                if (!got.equals(getField(vo, "expected_root")) ||
                    (Aer1Verify.truthy(mne) && got.equals(mne))) {
                    ok = false; note = "legacy root " + got;
                }
            }
            Object ewv = getField(vo, "expected_workflow_verdict");
            if (ok && ewv != null && ewv != Json.NULL) {
                if (!(vo instanceof Map) || !Json.has(vo, "receipt_ids")) {
                    ok = false; note = "expected_workflow_verdict without receipt_ids";
                } else {
                    List<String> ids = new ArrayList<>();
                    for (Object r : asList(getField(vo, "receipt_ids"))) ids.add((String) r);
                    Map<String, Object> wf = new LinkedHashMap<>();
                    List<Object> steps = new ArrayList<>();
                    for (int i = 0; i < ids.size(); i++) {
                        Map<String, Object> step = new LinkedHashMap<>();
                        step.put("seq", Json.Num.of(i + 1));
                        step.put("receipt_id", ids.get(i));
                        steps.add(step);
                    }
                    wf.put("steps", steps);
                    wf.put("merkle_root", getField(vo, "expected_root"));
                    List<String> fails = Aer1Verify.verifyWorkflowSteps(wf);
                    String verdict = !fails.isEmpty() ? "invalid" : "valid";
                    ok = verdict.equals(ewv);
                    Object rb = getField(vo, "rejected_by");
                    if (ok && "invalid".equals(verdict) && rb instanceof String) {
                        boolean hit = false;
                        for (String f0 : fails) if (f0.contains((String) rb)) { hit = true; break; }
                        ok = hit;
                    }
                    if (!ok) note = "workflow verdict " + verdict +
                        (!fails.isEmpty() ? " (" + fails.get(0) + ")" : "");
                }
            }
            record(group, str(getField(vo, "name")), ok, note);
            if (ok) pass++;
        }
        groupDone(group, pass, vectors.size());
    }

    private static double seqOf(Object leaf) {
        Object s = getField(leaf, "seq");
        return (s instanceof Json.Num) ? ((Json.Num) s).value : Double.NaN;
    }

    private static byte[] sha256bytes(String s) {
        try {
            java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
            return md.digest(Aer1Verify.utf8(s)); // Node utf8 parity (U+FFFD on lone surrogates)
        } catch (java.security.NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }

    private static byte[] sha256bytes(byte[] b) {
        try {
            return java.security.MessageDigest.getInstance("SHA-256").digest(b);
        } catch (java.security.NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }

    private static String hexStr(byte[] b) {
        StringBuilder sb = new StringBuilder(b.length * 2);
        for (byte x : b) sb.append(Character.forDigit((x >> 4) & 0xf, 16))
                           .append(Character.forDigit(x & 0xf, 16));
        return sb.toString();
    }

    // ---- 4. frozen v1 corpus (manifest verdicts) ---------------------------
    private static void runFrozenV1(String group, File dir) {
        Object manifest = null;
        try { manifest = loadJson(new File(dir, "manifest.json")); }
        catch (Exception e) { /* handled: empty vector list */ }
        List<?> vectors = manifest instanceof Map ? asList(getField(manifest, "vectors"))
                                                : List.of();
        int pass = 0;
        for (Object vo : vectors) {
            Object doc = null;
            Object vf = getField(vo, "file");
            try { doc = loadJson(new File(dir, (String) vf)); } catch (Exception e) { /* */ }
            List<String> core = doc != null ? Aer1Verify.checkReceipt(doc)
                                            : List.of("not valid JSON");
            List<String> prof = doc != null ? Aer1Verify.checkReferenceProfile(doc)
                                            : List.of("n/a");
            Object ec = getField(vo, "expected_core");
            Object ep = getField(vo, "expected_reference_producer");
            boolean coreOk = core.isEmpty() == "accept".equals(ec);
            boolean profOk = prof.isEmpty() == "accept".equals(ep);
            boolean ok = coreOk && profOk;
            record(group, str(vf), ok,
                "core:" + (!core.isEmpty() ? "reject(" + core.get(0) + ")" : "accept") +
                " expected " + ec +
                " | profile:" + (!prof.isEmpty() ? "reject(" + prof.get(0) + ")" : "accept") +
                " expected " + ep);
            if (ok) pass++;
        }
        groupDone(group, pass, vectors.size());
    }

    // ---- 5. fuzz corpus (unlabeled) ----------------------------------------
    private static void runFuzz(String group, File dir) {
        List<File> files = listJson(dir);
        int accept = 0, reject = 0, unparseable = 0;
        Map<String, Integer> byReason = new LinkedHashMap<>();
        Aer1Verify.Opts strict = new Aer1Verify.Opts(true);
        for (File f : files) {
            Object doc = null;
            try { doc = loadJson(f); } catch (Exception e) {
                unparseable++; reject++;
                byReason.merge("not valid JSON", 1, Integer::sum);
                continue;
            }
            List<String> fails = Aer1Verify.checkReceipt(doc, strict);
            if (!fails.isEmpty()) {
                reject++;
                byReason.merge(fails.get(0), 1, Integer::sum);
            } else {
                accept++;
            }
        }
        System.out.println("  => " + group + ": " + files.size() + " cases, " + accept +
            " accepted, " + reject + " rejected (" + unparseable + " unparseable)");
        List<Map.Entry<String, Integer>> top = new ArrayList<>(byReason.entrySet());
        top.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); // stable for ties
        for (int i = 0; i < Math.min(12, top.size()); i++) {
            System.out.println("       " + top.get(i).getValue() + "x " + top.get(i).getKey());
        }
        // JS pushes to the summary directly (no per-group "=>" line).
        summary.add(new String[]{group + " (unscored)",
            accept + "/" + files.size() + " accepted", Integer.toString(files.size())});
    }
}
