#!/usr/bin/env lua5.4
--[[
  Replays the ARION AER-1 negative must-fail corpus (neg-corpus/
  manifest.json) through the Lua port of verify.js, asserting each
  case produces EXACTLY the pinned {check,reason} failure set — a port
  of neg-corpus/run.js. Usage:

      lua5.4 run_negcorpus.lua [path-to-neg-corpus-dir]

  Expects aer1verify.lua on package.path (same dir works).
]]

local here = arg and arg[0] and arg[0]:match("^(.*)/[^/]*$") or "."
package.path = here .. "/?.lua;" .. package.path
local V = require("aer1verify")

local DIR = (arg and arg[1]) or (here .. "/../neg-corpus")

local function readFile(p)
    local f = io.open(p, "rb")
    if not f then return nil end
    local t = f:read("a"); f:close(); return t
end

local manifest = V.parseJson(readFile(DIR .. "/manifest.json"))
if manifest == nil then
    io.stderr:write("cannot load " .. DIR .. "/manifest.json\n")
    os.exit(2)
end

local pass, fail = 0, 0

local function runDoc(doc, flags)
    local opts = { kind = "auto" }
    if flags then
        for k in pairs(flags) do opts[k] = flags[k] end
    end
    local _, failures = V.verifyDocument(doc, opts)
    return failures
end

local function diff(expect, observed)
    local e, o = {}, {}
    if expect then
        for _, x in ipairs(expect) do
            e[x.check .. " :: " .. x.reason] = true
        end
    end
    for _, x in ipairs(observed) do
        o[x.check .. " :: " .. x.reason] = true
    end
    local missing, extra = {}, {}
    for k in pairs(e) do if not o[k] then missing[#missing + 1] = k end end
    for k in pairs(o) do if not e[k] then extra[#extra + 1] = k end end
    table.sort(missing); table.sort(extra)
    return missing, extra
end

local function loadVec(file)
    local doc = V.parseJson(readFile(DIR .. "/" .. file))
    if doc == nil then error("unparseable vector " .. file, 0) end
    return doc
end

for _, c in ipairs(manifest.cases) do
    local doc = loadVec(c.file)
    local observed = runDoc(doc, c.flags)
    local missing, extra = diff(c.expect, observed)
    if #missing == 0 and #extra == 0 then
        pass = pass + 1
        print("PASS " .. c.name)
    else
        fail = fail + 1
        io.write("FAIL " .. c.name)
        if #missing > 0 then
            io.write(" missing=" .. table.concat(missing, "|"))
        end
        if #extra > 0 then
            io.write(" extra=" .. table.concat(extra, "|"))
        end
        io.write("\n")
    end
end

for _, d in ipairs(manifest.differentials) do
    local doc = loadVec(d.file)
    local loose = runDoc(doc, {})
    local strict = d.strict_mode == "chain06"
        and runDoc(doc, { kind = "chain06" })
        or runDoc(doc, { strict = true })
    local ml, el = diff(d.expect_loose, loose)
    local ms, es = diff(d.expect_strict, strict)
    if #ml == 0 and #el == 0 and #ms == 0 and #es == 0 then
        pass = pass + 1
        print("PASS " .. d.name .. " (differential)")
    else
        fail = fail + 1
        io.write("FAIL " .. d.name .. " (differential)")
        if #ml > 0 or #el > 0 then
            io.write(" looseDiff=" .. table.concat(ml, "|")
                .. "/" .. table.concat(el, "|"))
        end
        if #ms > 0 or #es > 0 then
            io.write(" strictDiff=" .. table.concat(ms, "|")
                .. "/" .. table.concat(es, "|"))
        end
        io.write("\n")
    end
end

print("\n" .. pass .. "/" .. (pass + fail) .. " corpus cases conform")
os.exit(fail == 0 and 0 or 1)
