// runtime/eltest.el — El test framework runner (Phase 1).
//
// This is the RUNNER. It is written in El and consumes a registry that the
// compiler generates into the same translation unit when invoked as
// `elc --test`. Nothing here discovers tests; discovery already happened at
// compile time, which is what makes `--list` and filtering possible later.
//
// ── Architecture ─────────────────────────────────────────────────────────────
//
// The compiler lowers each `test "name" { ... }` block into a static C
// function and emits a static table of (name, fn) pairs plus a small set of
// index-based accessors. El has no function pointers, so the runner never
// sees one — it works entirely in indices:
//
//   __el_reg_count()      -> Int     number of registered tests
//   __el_reg_name(i)      -> String  test name at index i
//   __el_reg_invoke(i)    -> Int     run test i, return its failure count
//   __el_reg_last_ns()    -> Int     wall-clock ns of the last invoke
//   __el_reg_msg()        -> String  first failure message of the last invoke
//   __el_reg_asserts()    -> Int     assertions executed in the last invoke
//   __el_opt_json()       -> Int     1 if --json was passed
//
// Timing is taken in the generated C, immediately around the call, so no El
// call overhead lands inside the measurement.
//
// ── Output ───────────────────────────────────────────────────────────────────
//
// Structured events are the source of truth. The human renderer is written
// FROM the same fields the NDJSON renderer emits — never the reverse. Parsing
// human output back into structure is the one clear architectural mistake in
// Go's test tooling and we do not repeat it.
//
// Every result carries a duration. Always. A framework that cannot report how
// long its tests took cannot surface a performance regression, and a
// regression nobody can see is one nobody fixes.

// ── Small helpers (no imports — this file must stay self-contained) ──────────

// _elt_json_escape — minimal JSON string escaping for the NDJSON renderer.
fn _elt_json_escape(s: String) -> String {
    let out: String = ""
    let n: Int = str_len(s)
    let i: Int = 0
    while i < n {
        let ch: String = str_slice(s, i, i + 1)
        if str_eq(ch, "\"") {
            let out = out + "\\\""
        } else {
            if str_eq(ch, "\\") {
                let out = out + "\\\\"
            } else {
                if str_eq(ch, "\n") {
                    let out = out + "\\n"
                } else {
                    if str_eq(ch, "\t") {
                        let out = out + "\\t"
                    } else {
                        if str_eq(ch, "\r") {
                            let out = out + "\\r"
                        } else {
                            let out = out + ch
                        }
                    }
                }
            }
        }
        let i = i + 1
    }
    return out
}

// _elt_pad3 — left-pad an integer to three digits (for the ms.fraction form).
fn _elt_pad3(v: Int) -> String {
    if v < 10 { return "00" + int_to_str(v) }
    if v < 100 { return "0" + int_to_str(v) }
    return int_to_str(v)
}

// _elt_ms — render a nanosecond duration as "M.mmm" milliseconds.
//
// Deliberately avoids the modulo operator: the remainder is derived by
// subtraction so this stays portable across El backends.
fn _elt_ms(ns: Int) -> String {
    let total_us: Int = ns / 1000
    let ms_whole: Int = total_us / 1000
    let us_rem: Int = total_us - (ms_whole * 1000)
    return int_to_str(ms_whole) + "." + _elt_pad3(us_rem)
}

// _elt_secs — render a nanosecond duration as fractional seconds, for the
// NDJSON `elapsed` field. JUnit XML and test2json both use seconds-as-decimal.
fn _elt_secs(ns: Int) -> String {
    let total_ms: Int = ns / 1000000
    let s_whole: Int = total_ms / 1000
    let ms_rem: Int = total_ms - (s_whole * 1000)
    return int_to_str(s_whole) + "." + _elt_pad3(ms_rem)
}

// ── Event emission ───────────────────────────────────────────────────────────
//
// One function per event shape. Both renderers read the same fields; the
// human renderer is a projection of the event, not a separate code path.

fn _elt_emit_run(json_mode: Bool, name: String) {
    if json_mode {
        println("{\"action\":\"run\",\"test\":\"" + _elt_json_escape(name) + "\"}")
    }
}

fn _elt_emit_result(json_mode: Bool, name: String, fails: Int, ns: Int, asserts: Int, msg: String) {
    if json_mode {
        let action: String = "pass"
        if fails > 0 { let action = "fail" }
        let line: String = "{\"action\":\"" + action + "\""
        let line = line + ",\"test\":\"" + _elt_json_escape(name) + "\""
        let line = line + ",\"elapsed\":" + _elt_secs(ns)
        let line = line + ",\"assertions\":" + int_to_str(asserts)
        if fails > 0 {
            let line = line + ",\"failures\":" + int_to_str(fails)
            let line = line + ",\"message\":\"" + _elt_json_escape(msg) + "\""
        }
        let line = line + "}"
        println(line)
        return
    }
    // Human renderer — duration is never optional.
    if fails > 0 {
        println("FAIL  " + name + "  (" + _elt_ms(ns) + "ms)")
        println("      " + msg)
        return
    }
    println("ok    " + name + "  (" + _elt_ms(ns) + "ms)")
    return
}

fn _elt_emit_summary(json_mode: Bool, total: Int, failed: Int, ns: Int, asserts: Int) {
    let passed: Int = total - failed
    if json_mode {
        let line: String = "{\"action\":\"summary\""
        let line = line + ",\"tests\":" + int_to_str(total)
        let line = line + ",\"passed\":" + int_to_str(passed)
        let line = line + ",\"failed\":" + int_to_str(failed)
        let line = line + ",\"assertions\":" + int_to_str(asserts)
        let line = line + ",\"elapsed\":" + _elt_secs(ns)
        let line = line + "}"
        println(line)
        return
    }
    println("")
    println(int_to_str(total) + " tests, " + int_to_str(passed) + " passed, "
        + int_to_str(failed) + " failed, " + int_to_str(asserts) + " assertions in "
        + _elt_ms(ns) + "ms")
    return
}

// ── The runner ───────────────────────────────────────────────────────────────

// el_test_main — drive the compile-time registry.
//
// Called from the generated main(). Returns the number of FAILING TESTS, which
// becomes the process exit code. Note that this counts tests, not assertions:
// a test is the unit of result. The old harness counted assertions globally and
// therefore could not say which test failed, how long any of them took, or
// whether a test had run at all.
fn el_test_main() -> Int {
    let json_mode: Bool = false
    if __el_opt_json() == 1 { let json_mode = true }

    let n: Int = __el_reg_count()
    let i: Int = 0
    let failed: Int = 0
    let total_ns: Int = 0
    let total_asserts: Int = 0

    while i < n {
        let name: String = __el_reg_name(i)
        _elt_emit_run(json_mode, name)

        let fails: Int = __el_reg_invoke(i)
        let ns: Int = __el_reg_last_ns()
        let asserts: Int = __el_reg_asserts()
        let msg: String = __el_reg_msg()

        let total_ns = total_ns + ns
        let total_asserts = total_asserts + asserts
        if fails > 0 { let failed = failed + 1 }

        _elt_emit_result(json_mode, name, fails, ns, asserts, msg)
        let i = i + 1
    }

    _elt_emit_summary(json_mode, n, failed, total_ns, total_asserts)
    return failed
}
