// cell test - Run tests for packages

var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var time = use('time')
var json = use('json')
var blob = use('blob')
var dbg = use('js')
var testlib = use('internal/testlib')

// run gc with dbg.gc()

var _args = args == null ? [] : args

var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
var gc_after_each_test = false
var verify_ir = false
var diff_mode = false

var os_ref = use('internal/os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn

// Actor test support
def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
var pending_actor_tests = []
var actor_test_results = []

var is_valid_package = testlib.is_valid_package
var get_current_package_name = testlib.get_current_package_name

// Parse arguments
// Usage:
//   cell test                          - run all tests for current package
//   cell test tests/suite              - run specific test file in current package
//   cell test all                      - run all tests for current package
//   cell test package <name>           - run all tests for named package
//   cell test package <name> <test>    - run specific test in named package
//   cell test package all              - run all tests from all packages
//
// Flags:
//   -g        - run GC after each test
//   --verify  - enable IR verification (validates mcode IR after each optimizer pass)
//   --diff    - enable differential testing (run each test optimized and unoptimized, compare results)

function parse_args() {
    var cleaned_args = []
    var i = 0
    var name = null
    var lock = null
    var resolved = null
    var test_path = null
    for (i = 0; i < length(_args); i++) {
        if (_args[i] == '-g') {
            gc_after_each_test = true
        } else if (_args[i] == '--verify') {
            verify_ir = true
        } else if (_args[i] == '--diff') {
            diff_mode = true
        } else {
            push(cleaned_args, _args[i])
        }
    }
    _args = cleaned_args

    if (length(_args) == 0) {
        // cell test - run all tests for current package
        if (!is_valid_package('.')) {
            log.console('No cell.toml found in current directory')
            return false
        }
        target_pkg = null
        return true
    }

    if (_args[0] == 'all') {
        // cell test all - run all tests for current package
        if (!is_valid_package('.')) {
            log.console('No cell.toml found in current directory')
            return false
        }
        target_pkg = null
        return true
    }

    if (_args[0] == 'package') {
        if (length(_args) < 2) {
            log.console('Usage: cell test package <name> [test]')
            log.console('       cell test package all')
            return false
        }

        if (_args[1] == 'all') {
            // cell test package all - run tests from all packages
            all_pkgs = true
            log.console('Testing all packages...')
            return true
        }

        // cell test package <name> [test]
        name = _args[1]

        // Check if package exists in lock or is a local path
        lock = shop.load_lock()
        if (lock[name]) {
            target_pkg = name
        } else if (starts_with(name, '/') && is_valid_package(name)) {
            target_pkg = name
        } else {
            // Try to resolve as dependency alias from current package
            resolved = null
            if (is_valid_package('.')) {
                resolved = pkg.alias_to_package(null, name)
                if (resolved) {
                    target_pkg = resolved
                } else {
                    log.console(`Package not found: ${name}`)
                    return false
                }
            } else {
                log.console(`Package not found: ${name}`)
                return false
            }
        }

        if (length(_args) >= 3) {
            // cell test package <name> <test>
            target_test = _args[2]
        }

        log.console(`Testing package: ${target_pkg}`)
        return true
    }

    // cell test tests/suite or cell test <path> - specific test file
    test_path = _args[0]

    // Normalize path - add tests/ prefix if not present and doesn't start with /
    if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) {
        // Check if file exists as-is first
        if (!fd.is_file(test_path + '.cm') && !fd.is_file(test_path)) {
            // Try with tests/ prefix
            if (fd.is_file('tests/' + test_path + '.cm') || fd.is_file('tests/' + test_path)) {
                test_path = 'tests/' + test_path
            }
        }
    }

    target_test = test_path
    target_pkg = null

    if (!is_valid_package('.')) {
        log.console('No cell.toml found in current directory')
        return false
    }

    return true
}

if (!parse_args()) {
    $stop()
    return
}

// Enable IR verification if requested
if (verify_ir) {
    os_ref._verify_ir = true
    log.console('IR verification enabled')
}

if (diff_mode && !run_ast_noopt_fn) {
    log.console('error: --diff requires run_ast_noopt_fn (rebuild bootstrap)')
    $stop()
    return
}

var values_equal = testlib.values_equal
var describe = testlib.describe

// Diff mode: run a test function through noopt and compare
var diff_mismatches = 0
function diff_check(test_name, file_path, opt_fn, noopt_fn) {
    if (!diff_mode) return
    var opt_result = null
    var noopt_result = null
    var opt_err = null
    var noopt_err = null

    var _opt = function() {
        opt_result = opt_fn()
    } disruption {
        opt_err = "disrupted"
    }
    _opt()

    var _noopt = function() {
        noopt_result = noopt_fn()
    } disruption {
        noopt_err = "disrupted"
    }
    _noopt()

    if (opt_err != noopt_err) {
        log.console(`    DIFF ${test_name}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
        diff_mismatches = diff_mismatches + 1
    } else if (!values_equal(opt_result, noopt_result)) {
        log.console(`    DIFF ${test_name}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
        diff_mismatches = diff_mismatches + 1
    }
}

var ensure_dir = fd.ensure_dir
var get_pkg_dir = testlib.get_pkg_dir

// Collect .ce actor tests from a package
function collect_actor_tests(package_name, specific_test) {
    var prefix = get_pkg_dir(package_name)
    var tests_dir = prefix + '/tests'

    if (!fd.is_dir(tests_dir)) return []

    var files = pkg.list_files(package_name)
    var actor_tests = []
    var i = 0
    var f = null
    var test_name = null
    var match_name = null
    var test_base = null
    var match_base = null
    for (i = 0; i < length(files); i++) {
        f = files[i]
        // Check if file is in tests/ folder and is a .ce actor
        if (starts_with(f, "tests/") && ends_with(f, ".ce")) {
            // If specific test requested, filter
            if (specific_test) {
                test_name = text(f, 0, -3) // remove .ce
                match_name = specific_test
                if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
                if (!ends_with(match_name, '.ce')) match_name = match_name
                // Match without extension
                test_base = test_name
                match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
                if (test_base != match_base) continue
            }

            push(actor_tests, {
                package: package_name || "local",
                file: f,
                path: prefix + '/' + f
            })
        }
    }
    return actor_tests
}

// Spawn an actor test and track it
function spawn_actor_test(test_info) {
    var test_name = text(test_info.file, 6, -3)
    log.console(`  [ACTOR] ${test_info.file}`)

    var entry = {
        package: test_info.package,
        file: test_info.file,
        test: test_name,
        status: "running",
        start_time: time.number(),
        actor: null
    }

    var _spawn = function() {
        var actor_path = text(test_info.path, 0, -3)
        $start(function(event) {
            var end_time = time.number()
            var duration_ns = round((end_time - entry.start_time) * 1000000000)
            if (event.type == 'greet') {
                entry.actor = event.actor
                return
            }
            var idx = find(pending_actor_tests, e => e == entry)
            if (idx != null) {
                pending_actor_tests = array(
                    array(pending_actor_tests, 0, idx),
                    array(pending_actor_tests, idx + 1)
                )
            }
            entry.duration_ns = duration_ns
            if (event.type == 'stop') {
                entry.status = "passed"
                log.console(`    PASS  ${test_name}`)
            } else {
                entry.status = "failed"
                entry.error = { message: event.reason || "Actor disrupted" }
                log.console(`    FAIL ${test_name}: ${entry.error.message}`)
            }
            push(actor_test_results, entry)
            if (gc_after_each_test) dbg.gc()
            check_completion()
        }, actor_path)
        push(pending_actor_tests, entry)
    } disruption {
        entry.status = "failed"
        entry.error = { message: "Failed to spawn actor" }
        entry.duration_ns = 0
        push(actor_test_results, entry)
        log.console(`    FAIL ${test_name}: Failed to spawn`)
    }
    _spawn()
}

function run_tests(package_name, specific_test) {
    var prefix = get_pkg_dir(package_name)
    var tests_dir = prefix + '/tests'

    var pkg_result = {
        package: package_name || "local",
        files: [],
        total: 0,
        passed: 0,
        failed: 0
    }

    if (!fd.is_dir(tests_dir)) return pkg_result

    var files = pkg.list_files(package_name)
    var test_files = []
    var i = 0
    var f = null
    var test_name = null
    var match_name = null
    var match_base = null
    var mod_path = null
    var file_result = null
    for (i = 0; i < length(files); i++) {
        f = files[i]
        // Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests)
        if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
            // If specific test requested, filter
            if (specific_test) {
                test_name = text(f, 0, -3) // remove .cm
                match_name = specific_test
                if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
                // Match without extension
                match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
                if (test_name != match_base) continue
            }
            push(test_files, f)
        }
    }

    if (length(test_files) > 0) {
        if (package_name) log.console(`Running tests for ${package_name}`)
        else log.console(`Running tests for local package`)
    }

    var _load_file = null
    var load_error = false
    var err_entry = null
    for (i = 0; i < length(test_files); i++) {
        f = test_files[i]
        mod_path = text(f, 0, -3) // remove .cm
        load_error = false

        file_result = {
            name: f,
            tests: [],
            passed: 0,
            failed: 0
        }

        _load_file = function() {
            var test_mod = null
            var test_mod_noopt = null
            var use_pkg = package_name ? package_name : fd.realpath('.')
            var _load_noopt = null
            test_mod = shop.use(mod_path, use_pkg)

            // Load noopt version for diff mode
            if (diff_mode) {
                _load_noopt = function() {
                    var src_path = prefix + '/' + f
                    var src = text(fd.slurp(src_path))
                    var ast = analyze(src, src_path)
                    test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, stone({
                        use: function(path) { return shop.use(path, use_pkg) }
                    }))
                } disruption {
                    log.console(`    DIFF: failed to load noopt module for ${f}`)
                }
                _load_noopt()
            }

            var tests = []
            var j = 0
            var t = null
            var test_entry = null
            var start_time = null
            var _test_error = null
            var end_time = null
            var _run_one = null
            var all_keys = null
            var fn_count = 0
            var null_count = 0
            var other_count = 0
            var first_null_key = null
            var first_other_key = null
            if (is_function(test_mod)) {
                push(tests, {name: 'main', fn: test_mod})
            } else if (is_object(test_mod)) {
                all_keys = array(test_mod)
                log.console(`  Found ${length(all_keys)} test entries`)
                arrfor(all_keys, function(k) {
                    if (is_function(test_mod[k])) {
                        fn_count = fn_count + 1
                        push(tests, {name: k, fn: test_mod[k]})
                    } else if (is_null(test_mod[k])) {
                        null_count = null_count + 1
                        if (!first_null_key) first_null_key = k
                    } else {
                        other_count = other_count + 1
                        if (!first_other_key) first_other_key = k
                    }
                })
                log.console(`  functions=${fn_count} nulls=${null_count} other=${other_count}`)
                if (first_other_key) {
                    log.console(`  first other key: ${first_other_key}`)
                    log.console(`  is_number=${is_number(test_mod[first_other_key])} is_text=${is_text(test_mod[first_other_key])} is_logical=${is_logical(test_mod[first_other_key])} is_object=${is_object(test_mod[first_other_key])}`)
                }
            }

            if (length(tests) > 0) {
                log.console(`  ${f}`)
                for (j = 0; j < length(tests); j++) {
                    t = tests[j]
                    test_entry = {
                        package: pkg_result.package,
                        test: t.name,
                        status: "pending",
                        duration_ns: 0
                    }

                    start_time = time.number()
                    _test_error = null
                    _run_one = function() {
                        var ret = t.fn()

                        if (is_text(ret)) {
                            _test_error = ret
                            disrupt
                        } else if (ret && is_text(ret.message)) {
                            _test_error = ret.message
                            disrupt
                        }

                        test_entry.status = "passed"
                        log.console(`    PASS  ${t.name}`)
                    } disruption {
                        var e = _test_error
                        test_entry.status = "failed"
                        test_entry.error = {
                            message: e,
                            stack: (e && e.stack) ? e.stack : ""
                        }
                        if (e && e.name) test_entry.error.name = e.name

                        if (is_object(e) && e.message) {
                            test_entry.error.message = e.message
                        }

                        log.console(`    FAIL ${t.name} ${test_entry.error.message}`)
                        if (test_entry.error.stack) {
                            log.console(`      ${text(array(test_entry.error.stack, '\n'), '\n      ')}`)
                        }
                    }
                    _run_one()

                    // Differential check: compare opt vs noopt
                    if (diff_mode && test_mod_noopt && is_object(test_mod_noopt) && is_function(test_mod_noopt[t.name])) {
                        diff_check(t.name, f, t.fn, test_mod_noopt[t.name])
                    }

                    end_time = time.number()
                    test_entry.duration_ns = round((end_time - start_time) * 1000000000)

                    // Update counters at _load_file level (not inside _run_one)
                    if (test_entry.status == "passed") {
                        pkg_result.passed = pkg_result.passed + 1
                        file_result.passed = file_result.passed + 1
                    } else {
                        pkg_result.failed = pkg_result.failed + 1
                        file_result.failed = file_result.failed + 1
                    }

                    push(file_result.tests, test_entry)
                    pkg_result.total = pkg_result.total + 1
                    if (gc_after_each_test) {
                        dbg.gc()
                    }
                }
            }

        } disruption {
            load_error = true
        }
        _load_file()
        if (load_error) {
            log.console("  Error loading " + f)
            pkg_result.failed = pkg_result.failed + 1
            file_result.failed = file_result.failed + 1
            pkg_result.total = pkg_result.total + 1
        }
        push(pkg_result.files, file_result)
    }
    return pkg_result
}

var all_results = []
var all_actor_tests = []
var packages = null
var i = 0

if (all_pkgs) {
    // Run local first if we're in a valid package
    if (is_valid_package('.')) {
        push(all_results, run_tests(null, null))
        all_actor_tests = array(all_actor_tests, collect_actor_tests(null, null))
    }

    // Then all packages in lock
    packages = shop.list_packages()
    for (i = 0; i < length(packages); i++) {
        push(all_results, run_tests(packages[i], null))
        all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null))
    }
} else {
    push(all_results, run_tests(target_pkg, target_test))
    all_actor_tests = array(all_actor_tests, collect_actor_tests(target_pkg, target_test))
}

// Spawn actor tests if any
if (length(all_actor_tests) > 0) {
    log.console(`Running ${length(all_actor_tests)} actor test(s)...`)
    for (i = 0; i < length(all_actor_tests); i++) {
        spawn_actor_test(all_actor_tests[i])
    }
}

// Check for timed out actor tests
function check_timeouts() {
    var now = time.number()
    var timed_out = []
    var i = 0
    var entry = null
    var elapsed_ms = null
    var idx = null

    for (i = length(pending_actor_tests) - 1; i >= 0; i--) {
        entry = pending_actor_tests[i]
        elapsed_ms = (now - entry.start_time) * 1000
        if (elapsed_ms > ACTOR_TEST_TIMEOUT) {
            push(timed_out, i)
        }
    }

    for (i = 0; i < length(timed_out); i++) {
        idx = timed_out[i]
        entry = pending_actor_tests[idx]
        pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1))

        entry.status = "failed"
        entry.error = { message: "Test timed out" }
        entry.duration_ns = ACTOR_TEST_TIMEOUT * 1000000
        push(actor_test_results, entry)
        log.console(`    TIMEOUT ${entry.test}`)
    }

    if (length(pending_actor_tests) > 0) {
        $delay(check_timeouts, 1000)
    }
    check_completion()
}

// Check if all tests are complete and finalize
var finalized = false
function check_completion() {
    if (finalized) return
    if (length(pending_actor_tests) > 0) return

    finalized = true
    finalize_results()
}

function finalize_results() {
    var i = 0
    var j = 0
    var r = null
    var pkg_result = null
    var file_result = null

    // Add actor test results to all_results
    for (i = 0; i < length(actor_test_results); i++) {
        r = actor_test_results[i]
        pkg_result = null
        for (j = 0; j < length(all_results); j++) {
            if (all_results[j].package == r.package) {
                pkg_result = all_results[j]
                break
            }
        }
        if (!pkg_result) {
            pkg_result = { package: r.package, files: [], total: 0, passed: 0, failed: 0 }
            push(all_results, pkg_result)
        }

        file_result = null
        for (j = 0; j < length(pkg_result.files); j++) {
            if (pkg_result.files[j].name == r.file) {
                file_result = pkg_result.files[j]
                break
            }
        }
        if (!file_result) {
            file_result = { name: r.file, tests: [], passed: 0, failed: 0 }
            push(pkg_result.files, file_result)
        }

        push(file_result.tests, r)
        pkg_result.total = pkg_result.total + 1
        if (r.status == "passed") {
            pkg_result.passed = pkg_result.passed + 1
            file_result.passed = file_result.passed + 1
        } else {
            pkg_result.failed = pkg_result.failed + 1
            file_result.failed = file_result.failed + 1
        }
    }

    // Calculate totals
    var totals = { total: 0, passed: 0, failed: 0 }
    for (i = 0; i < length(all_results); i++) {
        totals.total = totals.total + all_results[i].total
        totals.passed = totals.passed + all_results[i].passed
        totals.failed = totals.failed + all_results[i].failed
    }

    log.console(`----------------------------------------`)
    log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
    if (diff_mode) {
        log.console(`Diff mismatches: ${text(diff_mismatches)}`)
    }

    generate_reports(totals)
    $stop()
}

// If no actor tests, finalize immediately
var totals = null
if (length(all_actor_tests) == 0) {
    totals = { total: 0, passed: 0, failed: 0 }
    for (i = 0; i < length(all_results); i++) {
        totals.total = totals.total + all_results[i].total
        totals.passed = totals.passed + all_results[i].passed
        totals.failed = totals.failed + all_results[i].failed
    }

    log.console(`----------------------------------------`)
    log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
    if (diff_mode) {
        log.console(`Diff mismatches: ${text(diff_mismatches)}`)
    }
} else {
    $delay(check_timeouts, 1000)
}

// Generate Reports function
function generate_reports(totals) {
    var timestamp = text(floor(time.number()))
    var report_dir = shop.get_reports_dir() + '/test_' + timestamp
    ensure_dir(report_dir)
    var i = 0
    var j = 0
    var k = 0
    var pkg_res = null
    var f = null
    var status = null
    var t = null
    var dur = null
    var pkg_tests = null
    var json_path = null

    var txt_report = `TEST REPORT
Date: ${time.text(time.number())}
Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}

=== SUMMARY ===
`
    for (i = 0; i < length(all_results); i++) {
        pkg_res = all_results[i]
        if (pkg_res.total == 0) continue
        txt_report = txt_report + `Package: ${pkg_res.package}\n`
        for (j = 0; j < length(pkg_res.files); j++) {
            f = pkg_res.files[j]
            status = f.failed == 0 ? "PASS" : "FAIL"
            txt_report = txt_report + `  [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
        }
    }

    txt_report = txt_report + `\n=== FAILURES ===\n`
    var has_failures = false
    for (i = 0; i < length(all_results); i++) {
        pkg_res = all_results[i]
        for (j = 0; j < length(pkg_res.files); j++) {
            f = pkg_res.files[j]
            for (k = 0; k < length(f.tests); k++) {
                t = f.tests[k]
                if (t.status == "failed") {
                    has_failures = true
                    txt_report = txt_report + `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
                    if (t.error) {
                        txt_report = txt_report + `  Message: ${t.error.message}\n`
                        if (t.error.stack) {
                            txt_report = txt_report + `  Stack:\n${text(array(array(t.error.stack, '\n'), l => `    ${l}`), '\n')}\n`
                        }
                    }
                    txt_report = txt_report + `\n`
                }
            }
        }
    }
    if (!has_failures) txt_report = txt_report + `None\n`

    txt_report = txt_report + `\n=== DETAILED RESULTS ===\n`
    for (i = 0; i < length(all_results); i++) {
        pkg_res = all_results[i]
        if (pkg_res.total == 0) continue

        for (j = 0; j < length(pkg_res.files); j++) {
            f = pkg_res.files[j]
            for (k = 0; k < length(f.tests); k++) {
                t = f.tests[k]
                dur = `${t.duration_ns || 0}ns`
                status = t.status == "passed" ? "PASS" : "FAIL"
                txt_report = txt_report + `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
            }
        }
    }
    ensure_dir(report_dir)
    fd.slurpwrite(`${report_dir}/test.txt`, stone(blob(txt_report)))
    log.console(`Report written to ${report_dir}/test.txt`)

    // Generate JSON per package
    for (i = 0; i < length(all_results); i++) {
        pkg_res = all_results[i]
        if (pkg_res.total == 0) continue

        pkg_tests = []
        for (j = 0; j < length(pkg_res.files); j++) {
            f = pkg_res.files[j]
            for (k = 0; k < length(f.tests); k++) {
                push(pkg_tests, f.tests[k])
            }
        }

        json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
        fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests))))
    }
}

// If no actor tests, generate reports and stop immediately
if (length(all_actor_tests) == 0) {
    generate_reports(totals)
    $stop()
}
