// runtime/test.el — El test framework: assertions, registration, and runner.
//
// Provides a minimal but complete test harness for El programs. No external
// dependencies. Written entirely in El using existing runtime primitives.
//
// ── Quick-start ──────────────────────────────────────────────────────────────
//
// 1. Write a test function with the standard test signature:
//
//      fn test_str_eq_works(_: String) -> String {
//          assert_true(str_eq("a", "a"), "same strings are equal")
//          assert_false(str_eq("a", "b"), "different strings are not equal")
//          return ""
//      }
//
// 2. Register it and run:
//
//      fn main() -> Void {
//          test_case("str_eq works", "test_str_eq_works")
//          test_run_all()
//      }
//
// Test functions must have the signature (String) -> String. The argument is
// a dummy passed by the threading mechanism (see runtime/thread.el) and should
// be ignored. The return value is likewise ignored — results flow through the
// state-based assertion primitives.
//
// ── State keys ───────────────────────────────────────────────────────────────
//
//   _test_cases       JSON array of {"name":"...","fn":"..."}
//   _test_pass_count  Int as string — total passing assertions
//   _test_fail_count  Int as string — total failing assertions
//   _test_failures    JSON array of failure message strings
//   _test_current     Name of the test case currently executing
//
// ── Dependencies ─────────────────────────────────────────────────────────────
//
//   runtime/string.el   — str_eq, str_concat, str_contains, int_to_str, ...
//   runtime/state.el    — state_set, state_get
//   runtime/json.el     — json_array_len, json_array_get_string, json_get,
//                         json_escape_string
//   runtime/thread.el   — spawn, join (for dynamic dispatch via dlsym)

// ── Internal: JSON array helpers ─────────────────────────────────────────────
//
// _test_json_append — append a quoted, escaped string element to a JSON array.
//
// Given an existing JSON array string (e.g. '["a","b"]') and a plain string
// value, returns a new array with the value appended (e.g. '["a","b","c"]').
//
// The array must be non-empty — always init with "[]" before calling.
fn _test_json_append(arr: String, val: String) -> String {
    let escaped: String = json_escape_string(val)
    let inner: String = str_slice(arr, 1, str_len(arr) - 1)
    if str_eq(inner, "") {
        return "[\"" + escaped + "\"]"
    }
    return "[" + inner + ",\"" + escaped + "\"]"
}

// _test_json_obj_append — append a raw JSON object string to a JSON array.
//
// Used to build the _test_cases list where each element is already a
// JSON object (not a plain string).
fn _test_json_obj_append(arr: String, obj: String) -> String {
    let inner: String = str_slice(arr, 1, str_len(arr) - 1)
    if str_eq(inner, "") {
        return "[" + obj + "]"
    }
    return "[" + inner + "," + obj + "]"
}

// ── Test registration ─────────────────────────────────────────────────────────

// test_case — register a named test case.
//
//   name:    Human-readable test case name (shown in output).
//   fn_name: Name of a top-level El function with signature
//            (String) -> String. The function should call assertion
//            primitives from this module. The String arg it receives is ""
//            and its return value is ignored.
//
// Test cases are stored in the _test_cases state key and executed in
// registration order by test_run_all().
fn test_case(name: String, fn_name: String) {
    let arr: String = state_get("_test_cases")
    if str_eq(arr, "") { arr = "[]" }
    let escaped_name: String = json_escape_string(name)
    let escaped_fn: String = json_escape_string(fn_name)
    let obj: String = "{\"name\":\"" + escaped_name + "\",\"fn\":\"" + escaped_fn + "\"}"
    let arr = _test_json_obj_append(arr, obj)
    state_set("_test_cases", arr)
}

// ── Test state helpers ────────────────────────────────────────────────────────

// _test_init — reset all test counters and failure lists.
//
// Called at the start of test_run_all(). Safe to call multiple times.
fn _test_init() {
    state_set("_test_pass_count", "0")
    state_set("_test_fail_count", "0")
    state_set("_test_failures", "[]")
    state_set("_test_current", "")
}

// _test_inc_pass — increment the global pass counter by 1.
fn _test_inc_pass() {
    let n: Int = str_to_int(state_get("_test_pass_count"))
    state_set("_test_pass_count", int_to_str(n + 1))
}

// _test_inc_fail — increment the global fail counter by 1.
fn _test_inc_fail() {
    let n: Int = str_to_int(state_get("_test_fail_count"))
    state_set("_test_fail_count", int_to_str(n + 1))
}

// test_pass — record a passing assertion for the current test case.
//
// Increments the pass counter. Called internally by assertions.
fn test_pass(name: String) {
    _test_inc_pass()
}

// test_fail — record a failing assertion for the current test case.
//
//   name: assertion label or description (usually the msg parameter)
//   msg:  detailed failure message including expected/got values
//
// Increments the fail counter and appends the message to _test_failures.
// Also prints the failure immediately for visibility.
fn test_fail(name: String, msg: String) {
    _test_inc_fail()
    let failures: String = state_get("_test_failures")
    if str_eq(failures, "") { failures = "[]" }
    let entry: String = "  " + msg
    let failures = _test_json_append(failures, entry)
    state_set("_test_failures", failures)
    println("  FAIL: " + msg)
}

// ── Assertions ────────────────────────────────────────────────────────────────

// assert_true — assert that condition is true.
//
//   condition: the boolean value to test
//   msg:       description shown on failure
fn assert_true(condition: Bool, msg: String) {
    if condition {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": expected true, got false")
}

// assert_false — assert that condition is false.
//
//   condition: the boolean value to test
//   msg:       description shown on failure
fn assert_false(condition: Bool, msg: String) {
    if !condition {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": expected false, got true")
}

// assert_eq — assert that two strings are equal.
//
//   a, b: strings to compare
//   msg:  description shown on failure (quoted values appended automatically)
fn assert_eq(a: String, b: String, msg: String) {
    if str_eq(a, b) {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": expected \"" + b + "\", got \"" + a + "\"")
}

// assert_int_eq — assert that two integers are equal.
//
//   a, b: integers to compare
//   msg:  description shown on failure
fn assert_int_eq(a: Int, b: Int, msg: String) {
    if a == b {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": expected " + int_to_str(b) + ", got " + int_to_str(a))
}

// assert_neq — assert that two strings are NOT equal.
//
//   a, b: strings to compare
//   msg:  description shown on failure
fn assert_neq(a: String, b: String, msg: String) {
    if !str_eq(a, b) {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": expected values to differ, but both are \"" + a + "\"")
}

// assert_contains — assert that string s contains substring sub.
//
//   s:   haystack string
//   sub: needle substring
//   msg: description shown on failure
fn assert_contains(s: String, sub: String, msg: String) {
    if str_contains(s, sub) {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not contain \"" + sub + "\"")
}

// assert_starts_with — assert that string s starts with prefix.
//
//   s:      string to inspect
//   prefix: expected prefix
//   msg:    description shown on failure
fn assert_starts_with(s: String, prefix: String, msg: String) {
    if str_starts_with(s, prefix) {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not start with \"" + prefix + "\"")
}

// assert_ends_with — assert that string s ends with suffix.
//
//   s:      string to inspect
//   suffix: expected suffix
//   msg:    description shown on failure
fn assert_ends_with(s: String, suffix: String, msg: String) {
    if str_ends_with(s, suffix) {
        test_pass(msg)
        return
    }
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg + ": \"" + s + "\" does not end with \"" + suffix + "\"")
}

// fail — unconditional test failure.
//
//   msg: failure message shown in output
//
// Use when a code path that must not be reached is reached, or when an
// expected exception did not occur.
fn fail(msg: String) {
    let test_name: String = state_get("_test_current")
    test_fail(msg, "[" + test_name + "] " + msg)
}

// ── Runner ────────────────────────────────────────────────────────────────────

// _test_run_one — execute a single registered test case by name.
//
//   name:    the human-readable test case name (set as _test_current)
//   fn_name: the El function to invoke via the thread mechanism
//
// Sets _test_current so that assertions inside the test function know which
// test they belong to. Spawns and immediately joins the test function in a
// child thread (same dlsym mechanism as parallel_map) so dynamic dispatch
// works without needing closures.
fn _test_run_one(name: String, fn_name: String) {
    state_set("_test_current", name)
    let before_fail: Int = str_to_int(state_get("_test_fail_count"))
    let tid: Int = __thread_create(fn_name, "")
    __thread_join(tid)
    let after_fail: Int = str_to_int(state_get("_test_fail_count"))
    if after_fail == before_fail {
        println("[test] " + name + " ... PASS")
    } else {
        println("[test] " + name + " ... FAIL")
    }
}

// test_run_all — execute all registered test cases and print a summary.
//
// Iterates through every test case registered via test_case(), runs each one,
// prints per-test PASS/FAIL status, then prints a summary line.
//
// Returns the total number of failing assertions. Exit with this value to
// signal CI failure:
//
//   fn main() -> Int {
//       test_case("str_eq", "test_str_eq")
//       return test_run_all()
//   }
fn test_run_all() -> Int {
    _test_init()

    let cases: String = state_get("_test_cases")
    if str_eq(cases, "") { cases = "[]" }

    let n: Int = json_array_len(cases)
    let i: Int = 0
    while i < n {
        let entry: String = json_array_get(cases, i)
        let name: String = json_get(entry, "name")
        let fn_name: String = json_get(entry, "fn")
        _test_run_one(name, fn_name)
        i = i + 1
    }

    let pass_count: Int = str_to_int(state_get("_test_pass_count"))
    let fail_count: Int = str_to_int(state_get("_test_fail_count"))
    println("[test] Summary: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")

    return fail_count
}
