<?php
/**
 * run_conformance.php — AER-1 conformance harness (PHP 8.2, stdlib only).
 *
 * Runs every corpus the same way aer-1/conformance.py does and prints one
 * line per vector, then a summary. Exit 0 when every expectation holds.
 *
 * Usage:
 *   php run_conformance.php [path-to-aer-1-dir]
 *
 * The spec dir defaults to $AER1_DIR, else ../../research/zambo-aer1/aer-1
 * relative to this file, else /workspace/research/zambo-aer1/aer-1.
 */

require __DIR__ . '/aer1.php';

function aer1_locate_spec(array $argv): string
{
    foreach (array_slice($argv, 1) as $a) {
        if (is_dir($a)) {
            return rtrim($a, '/');
        }
    }
    $candidates = array_filter([
        getenv('AER1_DIR') ?: null,
        dirname(__DIR__, 2) . '/research/zambo-aer1/aer-1',
        '/workspace/research/zambo-aer1/aer-1',
    ]);
    foreach ($candidates as $c) {
        if (is_dir($c)) {
            return $c;
        }
    }
    fwrite(STDERR, "error: cannot locate the aer-1 spec directory\n");
    exit(2);
}

function aer1_load_json_file(string $path)
{
    $text = file_get_contents($path);
    if ($text === false) {
        return null;
    }
    return aer1_json_decode($text);
}

function aer1_json_files(string $dir): array
{
    $files = glob(rtrim($dir, '/') . '/*.json') ?: [];
    sort($files);
    return $files;
}

$HERE = aer1_locate_spec($argv);
$problems = [];
$counts = [
    'valid_verified' => 0, 'valid_total' => 0,
    'invalid_rejected' => 0, 'invalid_total' => 0,
    'profile_invalid' => 0, 'profile_total' => 0,
    'anchored' => 0, 'anchored_total' => 0,
    'merkle' => 0, 'merkle_total' => 0,
    'chain_v06' => 0, 'chain_v06_total' => 0,
    'chain_v07' => 0, 'chain_v07_total' => 0,
    'commitment' => 0, 'commitment_total' => 0,
];

// ---------------- fixture dirs ----------------
$validPaths = aer1_json_files("$HERE/test-vectors/valid");
$invalidPaths = aer1_json_files("$HERE/test-vectors/invalid");
$profilePaths = aer1_json_files("$HERE/test-vectors/invalid-profile");
$anchoredPaths = aer1_json_files("$HERE/test-vectors/anchored");
// An empty fixture set must never silently pass.
if (!$validPaths)    $problems[] = 'no fixtures in test-vectors/valid (empty glob)';
if (!$invalidPaths)  $problems[] = 'no fixtures in test-vectors/invalid (empty glob)';
if (!$profilePaths)  $problems[] = 'no fixtures in test-vectors/invalid-profile (empty glob)';
if (!$anchoredPaths) $problems[] = 'no fixtures in test-vectors/anchored (empty glob)';

foreach ($validPaths as $path) {
    $name = basename($path);
    $counts['valid_total']++;
    $receipt = aer1_load_json_file($path);
    $fails = aer1_check($receipt);
    $ok = !$fails;
    if ($ok) $counts['valid_verified']++;
    printf("[valid]   %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
        $fails ? ' (' . implode('; ', $fails) . ')' : '');
    if (!$ok) $problems[] = "$name should conform but does not";
}

foreach ($invalidPaths as $path) {
    $name = basename($path);
    $counts['invalid_total']++;
    $receipt = aer1_load_json_file($path);
    $fails = aer1_check($receipt);
    $ok = (bool) $fails;
    if ($ok) $counts['invalid_rejected']++;
    printf("[invalid] %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
        $fails ? " (rejected: {$fails[0]})" : ' (accepted!)');
    if (!$ok) $problems[] = "$name should be rejected but was accepted";
}

// Profile boundary vectors: core-valid by design, MUST fail the profile check.
foreach ($profilePaths as $path) {
    $name = basename($path);
    $counts['profile_total']++;
    $receipt = aer1_load_json_file($path);
    $coreFails = aer1_check($receipt);
    $profFails = aer1_check_reference_profile($receipt);
    if ($coreFails) {
        printf("[profile-invalid] %s: FAIL (unexpected core failure: %s)\n",
            $name, $coreFails[0]);
        $problems[] = "$name should be core-valid but fails core: {$coreFails[0]}";
    } elseif (!$profFails) {
        printf("[profile-invalid] %s: FAIL (profile accepted!)\n", $name);
        $problems[] = "$name should be profile-rejected but was accepted";
    } else {
        $counts['profile_invalid']++;
        printf("[profile-invalid] %s: PASS (core accepted, profile rejected: %s)\n",
            $name, $profFails[0]);
    }
}

foreach ($anchoredPaths as $path) {
    $name = basename($path);
    $counts['anchored_total']++;
    $receipt = aer1_load_json_file($path);
    $coreFails = aer1_check($receipt);
    $tierFails = aer1_check_disinterested_tier($receipt);
    $expectedPass = ($name === 'anchored-valid.json');
    $passed = !$coreFails && ($expectedPass ? !$tierFails : (bool) $tierFails);
    if ($passed) $counts['anchored']++;
    printf("[anchored] %s: %s%s\n", $name, $passed ? 'PASS' : 'FAIL',
        $tierFails ? " (tier: {$tierFails[0]})" : '');
    if ($coreFails) $problems[] = "$name should be core-valid but fails core";
    if ($expectedPass && $tierFails) $problems[] = "$name should pass disinterested tier";
    if (!$expectedPass && !$tierFails) $problems[] = "$name should fail disinterested tier";
}

// ---------------- merkle-vectors.json ----------------
$path = "$HERE/merkle-vectors.json";
if (!file_exists($path)) {
    $problems[] = 'merkle-vectors.json is missing (no silent pass)';
} else {
    $corpus = aer1_load_json_file($path);
    $vectors = aer1_get($corpus, 'vectors') ?? [];
    if (!$vectors) {
        $problems[] = 'merkle-vectors.json contains no vectors (no silent pass)';
    }
    foreach ($vectors as $v) {
        $name = aer1_get($v, 'name') ?? '?';
        if (aer1_has($v, 'receipt_ids')) {
            $counts['merkle_total']++;
            $got = aer1_merkle_root(aer1_get($v, 'receipt_ids'));
            $ok = ($got === aer1_get($v, 'expected_root'));
            if ($ok) $counts['merkle']++;
            printf("[merkle]  %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
                $ok ? '' : " (got $got)");
            if (!$ok) {
                $problems[] = "merkle vector $name: expected "
                    . aer1_get($v, 'expected_root') . ", got $got";
            }
        } elseif (aer1_has($v, 'leaves')) {
            // Legacy construction: leaf = sha256(preimage), sorted by seq.
            $counts['merkle_total']++;
            $leaves = aer1_get($v, 'leaves');
            usort($leaves, fn($a, $b) => aer1_get($a, 'seq') <=> aer1_get($b, 'seq'));
            $lvl = array_map(fn($l) => hash('sha256', aer1_get($l, 'preimage'), true), $leaves);
            while (count($lvl) > 1) {
                if (count($lvl) % 2 !== 0) $lvl[] = $lvl[count($lvl) - 1];
                $next = [];
                for ($i = 0; $i < count($lvl); $i += 2) {
                    $next[] = hash('sha256', $lvl[$i] . $lvl[$i + 1], true);
                }
                $lvl = $next;
            }
            $got = $lvl ? bin2hex($lvl[0]) : hash('sha256', '');
            $ok = ($got === aer1_get($v, 'expected_root'))
                && ($got !== aer1_get($v, 'must_not_equal'));
            if ($ok) $counts['merkle']++;
            printf("[merkle]  %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
                $ok ? '' : " (got $got)");
            if (!$ok) {
                $problems[] = "merkle vector $name: legacy root mismatch (got $got)";
            }
        }
        if (aer1_get($v, 'must_not_equal') && aer1_has($v, 'receipt_ids')) {
            if (aer1_get($v, 'expected_root') === aer1_get($v, 'must_not_equal')) {
                $problems[] = "merkle vector $name: root must differ from the "
                    . "canonical/legacy counterpart";
            }
        }
        // Workflow-verdict extension.
        $verdictField = aer1_get($v, 'expected_workflow_verdict');
        if ($verdictField !== null) {
            if (!aer1_has($v, 'receipt_ids')) {
                $problems[] = "merkle vector $name: expected_workflow_verdict needs "
                    . "receipt_ids (no silent pass)";
                printf("[merkle-workflow] %s: FAIL (no receipt_ids)\n", $name);
                continue;
            }
            $wf = [
                'type' => 'verifiable-workflow-receipt',
                'version' => '1',
                'workflow_id' => '11111111-1111-1111-1111-111111111111',
                'receipt_id' => '22222222-2222-2222-2222-222222222222',
                'session_id' => 'test-session',
                'goal' => 'test workflow',
                'status' => 'ok',
                'steps' => [],
                'merkle_root' => aer1_get($v, 'expected_root'),
                'output_hash' => str_repeat('b', 64),
                'verify_url' => 'https://example.com/workflow/test',
            ];
            foreach (aer1_get($v, 'receipt_ids') as $i => $rid) {
                $wf['steps'][] = [
                    'seq' => $i + 1,
                    'tool' => 'test-tool',
                    'receipt_id' => $rid,
                    'receipt_hash' => str_repeat('a', 64),
                    'started_at' => '2026-01-01T00:00:00Z',
                    'ended_at' => '2026-01-01T00:00:01Z',
                    'status' => 'ok',
                ];
            }
            $wfFails = aer1_verify_workflow($wf);
            $verdict = $wfFails ? 'invalid' : 'valid';
            $ok = ($verdict === $verdictField);
            if ($ok && $verdict === 'invalid') {
                $rb = aer1_get($v, 'rejected_by');
                $hit = false;
                if (is_string($rb)) {
                    foreach ($wfFails as $f) {
                        if (str_contains($f, $rb)) { $hit = true; break; }
                    }
                }
                $ok = $hit;
            }
            printf("[merkle-workflow] %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
                $ok ? '' : " (verdict $verdict)");
            if (!$ok) {
                $problems[] = "merkle vector $name: expected workflow verdict "
                    . "$verdictField, got $verdict";
            }
        }
    }
}

// ---------------- chain corpora ----------------
$chainCorpora = [
    ['file' => 'chain-vectors.json',    'tag' => 'chain',     'fn' => 'aer1_verify_chain',    'ck' => 'chain_v06'],
    ['file' => 'chain-vectors-v07.json', 'tag' => 'chain-v07', 'fn' => 'aer1_verify_chain_v07', 'ck' => 'chain_v07'],
];
foreach ($chainCorpora as $cc) {
    $path = "$HERE/{$cc['file']}";
    if (!file_exists($path)) {
        $problems[] = "{$cc['file']} is missing (no silent pass)";
        continue;
    }
    $corpus = aer1_load_json_file($path);
    $vectors = aer1_get($corpus, 'vectors') ?? [];
    if (!$vectors) {
        $problems[] = "{$cc['file']} contains no vectors (no silent pass)";
    }
    foreach ($vectors as $v) {
        $name = aer1_get($v, 'name') ?? '?';
        $counts[$cc['ck'] . '_total']++;
        $expected = aer1_get($v, 'expected_verdict');
        if (!in_array($expected, ['valid', 'invalid'], true)) {
            $problems[] = "{$cc['tag']} vector $name: unknown expected_verdict "
                . "(no silent pass)";
            printf("[%-9s] %s: FAIL (unknown expected_verdict)\n", $cc['tag'], $name);
            continue;
        }
        // The corpus names the timeline member "timeline"; accept "entries".
        $tl = aer1_get($v, 'timeline');
        if ($tl === null) {
            $tl = aer1_get($v, 'entries');
        }
        $fails = ($cc['fn'])($tl);
        $ok = (!$fails) === ($expected === 'valid');
        if ($ok) $counts[$cc['ck']]++;
        printf("[%-9s] %s: %s%s\n", $cc['tag'], $name, $ok ? 'PASS' : 'FAIL',
            $ok ? '' : ' (' . implode('; ', $fails) . ')');
        if (!$ok) {
            $problems[] = "{$cc['tag']} vector $name: expected $expected, got "
                . ($fails ? 'invalid' : 'valid');
        }
    }
}

// ---------------- commitment-vectors.json ----------------
$path = "$HERE/commitment-vectors.json";
if (!file_exists($path)) {
    $problems[] = 'commitment-vectors.json is missing (no silent pass)';
} else {
    $corpus = aer1_load_json_file($path);
    $vectors = aer1_get($corpus, 'vectors') ?? [];
    if (!$vectors) {
        $problems[] = 'commitment-vectors.json contains no vectors (no silent pass)';
    }
    foreach ($vectors as $v) {
        $name = aer1_get($v, 'name') ?? '?';
        $counts['commitment_total']++;
        $expected = aer1_get($v, 'expected_verdict');
        if (!in_array($expected, ['valid', 'invalid'], true)) {
            $problems[] = "commitment vector $name: unknown expected_verdict "
                . "(no silent pass)";
            printf("[commitment] %s: FAIL (unknown expected_verdict)\n", $name);
            continue;
        }
        $fails = aer1_verify_chain_commitment(
            aer1_get($v, 'timeline'), aer1_get($v, 'commitment'));
        $ok = (!$fails) === ($expected === 'valid');
        if ($ok) $counts['commitment']++;
        printf("[commitment] %s: %s%s\n", $name, $ok ? 'PASS' : 'FAIL',
            $ok ? '' : ' (' . implode('; ', $fails) . ')');
        if (!$ok) {
            $problems[] = "commitment vector $name: expected $expected, got "
                . ($fails ? 'invalid' : 'valid');
        }
    }
}

// ---------------- summary ----------------
echo "\n";
printf("summary: valid verified %d/%d, invalid rejected %d/%d, "
    . "profile-invalid %d/%d, anchored %d/%d\n",
    $counts['valid_verified'], $counts['valid_total'],
    $counts['invalid_rejected'], $counts['invalid_total'],
    $counts['profile_invalid'], $counts['profile_total'],
    $counts['anchored'], $counts['anchored_total']);
printf("summary: merkle %d/%d, chain v06 %d/%d, chain v07 %d/%d, "
    . "commitment %d/%d\n",
    $counts['merkle'], $counts['merkle_total'],
    $counts['chain_v06'], $counts['chain_v06_total'],
    $counts['chain_v07'], $counts['chain_v07_total'],
    $counts['commitment'], $counts['commitment_total']);

if ($problems) {
    echo "\nCONFORMANCE FAILED:\n";
    foreach ($problems as $p) {
        echo " - $p\n";
    }
    exit(1);
}
echo "\nCONFORMANCE OK: all vectors behave as expected.\n";
exit(0);
