///|
/// Bun namespace
/// https://bun.sh/docs/api

///|
#external
pub type Bun

///|
pub fn Bun::as_any(self : Bun) -> @core.Any = "%identity"

///|
/// Bun global object
pub extern "js" fn bun() -> Bun =
  #| () => Bun

///| Runtime Information

///|
/// Get Bun version
pub fn Bun::version(self : Bun) -> String {
  self.as_any()["version"].cast()
}

///|
/// Get Bun revision
pub fn Bun::revision(self : Bun) -> String {
  self.as_any()["revision"].cast()
}

///| Environment Variables

///|
/// Get environment variable
pub fn Bun::env_get(self : Bun, key : String) -> String? {
  self.as_any()["env"][key].cast() |> @core.identity_option
}

///|
/// Get all environment variables as an object
pub fn Bun::env(self : Bun) -> @core.Any {
  self.as_any()["env"].cast()
}

///| File Operations

///|
/// Read a file as text
pub async fn Bun::file_text(self : Bun, path : String) -> String {
  let file = self.as_any()._call("file", [@core.any(path)])
  let promise : @core.Promise[String] = file._call("text", []).cast()
  promise.wait()
}

///|
/// Read a file as ArrayBuffer
pub async fn Bun::file_arrayBuffer(
  self : Bun,
  path : String,
) -> @arraybuffer.ArrayBuffer {
  let file = self.as_any()._call("file", [@core.any(path)])
  let promise : @core.Promise[@arraybuffer.ArrayBuffer] = file
    ._call("arrayBuffer", [])
    .cast()
  promise.wait()
}

///|
/// Write data to a file
pub async fn Bun::write(self : Bun, path : String, data : @core.Any) -> Int {
  let promise : @core.Promise[Int] = self
    .as_any()
    ._call("write", [@core.any(path), data])
    .cast()
  promise.wait()
}

///| Process APIs

///|
/// Get current working directory (Bun.cwd is a property, not a function)
pub fn Bun::cwd(self : Bun) -> String {
  self.as_any()["cwd"].cast()
}

///|
/// Get command line arguments
extern "js" fn ffi_get_argv() -> Array[String] =
  #| () => process.argv

///|
/// Get command line arguments (excluding the binary and script path)
pub fn Bun::argv(self : Bun) -> Array[String] {
  ignore(self)
  ffi_get_argv()
}

///|
/// Get the main module path
/// NOTE: If you define `::main`, `moon coverage analyze` will fail
/// https://github.com/moonbitlang/moonbit-docs/issues/1071
pub fn Bun::main_(self : Bun) -> String {
  self.as_any()["main"].cast()
}

///|
/// Exit the process
pub fn Bun::exit(self : Bun, code? : Int) -> Unit {
  match code {
    Some(c) => self.as_any()._call("exit", [@core.any(c)]) |> ignore
    None => self.as_any()._call("exit", []) |> ignore
  }
}

///| Test APIs

///|
#external
pub type TestContext

///|
pub fn TestContext::as_any(self : TestContext) -> @core.Any = "%identity"

///|
/// Get the test name
pub fn TestContext::name(self : TestContext) -> String {
  self.as_any()["name"].cast()
}

///|
/// test(name, fn) - Global test function with proper Promise wrapping
/// Takes an async function (CPS style with _ctx, _cont, _err_cont)
extern "js" fn ffi_test_async(
  name : String,
  f : async (TestContext) -> Unit,
) -> Unit =
  #| (name, f) => {
  #|   test(name, () => {
  #|     return new Promise((resolve, reject) => {
  #|       f(null, resolve, reject);
  #|     });
  #|   });
  #| }

///|
/// test(name, fn) - Global test function for sync tests
extern "js" fn ffi_test_sync(name : String, f : @core.Any) -> Unit =
  #| (name, f) => test(name, () => f(null))

///|
/// Bun.test(name, fn) - wrapper for convenience
pub fn[E : Show + Error] Bun::test_(
  self : Bun,
  name : String,
  f : (TestContext) -> Unit raise E,
) -> Unit {
  ignore(self)
  let wrapped : (TestContext) -> @core.Any = ctx => {
    @core.export_sync(fn() -> @core.Any raise E {
      f(ctx)
      @core.undefined()
    })
  }
  let fn_any : @core.Any = @core.from_fn1(wrapped).cast()
  ffi_test_sync(name, fn_any)
}

///|
/// Bun.test(name, async fn) - wrapper for convenience
pub fn Bun::test_async(
  self : Bun,
  name : String,
  f : async (TestContext) -> Unit,
) -> Unit {
  ignore(self)
  // async functions are compiled to CPS style with (_ctx, _cont, _err_cont) signature
  // Pass the CPS function directly to ffi_test_async
  ffi_test_async(name, f)
}

///|
/// expect() function for assertions
#external
pub type Expectation

///|
pub fn Expectation::as_any(self : Expectation) -> @core.Any = "%identity"

///|
/// Create an expectation
pub extern "js" fn expect(value : @core.Any) -> Expectation =
  #| (value) => expect(value)

///|
/// Assert that the value equals the expected value
pub fn Expectation::toBe(self : Expectation, expected : @core.Any) -> Unit {
  self.as_any()._call("toBe", [expected]) |> ignore
}

///|
/// Assert that the value equals the expected value (deep equality)
pub fn Expectation::toEqual(self : Expectation, expected : @core.Any) -> Unit {
  self.as_any()._call("toEqual", [expected]) |> ignore
}

///|
/// Assert that the value is truthy
pub fn Expectation::toBeTruthy(self : Expectation) -> Unit {
  self.as_any()._call("toBeTruthy", []) |> ignore
}

///|
/// Assert that the value is falsy
pub fn Expectation::toBeFalsy(self : Expectation) -> Unit {
  self.as_any()._call("toBeFalsy", []) |> ignore
}

///|
/// Assert that the value is null
pub fn Expectation::toBeNull(self : Expectation) -> Unit {
  self.as_any()._call("toBeNull", []) |> ignore
}

///|
/// Assert that the value is undefined
pub fn Expectation::toBeUndefined(self : Expectation) -> Unit {
  self.as_any()._call("toBeUndefined", []) |> ignore
}

///|
/// Assert that the value is defined (not undefined)
pub fn Expectation::toBeDefined(self : Expectation) -> Unit {
  self.as_any()._call("toBeDefined", []) |> ignore
}

///|
/// Assert that a string contains a substring
pub fn Expectation::toContain(self : Expectation, substring : String) -> Unit {
  self.as_any()._call("toContain", [@core.any(substring)]) |> ignore
}

///| HTTP Server APIs

///|
#external
pub type Server

///|
pub fn Server::as_any(self : Server) -> @core.Any = "%identity"

///|
/// Get the server port
pub fn Server::port(self : Server) -> Int {
  self.as_any()["port"].cast()
}

///|
/// Get the server hostname
pub fn Server::hostname(self : Server) -> String {
  self.as_any()["hostname"].cast()
}

///|
/// Stop the server
pub fn Server::stop(self : Server) -> Unit {
  self.as_any()._call("stop", []) |> ignore
}

///|
/// Create an HTTP server with a fetch handler
pub fn Bun::serve(
  self : Bun,
  port? : Int,
  hostname? : String,
  fetch : @core.Any,
) -> Server {
  let entries : Array[(String, @core.Any)] = []
  entries.push(("fetch", fetch))
  if port is Some(p) {
    entries.push(("port", @core.any(p)))
  }
  if hostname is Some(h) {
    entries.push(("hostname", @core.any(h)))
  }
  let opts = @core.from_entries(entries)
  self.as_any()._call("serve", [opts.cast()]).cast()
}

///| Hash and Crypto

///|
/// Hash a password using bcrypt
pub async fn Bun::password_hash(self : Bun, password : String) -> String {
  let promise : @core.Promise[String] = self.as_any()["password"]
    ._call("hash", [@core.any(password)])
    .cast()
  promise.wait()
}

///|
/// Verify a password against a hash
pub async fn Bun::password_verify(
  self : Bun,
  password : String,
  hash : String,
) -> Bool {
  let promise : @core.Promise[Bool] = self.as_any()["password"]
    ._call("verify", [@core.any(password), @core.any(hash)])
    .cast()
  promise.wait()
}

///| Other Utilities

///|
/// Sleep for a duration in milliseconds
pub async fn Bun::sleep(self : Bun, ms : Int) -> Unit {
  let promise : @core.Promise[Unit] = self
    .as_any()
    ._call("sleep", [@core.any(ms)])
    .cast()
  promise.wait()
}

///|
/// Get a high-resolution timestamp in nanoseconds
pub fn Bun::nanoseconds(self : Bun) -> Int {
  self.as_any()._call("nanoseconds", []).cast()
}

///|
/// Returns true if running in Bun
pub extern "js" fn is_bun() -> Bool =
  #| () => typeof Bun !== "undefined"

///| File System APIs (using Node.js fs module compatibility)

///|
/// Delete a file (uses Node.js fs.unlinkSync for compatibility)
pub extern "js" fn unlink_sync(path : String) -> Unit =
  #| (path) => {
  #|   const fs = require('fs');
  #|   fs.unlinkSync(path);
  #| }

///| Process & Command Execution APIs

///|
#external
pub type Subprocess

///|
pub fn Subprocess::as_any(self : Subprocess) -> @core.Any = "%identity"

///|
/// Get process ID
pub fn Subprocess::pid(self : Subprocess) -> Int {
  self.as_any()["pid"].cast()
}

///|
/// Get exit code (if process has exited)
pub fn Subprocess::exitCode(self : Subprocess) -> Int? {
  self.as_any()["exitCode"].cast() |> @core.identity_option
}

///|
/// Get stdin writer
pub fn Subprocess::stdin(self : Subprocess) -> @core.Any {
  self.as_any()["stdin"].cast()
}

///|
/// Get stdout reader
pub fn Subprocess::stdout(self : Subprocess) -> @core.Any {
  self.as_any()["stdout"].cast()
}

///|
/// Get stderr reader
pub fn Subprocess::stderr(self : Subprocess) -> @core.Any {
  self.as_any()["stderr"].cast()
}

///|
/// Kill the subprocess
pub fn Subprocess::kill(self : Subprocess, signal? : Int) -> Unit {
  match signal {
    Some(s) => self.as_any()._call("kill", [@core.any(s)]) |> ignore
    None => self.as_any()._call("kill", []) |> ignore
  }
}

///|
/// Wait for the process to exit
pub async fn Subprocess::exited(self : Subprocess) -> Int {
  let promise : @core.Promise[Int] = self.as_any()["exited"].cast()
  promise.wait()
}

///|
/// Spawn options for Bun.spawn()
pub struct SpawnOptions {
  cmd : Array[String]
  cwd : String?
  env : @core.Any?
  stdin : String?
  stdout : String?
  stderr : String?
}

///|
/// Create spawn options from command array
pub fn SpawnOptions::new(cmd : Array[String]) -> SpawnOptions {
  { cmd, cwd: None, env: None, stdin: None, stdout: None, stderr: None }
}

///|
/// Spawn a subprocess (Bun.spawn)
extern "js" fn ffi_spawn(
  cmd : @core.Any,
  cwd : @core.Any,
  env : @core.Any,
  stdin : @core.Any,
  stdout : @core.Any,
  stderr : @core.Any,
) -> Subprocess =
  #| (cmd, cwd, env, stdin, stdout, stderr) => {
  #|   const opts = {};
  #|   if (cwd !== undefined) opts.cwd = cwd;
  #|   if (env !== undefined) opts.env = env;
  #|   if (stdin !== undefined) opts.stdin = stdin;
  #|   if (stdout !== undefined) opts.stdout = stdout;
  #|   if (stderr !== undefined) opts.stderr = stderr;
  #|   return Bun.spawn(cmd, opts);
  #| }

///|
/// Convert string array to Any array
fn string_array_to_any(arr : Array[String]) -> @core.Any = "%identity"

///|
/// Spawn a subprocess
pub fn Bun::spawn(self : Bun, opts : SpawnOptions) -> Subprocess {
  ignore(self)
  let cmd_any = string_array_to_any(opts.cmd)
  ffi_spawn(
    cmd_any,
    match opts.cwd {
      Some(c) => @core.any(c)
      None => @global.undefined()
    },
    match opts.env {
      Some(e) => e
      None => @global.undefined()
    },
    match opts.stdin {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
    match opts.stdout {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
    match opts.stderr {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
  )
}

///|
/// Spawn and wait for result (similar to Bun.spawnSync)
extern "js" fn ffi_spawn_sync(
  cmd : @core.Any,
  cwd : @core.Any,
  env : @core.Any,
  stdin : @core.Any,
  stdout : @core.Any,
  stderr : @core.Any,
) -> @core.Any =
  #| (cmd, cwd, env, stdin, stdout, stderr) => {
  #|   const opts = {};
  #|   if (cwd !== undefined) opts.cwd = cwd;
  #|   if (env !== undefined) opts.env = env;
  #|   if (stdin !== undefined) opts.stdin = stdin;
  #|   if (stdout !== undefined) opts.stdout = stdout;
  #|   if (stderr !== undefined) opts.stderr = stderr;
  #|   return Bun.spawnSync(cmd, opts);
  #| }

///|
/// Spawn a subprocess synchronously
pub fn Bun::spawnSync(self : Bun, opts : SpawnOptions) -> @core.Any {
  ignore(self)
  let cmd_any = string_array_to_any(opts.cmd)
  ffi_spawn_sync(
    cmd_any,
    match opts.cwd {
      Some(c) => @core.any(c)
      None => @global.undefined()
    },
    match opts.env {
      Some(e) => e
      None => @global.undefined()
    },
    match opts.stdin {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
    match opts.stdout {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
    match opts.stderr {
      Some(s) => @core.any(s)
      None => @global.undefined()
    },
  )
}

///| Utility Functions

///|
/// Find executable in PATH (Bun.which)
pub fn Bun::which(self : Bun, bin : String) -> String? {
  self.as_any()._call("which", [@core.any(bin)]).cast() |> @core.identity_option
}

///|
/// Escape HTML string (Bun.escapeHTML)
pub fn Bun::escapeHTML(self : Bun, html : String) -> String {
  self.as_any()._call("escapeHTML", [@core.any(html)]).cast()
}

///|
/// Get string display width (Bun.stringWidth)
pub fn Bun::stringWidth(self : Bun, text : String) -> Int {
  self.as_any()._call("stringWidth", [@core.any(text)]).cast()
}

///|
/// Generate UUIDv7 (Bun.randomUUIDv7)
pub extern "js" fn random_uuid_v7() -> String =
  #| () => Bun.randomUUIDv7()

///|
/// Peek at a Promise state (Bun.peek)
pub fn Bun::peek(self : Bun, promise : @core.Any) -> @core.Any {
  self.as_any()._call("peek", [promise]).cast()
}

///| Hashing APIs

///|
/// Hash data with specified algorithm (Bun.hash)
/// Algorithms: "sha1", "sha256", "sha512", "md5", etc.
pub fn Bun::hash_(
  self : Bun,
  data : @core.Any,
  algorithm? : String,
) -> @core.Any {
  match algorithm {
    Some(alg) => self.as_any()._call("hash", [data, @core.any(alg)]).cast()
    None => self.as_any()._call("hash", [data]).cast()
  }
}

///|
#external
pub type CryptoHasher

///|
pub fn CryptoHasher::as_any(self : CryptoHasher) -> @core.Any = "%identity"

///|
/// Create a new CryptoHasher
extern "js" fn ffi_new_crypto_hasher(algorithm : String) -> CryptoHasher =
  #| (algorithm) => new Bun.CryptoHasher(algorithm)

///|
/// Create a new CryptoHasher with specified algorithm
pub fn CryptoHasher::new(algorithm : String) -> CryptoHasher {
  ffi_new_crypto_hasher(algorithm)
}

///|
/// Update hasher with data
pub fn CryptoHasher::update(self : CryptoHasher, data : @core.Any) -> Unit {
  self.as_any()._call("update", [data]) |> ignore
}

///|
/// Get digest as hex string
pub fn CryptoHasher::digest_hex(self : CryptoHasher) -> String {
  self.as_any()._call("digest", [@core.any("hex")]).cast()
}

///|
/// Get digest as base64 string
pub fn CryptoHasher::digest_base64(self : CryptoHasher) -> String {
  self.as_any()._call("digest", [@core.any("base64")]).cast()
}

///|
/// Get digest as ArrayBuffer
pub fn CryptoHasher::digest_buffer(
  self : CryptoHasher,
) -> @arraybuffer.ArrayBuffer {
  self.as_any()._call("digest", []).cast()
}

///| Glob API

///|
#external
pub type Glob

///|
pub fn Glob::as_any(self : Glob) -> @core.Any = "%identity"

///|
/// Create a new Glob matcher
extern "js" fn ffi_new_glob(pattern : String) -> Glob =
  #| (pattern) => new Bun.Glob(pattern)

///|
/// Create a Glob matcher with pattern
pub fn Glob::new(pattern : String) -> Glob {
  ffi_new_glob(pattern)
}

///|
/// Match a string against the glob pattern
pub fn Glob::match_(self : Glob, path : String) -> Bool {
  self.as_any()._call("match", [@core.any(path)]).cast()
}

///|
/// Scan files matching the glob pattern
pub fn Glob::scan(self : Glob, cwd? : String) -> @core.Any {
  match cwd {
    Some(dir) => {
      let opts = @core.from_entries([("cwd", @core.any(dir))])
      self.as_any()._call("scan", [opts.cast()]).cast()
    }
    None => self.as_any()._call("scan", []).cast()
  }
}