#!/usr/bin/env ruby
# frozen_string_literal: true
#
# run_conformance.rb -- AER-1 conformance harness for the Ruby verifier.
# Mirrors aer-1/conformance.py main(): runs every corpus vector and prints
# "[corpus] name: PASS/FAIL" plus a summary. Exit 0 when every vector
# behaves as the reference runner expects.
#
# Usage:
#   ruby run_conformance.rb [AER1_REPO_DIR]
# Default corpus root: ../research/zambo-aer1/aer-1 relative to this file
# (override with the argument or the AER1_DIR environment variable).

require "json"
require "openssl"
require_relative "aer1"

AER1_ROOT = ARGV[0] || ENV["AER1_DIR"] ||
            File.expand_path("../../research/zambo-aer1/aer-1", __dir__)

$problems = []
$counts = Hash.new(0)

def load_json_file(path)
  JSON.parse(File.read(path, encoding: "UTF-8"))
rescue StandardError => e
  # A fixture that cannot even be parsed is a harness-level problem, never
  # a silent pass (mirrors the reference runner's fail-closed posture).
  { "__parse_error__" => e.message }
end

def report(corpus, name, ok, detail = nil)
  puts "[#{corpus}] #{name}: #{ok ? 'PASS' : 'FAIL'}#{detail ? " #{detail}" : ''}"
  $counts[corpus] += 1 if ok
end

# ----------------------------------------------------------------------
# Fixture directories
# ----------------------------------------------------------------------
def run_receipt_corpora
  %w[valid invalid invalid-profile anchored].each do |dir|
    paths = Dir.glob(File.join(AER1_ROOT, "test-vectors", dir, "*.json")).sort
    if paths.empty?
      $problems << "no fixtures in test-vectors/#{dir} (empty glob)"
      next
    end
    paths.each { |p| yield dir, p }
  end
end

def corpus_valid(path)
  receipt = load_json_file(path)
  fails = receipt.key?("__parse_error__") ? ["unparseable: #{receipt['__parse_error__']}"] : AER1.check(receipt)
  report("valid", File.basename(path), fails.empty?,
         fails.empty? ? nil : "(#{fails.join('; ')})")
  $problems << "#{File.basename(path)} should conform but does not" unless fails.empty?
end

def corpus_invalid(path)
  receipt = load_json_file(path)
  fails = receipt.key?("__parse_error__") ? ["unparseable: #{receipt['__parse_error__']}"] : AER1.check(receipt)
  report("invalid", File.basename(path), !fails.empty?,
         fails.empty? ? "(accepted!)" : "(rejected: #{fails[0]})")
  $problems << "#{File.basename(path)} should be rejected but was accepted" if fails.empty?
end

def corpus_profile_invalid(path)
  receipt = load_json_file(path)
  core_fails  = AER1.check(receipt)
  prof_fails  = AER1.check_reference_profile(receipt)
  if !core_fails.empty?
    report("profile-invalid", File.basename(path), false,
           "(unexpected core failure: #{core_fails[0]})")
    $problems << "#{File.basename(path)} should be core-valid but fails core: #{core_fails[0]}"
  elsif prof_fails.empty?
    report("profile-invalid", File.basename(path), false, "(profile accepted!)")
    $problems << "#{File.basename(path)} should be profile-rejected but was accepted"
  else
    report("profile-invalid", File.basename(path), true,
           "(core accepted, profile rejected: #{prof_fails[0]})")
  end
end

def corpus_anchored(path)
  receipt = load_json_file(path)
  core_fails = AER1.check(receipt)
  tier_fails = AER1.check_disinterested_tier(receipt)
  expected_pass = File.basename(path) == "anchored-valid.json"
  passed = core_fails.empty? && (expected_pass ? tier_fails.empty? : !tier_fails.empty?)
  report("anchored", File.basename(path), passed,
         tier_fails.empty? ? nil : "(tier: #{tier_fails[0]})")
  $problems << "#{File.basename(path)} should be core-valid but fails core" unless core_fails.empty?
  $problems << "#{File.basename(path)} should pass disinterested tier" if expected_pass && !tier_fails.empty?
  $problems << "#{File.basename(path)} should fail disinterested tier" if !expected_pass && tier_fails.empty?
end

# ----------------------------------------------------------------------
# Section 8.1 / 8.2 Merkle + workflow vectors
# ----------------------------------------------------------------------
def check_merkle_vectors
  path = File.join(AER1_ROOT, "merkle-vectors.json")
  return $problems << "merkle-vectors.json is missing (no silent pass)" unless File.exist?(path)
  vectors = (load_json_file(path)["vectors"] || [])
  return $problems << "merkle-vectors.json contains no vectors (no silent pass)" if vectors.empty?

  vectors.each do |v|
    name = v["name"] || "?"
    if v.key?("receipt_ids")
      got = AER1.merkle_root_canonical(v["receipt_ids"])
      ok = got == v["expected_root"]
      report("merkle", name, ok, ok ? nil : "(got #{got})")
      $problems << "merkle vector #{name}: expected #{v['expected_root']}, got #{got}" unless ok
    elsif v.key?("leaves")
      # Legacy construction: leaf = sha256(preimage utf-8), ordered by seq;
      # the historical root must recompute and must differ from canonical.
      lvl = v["leaves"].sort_by { |l| l["seq"] }
                     .map { |l| OpenSSL::Digest::SHA256.digest(l["preimage"].to_s) }
      while lvl.length > 1
        lvl << lvl[-1] if lvl.length.odd?
        lvl = (0...lvl.length).step(2).map do |i|
          OpenSSL::Digest::SHA256.digest(lvl[i] + lvl[i + 1])
        end
      end
      got = lvl.empty? ? OpenSSL::Digest::SHA256.hexdigest("") : lvl[0].unpack1("H*")
      ok = (got == v["expected_root"] && got != v["must_not_equal"])
      report("merkle", name, ok, ok ? nil : "(got #{got})")
      $problems << "merkle vector #{name}: legacy root mismatch (got #{got})" unless ok
    end
    if v["must_not_equal"] && v.key?("receipt_ids") && v["expected_root"] == v["must_not_equal"]
      $problems << "merkle vector #{name}: root must differ from the canonical/legacy counterpart"
    end

    verdict_field = v["expected_workflow_verdict"]
    next if verdict_field.nil?
    unless v.key?("receipt_ids")
      $problems << "merkle vector #{name}: expected_workflow_verdict needs receipt_ids (no silent pass)"
      report("merkle-workflow", name, false, "(no receipt_ids)")
      next
    end
    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" => v["receipt_ids"].each_with_index.map do |rid, i|
        { "seq" => i + 1, "tool" => "test-tool", "receipt_id" => rid,
          "receipt_hash" => "a" * 64,
          "started_at" => "2026-01-01T00:00:00Z",
          "ended_at" => "2026-01-01T00:00:01Z", "status" => "ok" }
      end,
      "merkle_root" => v["expected_root"],
      "output_hash" => "b" * 64,
      "verify_url" => "https://example.com/workflow/test"
    }
    wf_fails = AER1.verify_workflow(wf)
    verdict = wf_fails.empty? ? "valid" : "invalid"
    ok = verdict == verdict_field
    if ok && verdict == "invalid"
      rb = v["rejected_by"]
      ok = false unless rb.is_a?(String) && wf_fails.any? { |f| f.include?(rb) }
    end
    report("merkle-workflow", name, ok, ok ? nil : "(verdict #{verdict})")
    $problems << "merkle vector #{name}: expected workflow verdict #{verdict_field}, got #{verdict}" unless ok
  end
end

# ----------------------------------------------------------------------
# Section 7 chain vectors (v06 regression + v07 hardened)
# ----------------------------------------------------------------------
def check_chain_vectors(file, corpus, verifier)
  path = File.join(AER1_ROOT, file)
  return $problems << "#{file} is missing (no silent pass)" unless File.exist?(path)
  vectors = (load_json_file(path)["vectors"] || [])
  return $problems << "#{file} contains no vectors (no silent pass)" if vectors.empty?

  vectors.each do |v|
    name = v["name"] || "?"
    expected = v["expected_verdict"]
    unless %w[valid invalid].include?(expected)
      $problems << "#{corpus} vector #{name}: unknown expected_verdict #{expected.inspect} (no silent pass)"
      report(corpus, name, false, "(unknown expected_verdict)")
      next
    end
    tl = v["timeline"]
    tl = v["entries"] if tl.nil?
    fails = AER1.send(verifier, tl)
    ok = fails.empty? == (expected == "valid")
    report(corpus, name, ok, ok ? nil : "(#{fails.join('; ')})")
    $problems << "#{corpus} vector #{name}: expected #{expected}, got #{fails.empty? ? 'valid' : 'invalid'}" unless ok
  end
end

# ----------------------------------------------------------------------
# Section 7.3 commitment vectors
# ----------------------------------------------------------------------
def check_commitment_vectors
  path = File.join(AER1_ROOT, "commitment-vectors.json")
  return $problems << "commitment-vectors.json is missing (no silent pass)" unless File.exist?(path)
  vectors = (load_json_file(path)["vectors"] || [])
  return $problems << "commitment-vectors.json contains no vectors (no silent pass)" if vectors.empty?

  vectors.each do |v|
    name = v["name"] || "?"
    expected = v["expected_verdict"]
    unless %w[valid invalid].include?(expected)
      $problems << "commitment vector #{name}: unknown expected_verdict #{expected.inspect} (no silent pass)"
      report("commitment", name, false, "(unknown expected_verdict)")
      next
    end
    fails = AER1.verify_chain_commitment(v["timeline"], v["commitment"])
    ok = fails.empty? == (expected == "valid")
    report("commitment", name, ok, ok ? nil : "(#{fails.join('; ')})")
    $problems << "commitment vector #{name}: expected #{expected}, got #{fails.empty? ? 'valid' : 'invalid'}" unless ok
  end
end

# ----------------------------------------------------------------------
run_receipt_corpora do |dir, path|
  case dir
  when "valid"           then corpus_valid(path)
  when "invalid"         then corpus_invalid(path)
  when "invalid-profile" then corpus_profile_invalid(path)
  when "anchored"        then corpus_anchored(path)
  end
end
check_merkle_vectors
check_chain_vectors("chain-vectors.json", "chain", :verify_chain)
check_chain_vectors("chain-vectors-v07.json", "chain-v07", :verify_chain_v07)
check_commitment_vectors

puts
puts "Summary:"
printf "  valid verified:        %d\n", $counts["valid"]
printf "  invalid rejected:      %d\n", $counts["invalid"]
printf "  profile-invalid:       %d\n", $counts["profile-invalid"]
printf "  anchored:              %d\n", $counts["anchored"]
printf "  chain (v06) passed:    %d\n", $counts["chain"]
printf "  chain (v07) passed:    %d\n", $counts["chain-v07"]
printf "  commitment passed:     %d\n", $counts["commitment"]
printf "  merkle passed:         %d\n", $counts["merkle"]
printf "  merkle-workflow:       %d\n", $counts["merkle-workflow"]
puts
if $problems.empty?
  puts "CONFORMANCE OK: all vectors behave as expected."
  exit 0
else
  puts "CONFORMANCE FAILED:"
  $problems.each { |p| puts " - #{p}" }
  exit 1
end
