// Copyright 2018-2026 the Deno authors. MIT license.

#![allow(clippy::print_stderr, reason = "test code")]

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::future::poll_fn;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::task::Context;
use std::task::Poll;

use deno_error::JsErrorBox;
use deno_error::JsErrorClass;
use deno_ops::op2;
use futures::future::FutureExt;
use parking_lot::Mutex;
use tokio::task::LocalSet;
use url::Url;

use crate::FastString;
use crate::ImportAttributesContext;
use crate::ModuleCodeString;
use crate::ModuleSource;
use crate::ModuleSpecifier;
use crate::ModuleType;
use crate::ResolutionKind;
use crate::RuntimeOptions;
use crate::ascii_str;
// deno_ops macros generate code assuming deno_core in scope.
use crate::deno_core;
use crate::error::CoreErrorKind;
use crate::error::exception_to_err_result;
use crate::modules::CustomModuleEvaluationKind;
use crate::modules::IntoModuleName;
use crate::modules::ModuleCodeBytes;
use crate::modules::ModuleConcreteError;
use crate::modules::ModuleError;
use crate::modules::ModuleInfo;
use crate::modules::ModuleLoadOptions;
use crate::modules::ModuleRequest;
use crate::modules::ModuleSourceCode;
use crate::modules::RequestedModuleType;
use crate::modules::SourceCodeCacheInfo;
use crate::modules::loaders::ModuleLoadEventCounts;
use crate::modules::loaders::TestingModuleLoader;
use crate::modules::loaders::*;
use crate::resolve_import;
use crate::resolve_url;
use crate::runtime::JsRuntime;
use crate::runtime::JsRuntimeForSnapshot;

#[derive(Default)]
struct MockLoader {
  pub loads: Arc<Mutex<Vec<String>>>,
  pub code_cache: HashMap<String, Vec<u8>>,
  pub updated_code_cache: Arc<Mutex<HashMap<String, Vec<u8>>>>,
}

impl MockLoader {
  fn new() -> Rc<Self> {
    Default::default()
  }

  fn new_with_code_cache(code_cache: HashMap<String, Vec<u8>>) -> Rc<Self> {
    Rc::new(Self {
      code_cache,
      ..Default::default()
    })
  }
}

fn mock_source_code(url: &str) -> Option<(&'static str, &'static str)> {
  const A_SRC: &str = r#"
import { b } from "/b.js";
import { c } from "/c.js";
if (b() != 'b') throw Error();
if (c() != 'c') throw Error();
if (!import.meta.main) throw Error();
if (import.meta.url != 'file:///a.js') throw Error();
if (import.meta.WasmInstance !== undefined) throw Error();
"#;

  const B_SRC: &str = r#"
import { c } from "/c.js";
if (c() != 'c') throw Error();
export function b() { return 'b'; }
if (import.meta.main) throw Error();
if (import.meta.url != 'file:///b.js') throw Error();
"#;

  const C_SRC: &str = r#"
import { d } from "/d.js";
export function c() { return 'c'; }
if (d() != 'd') throw Error();
if (import.meta.main) throw Error();
if (import.meta.url != 'file:///c.js') throw Error();
"#;

  const D_SRC: &str = r#"
export function d() { return 'd'; }
if (import.meta.main) throw Error();
if (import.meta.url != 'file:///d.js') throw Error();
"#;

  const CIRCULAR1_SRC: &str = r#"
import "/circular2.js";
Deno.core.print("circular1");
"#;

  const CIRCULAR2_SRC: &str = r#"
import "/circular3.js";
Deno.core.print("circular2");
"#;

  const CIRCULAR3_SRC: &str = r#"
import "/circular1.js";
import "/circular2.js";
Deno.core.print("circular3");
"#;

  const REDIRECT1_SRC: &str = r#"
import "./redirect2.js";
Deno.core.print("redirect1");
"#;

  const REDIRECT2_SRC: &str = r#"
import "./redirect3.js";
Deno.core.print("redirect2");
"#;

  const REDIRECT3_SRC: &str = r#"Deno.core.print("redirect3");"#;

  const CONCURRENT_REDIRECT_SRC: &str = r#"
import "./redirect2.js";  // This import loads the module and registers the redirect alias
import "./dir/redirect2.js";  // This import aborts load only after detecting it's an already loaded redirect
import "./dir/redirect3.js";  // Tihs import is still in the queue, preventing RecursiveModuleLoad to transition to LoadState::Done
Deno.core.print("concurrent_redirect");
"#;

  const MAIN_SRC: &str = r#"
// never_ready.js never loads.
import "/never_ready.js";
// slow.js resolves after one tick.
import "/slow.js";
"#;

  const SLOW_SRC: &str = r#"
// Circular import of never_ready.js
// Does this trigger two ModuleLoader calls? It shouldn't.
import "/never_ready.js";
import "/a.js";
"#;

  const BAD_IMPORT_SRC: &str = r#"import "foo";"#;

  // (code, real_module_name)
  let spec: Vec<&str> = url.split("file://").collect();
  match spec[1] {
    "/a.js" => Some((A_SRC, "file:///a.js")),
    "/b.js" => Some((B_SRC, "file:///b.js")),
    "/c.js" => Some((C_SRC, "file:///c.js")),
    "/d.js" => Some((D_SRC, "file:///d.js")),
    "/circular1.js" => Some((CIRCULAR1_SRC, "file:///circular1.js")),
    "/circular2.js" => Some((CIRCULAR2_SRC, "file:///circular2.js")),
    "/circular3.js" => Some((CIRCULAR3_SRC, "file:///circular3.js")),
    "/redirect1.js" => Some((REDIRECT1_SRC, "file:///redirect1.js")),
    "/dir/redirect2.js" => Some((REDIRECT2_SRC, "file:///dir/redirect2.js")),
    // pretend redirect - real module name is different than one requested
    "/redirect2.js" => Some((REDIRECT2_SRC, "file:///dir/redirect2.js")),
    "/dir/redirect3.js" => Some((REDIRECT3_SRC, "file:///redirect3.js")),
    "/concurrent_redirect.js" => {
      Some((CONCURRENT_REDIRECT_SRC, "file:///concurrent_redirect.js"))
    }
    "/slow.js" => Some((SLOW_SRC, "file:///slow.js")),
    "/never_ready.js" => {
      Some(("should never be Ready", "file:///never_ready.js"))
    }
    "/main.js" => Some((MAIN_SRC, "file:///main.js")),
    "/bad_import.js" => Some((BAD_IMPORT_SRC, "file:///bad_import.js")),
    // deliberately empty code.
    "/main_with_code.js" => Some(("", "file:///main_with_code.js")),
    _ => None,
  }
}

#[derive(Debug, PartialEq)]
enum MockError {
  ResolveErr,
  LoadErr,
}

impl fmt::Display for MockError {
  fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
    unimplemented!()
  }
}

impl std::error::Error for MockError {
  fn cause(&self) -> Option<&dyn std::error::Error> {
    unimplemented!()
  }
}

impl JsErrorClass for MockError {
  fn get_class(&self) -> Cow<'static, str> {
    unimplemented!()
  }

  fn get_message(&self) -> Cow<'static, str> {
    unimplemented!()
  }

  fn get_additional_properties(&self) -> deno_error::AdditionalProperties {
    unimplemented!()
  }

  fn get_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
    self
  }
}

struct DelayedSourceCodeFuture {
  url: String,
  counter: u32,
  code_cache: Option<Vec<u8>>,
}

impl Future for DelayedSourceCodeFuture {
  type Output = Result<ModuleSource, ModuleLoaderError>;

  fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
    let inner = self.get_mut();
    inner.counter += 1;
    if inner.url == "file:///never_ready.js" {
      return Poll::Pending;
    }
    if inner.url == "file:///slow.js" && inner.counter < 2 {
      // TODO(ry) Hopefully in the future we can remove current task
      // notification.
      cx.waker().wake_by_ref();
      return Poll::Pending;
    }
    match mock_source_code(&inner.url) {
      Some(src) => {
        let hash = {
          use std::hash::Hash;
          use std::hash::Hasher;
          let mut hasher = twox_hash::XxHash64::default();
          src.1.hash(&mut hasher);
          hasher.finish()
        };
        Poll::Ready(Ok(ModuleSource::for_test_with_redirect(
          src.0,
          inner.url.as_str(),
          src.1,
          Some(SourceCodeCacheInfo {
            hash,
            data: inner
              .code_cache
              .as_ref()
              .map(|code_cache| Cow::Owned(code_cache.clone())),
          }),
        )))
      }
      None => Poll::Ready(Err(JsErrorBox::from_err(MockError::LoadErr))),
    }
  }
}

impl ModuleLoader for MockLoader {
  fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
    _kind: ResolutionKind,
  ) -> ModuleResolveResponse {
    let referrer = if referrer == "." {
      "file:///"
    } else {
      referrer
    };

    let output_specifier = match resolve_import(specifier, referrer) {
      Ok(specifier) => specifier,
      Err(..) => {
        return Err(JsErrorBox::from_err(MockError::ResolveErr));
      }
    };

    if mock_source_code(output_specifier.as_ref()).is_some() {
      Ok(output_specifier)
    } else {
      Err(JsErrorBox::from_err(MockError::ResolveErr))
    }
  }

  fn load(
    &self,
    module_specifier: &ModuleSpecifier,
    _maybe_referrer: Option<&ModuleLoadReferrer>,
    _options: ModuleLoadOptions,
  ) -> ModuleLoadResponse {
    let mut loads = self.loads.lock();
    loads.push(module_specifier.to_string());
    let url = module_specifier.to_string();
    let code_cache = self.code_cache.get(&url);
    ModuleLoadResponse::Async(
      DelayedSourceCodeFuture {
        url,
        counter: 0,
        code_cache: code_cache.cloned(),
      }
      .boxed(),
    )
  }

  fn code_cache_ready(
    &self,
    module_specifier: ModuleSpecifier,
    _hash: u64,
    code_cache: &[u8],
  ) -> Pin<Box<dyn Future<Output = ()>>> {
    let mut updated_code_cache = self.updated_code_cache.lock();
    updated_code_cache
      .insert(module_specifier.to_string(), code_cache.to_vec());
    async {}.boxed_local()
  }
}

#[test]
fn test_recursive_load() {
  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });
  let spec = resolve_url("file:///a.js").unwrap();
  let a_id_fut = runtime.load_main_es_module(&spec);
  let a_id = futures::executor::block_on(a_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(a_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();
  let l = loads.lock();
  assert_eq!(
    l.to_vec(),
    vec![
      "file:///a.js",
      "file:///b.js",
      "file:///c.js",
      "file:///d.js"
    ]
  );

  let module_map_rc = runtime.module_map();
  let modules = module_map_rc;

  assert_eq!(
    modules.get_id("file:///a.js", RequestedModuleType::None),
    Some(a_id)
  );
  let b_id = modules
    .get_id("file:///b.js", RequestedModuleType::None)
    .unwrap();
  let c_id = modules
    .get_id("file:///c.js", RequestedModuleType::None)
    .unwrap();
  let d_id = modules
    .get_id("file:///d.js", RequestedModuleType::None)
    .unwrap();
  assert_eq!(
    modules.get_requested_modules(a_id),
    Some(vec![
      ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///b.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/b.js".to_string()),
        referrer_source_offset: Some(19),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      },
      ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///c.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/c.js".to_string()),
        referrer_source_offset: Some(46),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      },
    ])
  );
  assert_eq!(
    modules.get_requested_modules(b_id),
    Some(vec![ModuleRequest {
      reference: crate::modules::ModuleReference {
        specifier: ModuleSpecifier::parse("file:///c.js").unwrap(),
        requested_module_type: RequestedModuleType::None,
      },
      specifier_key: Some("/c.js".to_string()),
      referrer_source_offset: Some(19),
      phase: crate::modules::ModuleImportPhase::Evaluation,
    },])
  );
  assert_eq!(
    modules.get_requested_modules(c_id),
    Some(vec![ModuleRequest {
      reference: crate::modules::ModuleReference {
        specifier: ModuleSpecifier::parse("file:///d.js").unwrap(),
        requested_module_type: RequestedModuleType::None,
      },
      specifier_key: Some("/d.js".to_string()),
      referrer_source_offset: Some(19),
      phase: crate::modules::ModuleImportPhase::Evaluation,
    },])
  );
  assert_eq!(modules.get_requested_modules(d_id), Some(vec![]));
}

#[test]
fn test_mods() {
  let loader = Rc::new(TestingModuleLoader::new(NoopModuleLoader));
  static DISPATCH_COUNT: AtomicUsize = AtomicUsize::new(0);

  #[op2(fast)]
  fn op_test(control: u8) -> u8 {
    DISPATCH_COUNT.fetch_add(1, Ordering::Relaxed);
    assert_eq!(control, 42);
    43
  }

  deno_core::extension!(test_ext, ops = [op_test]);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  runtime
    .execute_script(
      "setup.js",
      r#"
      function assert(cond) {
        if (!cond) {
          throw Error("assert");
        }
      }
      "#,
    )
    .unwrap();

  assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);

  let module_map = runtime.module_map().clone();

  let (mod_a, mod_b) = {
    deno_core::scope!(scope, runtime);
    let mod_a = module_map
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///a.js").into(),
        ascii_str!(
          r#"
          import { b } from './b.js'
          if (b() != 'b') throw Error();
          let control = 42;
          Deno.core.ops.op_test(control);
        "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);
    let imports = module_map.get_requested_modules(mod_a);
    assert_eq!(
      imports,
      Some(vec![ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///b.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("./b.js".to_string()),
        referrer_source_offset: Some(29),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      },])
    );

    let mod_b = module_map
      .new_es_module(
        scope,
        false,
        ascii_str!("file:///b.js").into(),
        ascii_str!("export function b() { return 'b' }").into(),
        false,
        None,
      )
      .unwrap();
    let imports = module_map.get_requested_modules(mod_b).unwrap();
    assert_eq!(imports.len(), 0);
    (mod_a, mod_b)
  };

  runtime.instantiate_module(mod_b).unwrap();
  assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);
  assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 0, 0, 0));

  runtime.instantiate_module(mod_a).unwrap();
  assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(mod_a);
  assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 1);
}

#[test]
fn test_lazy_loaded_esm() {
  deno_core::extension!(test_ext, lazy_loaded_esm = [dir "modules/testdata", "lazy_loaded.js"]);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    ..Default::default()
  });

  runtime
    .lazy_load_es_module_with_code(
      "ext:cli/more_lazy.js",
      crate::ascii_str_include!("testdata/lazy_loaded.js"),
    )
    .unwrap();

  runtime
    .execute_script(
      "setup.js",
      r#"
      Deno.core.print("1\n");
      const module = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded.js")();
      module.blah("hello\n");
      Deno.core.print(`${JSON.stringify(module)}\n`);
      const module1 = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded.js")();
      if (module !== module1) throw new Error("should return the same error");
      "#,
    )
    .unwrap();
}

#[test]
fn test_lazy_loaded_esm_rejects_non_lazy_cached_module() {
  let mut runtime = JsRuntime::new(Default::default());

  runtime
    .execute_script(
      "setup.js",
      r#"
      const load = Deno.core.createLazyLoader("ext:core/ops");
      let error;
      try {
        load();
      } catch (caught) {
        error = caught;
      }
      if (error === undefined) {
        throw new Error("a non-lazy cached module was loaded");
      }
      if (!error.message.includes("cannot be lazy-loaded")) {
        throw error;
      }
      "#,
    )
    .unwrap();
}

#[test]
fn test_lazy_loaded_esm_aliased_specifier() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_esm = [
      dir "modules/testdata",
      "custom:aliased" = "lazy_loaded_aliased.js",
    ]
  );

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    ..Default::default()
  });

  // Test via createLazyLoader
  runtime
    .execute_script(
      "setup.js",
      r#"
      const module = Deno.core.createLazyLoader("custom:aliased")();
      if (module.value !== "aliased") throw new Error("expected 'aliased', got " + module.value);
      if (module.default.value !== "aliased") throw new Error("expected default.value to be 'aliased'");
      Deno.core.print("lazy_loaded_esm aliased specifier works\n");
      "#,
    )
    .unwrap();
}

#[tokio::test]
async fn test_lazy_loaded_esm_aliased_via_import() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_esm = [
      dir "modules/testdata",
      "custom:aliased" = "lazy_loaded_aliased.js",
    ]
  );

  let loader = Rc::new(StaticModuleLoader::with(
    ModuleSpecifier::parse("file:///importer.js").unwrap(),
    crate::ascii_str_include!("testdata/lazy_loaded_importer.js"),
  ));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime
    .load_side_es_module_from_code(
      &ModuleSpecifier::parse("file:///importer.js").unwrap(),
      crate::ascii_str_include!("testdata/lazy_loaded_importer.js"),
    )
    .await
    .unwrap();
  let result = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  result.await.unwrap();
}

/// Regression test for https://github.com/denoland/deno/issues/33940
///
/// When two lazy-loaded ESM modules (A and B) are statically imported as
/// siblings, V8 instantiates both during the link phase and evaluates them
/// in DFS post-order. If A's evaluation calls `createLazyLoader(B)()`, the
/// `op_lazy_load_esm` early-return path used to hand back B's module
/// namespace without first evaluating it, so any `export const` bindings in
/// B were in the temporal dead zone — producing `Cannot access 'X' before
/// initialization`. This reproduces the gemini-cli failure where
/// `events_esm.ts` eagerly destructures `EventEmitterAsyncResource`, whose
/// getter lazy-loads `async_hooks_esm.ts` (a sibling in the user's bundle).
#[tokio::test]
async fn test_lazy_load_esm_evaluates_pre_instantiated_sibling() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_esm = [
      dir "modules/testdata",
      "custom:lazy_a" = "lazy_load_sibling_a.js",
      "custom:lazy_b" = "lazy_load_sibling_b.js",
    ]
  );

  let loader = Rc::new(StaticModuleLoader::with(
    ModuleSpecifier::parse("file:///main.js").unwrap(),
    crate::ascii_str_include!("testdata/lazy_load_sibling_main.js"),
  ));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime
    .load_main_es_module(&ModuleSpecifier::parse("file:///main.js").unwrap())
    .await
    .unwrap();
  let result = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  result.await.unwrap();
}

/// Regression test for https://github.com/denoland/deno/issues/36216
///
/// Evaluating a pre-instantiated module cached under a synthetic ESM specifier
/// can synchronously start a dynamic import. The cache-hit path must not keep
/// `ModuleMapData` borrowed while V8 runs the module body, because starting
/// the import allocates a new module load ID from the same map.
#[test]
fn test_cached_synthetic_esm_evaluation_allows_dynamic_import() {
  let mut runtime = JsRuntime::new(Default::default());
  let module_map = runtime.module_map().clone();

  deno_core::scope!(scope, runtime);
  module_map.add_synthetic_esm_module(
    ascii_str!("custom:synthetic").into(),
    ascii_str!("ext:test/backing.js").into(),
  );
  let module_id = module_map
    .new_es_module(
      scope,
      false,
      ascii_str!("custom:synthetic").into(),
      ascii_str!(r#"import("file:///dynamic_import.js").catch(() => {});"#)
        .into(),
      false,
      None,
    )
    .unwrap();
  module_map.instantiate_module(scope, module_id).unwrap();
  module_map
    .lazy_load_synthetic_esm_module(scope, "custom:synthetic")
    .unwrap();
}

/// Regression test for https://github.com/denoland/deno/issues/34307
///
/// Two concurrent dynamic `import()` calls each spawn their own
/// `RecursiveModuleLoad`. If both transitively depend on the same
/// `lazy_loaded_esm` module, their per-load `visited`/`get_id` dedup misses
/// (the first hasn't registered the module yet) and both queue load futures
/// for the same lazy specifier. The first wins `take_lazy_esm_source`; the
/// second used to get `None` and fall through to `loader.load()`, which
/// surfaces "Unsupported scheme" for `node:` builtins in the wild (vite 8
/// + react-router 7.15 under a CJS bin).
#[tokio::test]
async fn test_lazy_loaded_esm_concurrent_loads() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_esm = [
      dir "modules/testdata",
      "custom:lazy_shared" = "lazy_loaded_shared.js",
    ]
  );

  let main =
    ModuleSpecifier::parse("file:///lazy_loaded_concurrent_main.js").unwrap();
  let a =
    ModuleSpecifier::parse("file:///lazy_loaded_concurrent_a.js").unwrap();
  let b =
    ModuleSpecifier::parse("file:///lazy_loaded_concurrent_b.js").unwrap();
  let loader = Rc::new(StaticModuleLoader::new([
    (
      main.clone(),
      crate::ascii_str_include!("testdata/lazy_loaded_concurrent_main.js"),
    ),
    (
      a,
      crate::ascii_str_include!("testdata/lazy_loaded_concurrent_a.js"),
    ),
    (
      b,
      crate::ascii_str_include!("testdata/lazy_loaded_concurrent_b.js"),
    ),
  ]));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime.load_main_es_module(&main).await.unwrap();
  let result = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  result.await.unwrap();
}

#[test]
fn test_lazy_loaded_script() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_js = [dir "modules/testdata", "lazy_script.js", "lazy_script_dep.js"]
  );

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    ..Default::default()
  });

  runtime
    .execute_script(
      "test_load_ext_script.js",
      r#"
      // Load a lazy script
      const module = Deno.core.loadExtScript("ext:test_ext/lazy_script.js");
      if (module.foo !== "foo") throw new Error("expected foo");
      if (module.bar !== 123) throw new Error("expected 123");

      // Should be cached - same object returned
      const module2 = Deno.core.loadExtScript("ext:test_ext/lazy_script.js");
      if (module !== module2) throw new Error("should return the same cached object");

      // Load a script that depends on another lazy script
      const dep = Deno.core.loadExtScript("ext:test_ext/lazy_script_dep.js");
      if (dep.fromDep !== true) throw new Error("expected fromDep");
      if (dep.utils.foo !== "foo") throw new Error("expected nested foo");

      Deno.core.print("lazy script test passed\n");
      "#,
    )
    .unwrap();
}

#[test]
fn test_lazy_loaded_script_not_found() {
  deno_core::extension!(test_ext);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    ..Default::default()
  });

  let result = runtime.execute_script(
    "test_not_found.js",
    r#"Deno.core.loadExtScript("ext:test_ext/nonexistent.js")"#,
  );
  assert!(result.is_err());
  let err = result.unwrap_err();
  assert!(
    err.to_string().contains("cannot be lazy-loaded"),
    "unexpected error: {}",
    err
  );
}

#[test]
fn test_json_text_bytes_modules() {
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  runtime
    .execute_script(
      "setup.js",
      r#"
        function assert(cond) {
          if (!cond) {
            throw Error("assert");
          }
        }
        "#,
    )
    .unwrap();

  let module_map = runtime.module_map().clone();

  let (mod_b, mod_c, mod_d, mod_e) = {
    deno_core::scope!(scope, runtime);
    let specifier_b = ascii_str!("file:///b.js");

    let mod_b = module_map
      .new_es_module(
        scope,
        false,
        specifier_b.into(),
        ascii_str!(
          r#"
          import jsonData from './c.json' with {type: "json"};
          assert(jsonData.a == "b");
          assert(jsonData.c.d == 10);
          import txt from './d.txt' with {type: "text"};
          assert(txt == "hello there");
          import bytes from './e.bin' with {type: "bytes"};
          assert(bytes.length === 3);
          assert(bytes[0] === 1);
          assert(bytes[1] === 2);
          assert(bytes[2] === 3);
        "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    let imports = module_map.get_requested_modules(mod_b);
    assert_eq!(
      imports,
      Some(vec![
        ModuleRequest {
          reference: crate::modules::ModuleReference {
            specifier: ModuleSpecifier::parse("file:///c.json").unwrap(),
            requested_module_type: RequestedModuleType::Json,
          },
          specifier_key: Some("./c.json".to_string()),
          referrer_source_offset: Some(32),
          phase: crate::modules::ModuleImportPhase::Evaluation,
        },
        ModuleRequest {
          reference: crate::modules::ModuleReference {
            specifier: ModuleSpecifier::parse("file:///d.txt").unwrap(),
            requested_module_type: RequestedModuleType::Text,
          },
          specifier_key: Some("./d.txt".to_string()),
          referrer_source_offset: Some(165),
          phase: crate::modules::ModuleImportPhase::Evaluation,
        },
        ModuleRequest {
          reference: crate::modules::ModuleReference {
            specifier: ModuleSpecifier::parse("file:///e.bin").unwrap(),
            requested_module_type: RequestedModuleType::Bytes,
          },
          specifier_key: Some("./e.bin".to_string()),
          referrer_source_offset: Some(264),
          phase: crate::modules::ModuleImportPhase::Evaluation,
        },
      ])
    );

    let mod_c = module_map
      .new_json_module(
        scope,
        ascii_str!("file:///c.json"),
        ascii_str!("{\"a\": \"b\", \"c\": {\"d\": 10}}"),
      )
      .unwrap();
    assert_eq!(module_map.get_requested_modules(mod_c).unwrap().len(), 0);

    let mod_d = module_map
      .new_text_module(
        scope,
        ascii_str!("file:///d.txt"),
        ascii_str!("hello there"),
      )
      .unwrap();
    assert_eq!(module_map.get_requested_modules(mod_d).unwrap().len(), 0);

    let mod_e = module_map
      .new_bytes_module(
        scope,
        ascii_str!("file:///e.bin"),
        ModuleCodeBytes::Static(&[1, 2, 3]),
      )
      .unwrap();
    assert_eq!(module_map.get_requested_modules(mod_e).unwrap().len(), 0);

    (mod_b, mod_c, mod_d, mod_e)
  };

  runtime.instantiate_module(mod_e).unwrap();
  runtime.instantiate_module(mod_d).unwrap();
  runtime.instantiate_module(mod_c).unwrap();

  assert_eq!(loader.counts(), ModuleLoadEventCounts::new(3, 0, 0, 0));

  runtime.instantiate_module(mod_b).unwrap();

  let receiver = runtime.mod_evaluate(mod_b);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();
  futures::executor::block_on(receiver).unwrap();
}

#[test]
fn test_validate_import_attributes_default() {
  // Verify that unless `validate_import_attributes_cb` is passed all import
  // are allowed and don't have any problem executing "invalid" code.

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  let module_map_rc = runtime.module_map().clone();
  deno_core::scope!(scope, runtime);
  module_map_rc
    .new_es_module(
      scope,
      false,
      ascii_str!("file:///a.js").into(),
      ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#)
        .into(),
      false,
      None,
    )
    .unwrap();

  module_map_rc
    .new_es_module(
      scope,
      true,
      ascii_str!("file:///a.js").into(),
      ascii_str!(r#"import jsonData from './c.json' with {type: "bar"};"#)
        .into(),
      false,
      None,
    )
    .unwrap();
}

fn fs_module_loader_specifier(file_name: &str) -> ModuleSpecifier {
  let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    .join("modules")
    .join("testdata")
    .join(file_name);
  deno_path_util::url_from_file_path(&path).unwrap()
}

async fn load_fs_module(
  file_name: &str,
  requested_module_type: RequestedModuleType,
) -> Result<ModuleSource, ModuleLoaderError> {
  let specifier = fs_module_loader_specifier(file_name);
  let response = FsModuleLoader.load(
    &specifier,
    None,
    ModuleLoadOptions {
      is_dynamic_import: false,
      is_synchronous: false,
      requested_module_type,
    },
  );
  match response {
    ModuleLoadResponse::Sync(result) => result,
    ModuleLoadResponse::Async(future) => future.await,
  }
}

#[tokio::test]
async fn test_fs_module_loader_rejects_non_json_for_json_request() {
  for file_name in ["fs_module_loader.js", "fs_module_loader"] {
    let specifier = fs_module_loader_specifier(file_name);
    let error = load_fs_module(file_name, RequestedModuleType::Json)
      .await
      .unwrap_err();

    assert_eq!(error.get_class(), "TypeError");
    assert_eq!(
      error.to_string(),
      format!(
        "Expected a JSON module, but identified a JavaScript module.\n  Specifier: {specifier}"
      )
    );
  }

  let module =
    load_fs_module("fs_module_loader.json", RequestedModuleType::Json)
      .await
      .unwrap();
  assert_eq!(module.module_type, ModuleType::Json);
}

#[test]
fn test_validate_import_attributes_callback() {
  // Verify that `validate_import_attributes_cb` is called and can deny
  // attributes.

  fn validate_import_attributes(
    scope: &mut v8::PinScope,
    assertions: &HashMap<String, String>,
    _context: &ImportAttributesContext,
  ) {
    for (key, value) in assertions {
      let msg = if key != "type" {
        Some(format!("\"{key}\" attribute is not supported."))
      } else if value != "json" {
        Some(format!("\"{value}\" is not a valid module type."))
      } else {
        None
      };

      let Some(msg) = msg else {
        continue;
      };

      let message = v8::String::new(scope, &msg).unwrap();
      let exception = v8::Exception::type_error(scope, message);
      scope.throw_exception(exception);
      return;
    }
  }

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    validate_import_attributes_cb: Some(Box::new(validate_import_attributes)),
    ..Default::default()
  });

  let module_map_rc = runtime.module_map().clone();

  {
    deno_core::scope!(scope, runtime);
    let module_err = module_map_rc
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///a.js").into(),
        ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#)
          .into(),
        false,
        None,
      )
      .unwrap_err();

    let ModuleError::Exception(exc) = module_err else {
      unreachable!();
    };
    let exception = v8::Local::new(scope, exc);
    let err =
      exception_to_err_result::<()>(scope, exception, false, true).unwrap_err();
    assert_eq!(
      err.to_string(),
      "Uncaught TypeError: \"foo\" attribute is not supported."
    );
  }

  {
    deno_core::scope!(scope, runtime);
    let module_err = module_map_rc
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///a.js").into(),
        ascii_str!(r#"import jsonData from './c.json' with {type: "bar"};"#)
          .into(),
        false,
        None,
      )
      .unwrap_err();

    let ModuleError::Exception(exc) = module_err else {
      unreachable!();
    };
    let exception = v8::Local::new(scope, exc);
    let err =
      exception_to_err_result::<()>(scope, exception, false, true).unwrap_err();
    assert_eq!(
      err.to_string(),
      "Uncaught TypeError: \"bar\" is not a valid module type."
    );
  }
}

#[test]
fn test_validate_import_attributes_callback2() {
  fn validate_import_attrs(
    scope: &mut v8::PinScope,
    _attrs: &HashMap<String, String>,
    _context: &ImportAttributesContext,
  ) {
    let msg = v8::String::new(scope, "boom!").unwrap();
    let ex = v8::Exception::type_error(scope, msg);
    scope.throw_exception(ex);
  }

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    validate_import_attributes_cb: Some(Box::new(validate_import_attrs)),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  {
    deno_core::scope!(scope, runtime);
    let module_err = module_map
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///a.js").into(),
        ascii_str!(r#"import jsonData from './c.json' with {foo: "bar"};"#)
          .into(),
        false,
        None,
      )
      .unwrap_err();

    let ModuleError::Exception(exc) = module_err else {
      unreachable!();
    };
    let exception = v8::Local::new(scope, exc);
    let err =
      exception_to_err_result::<()>(scope, exception, false, true).unwrap_err();
    assert_eq!(err.to_string(), "Uncaught TypeError: boom!");
  }
}

#[tokio::test]
async fn test_validate_import_attributes_callback_dynamic_import() {
  fn validate_import_attributes(
    scope: &mut v8::PinScope,
    _attributes: &HashMap<String, String>,
    _context: &ImportAttributesContext,
  ) {
    let message = v8::String::new(scope, "boom!").unwrap();
    let exception = v8::Exception::type_error(scope, message);
    scope.throw_exception(exception);
  }

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///target.js").unwrap(),
    ascii_str!("globalThis.executed = true;"),
  )));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    validate_import_attributes_cb: Some(Box::new(validate_import_attributes)),
    ..Default::default()
  });

  runtime
    .execute_script(
      "file:///main.js",
      r#"
      globalThis.executed = false;
      globalThis.importError = undefined;
      import("./target.js", { with: { unsupported: "value" } })
        .catch((error) => globalThis.importError = error);
      "#,
    )
    .unwrap();
  runtime.run_event_loop(Default::default()).await.unwrap();

  assert_eq!(loader.counts(), ModuleLoadEventCounts::default());
  runtime
    .execute_script(
      "file:///check.js",
      r#"
      if (globalThis.executed) {
        throw new Error("rejected import was evaluated");
      }
      if (!(globalThis.importError instanceof TypeError)) {
        throw new Error("expected dynamic import to reject with TypeError");
      }
      if (globalThis.importError.message !== "boom!") {
        throw new Error(`unexpected rejection: ${globalThis.importError}`);
      }
      "#,
    )
    .unwrap();
}

#[test]
fn test_custom_module_type_default() {
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  let err = {
    deno_core::scope!(scope, runtime);
    let specifier_a = ascii_str!("file:///a.png").into();
    module_map
      .new_module(
        scope,
        true,
        false,
        ModuleSource {
          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
          module_url_found: None,
          code_cache: None,
          module_url_specified: specifier_a,
          module_type: ModuleType::Other("bytes".into()),
        },
      )
      .unwrap_err()
  };

  match err {
    ModuleError::Concrete(ModuleConcreteError::UnsupportedKind(kind)) => {
      assert_eq!(kind, "bytes");
    }
    _ => unreachable!(),
  };
}

#[test]
fn test_custom_module_type_callback_synthetic() {
  fn custom_eval_cb(
    scope: &mut v8::PinScope,
    module_type: Cow<'_, str>,
    _module_name: &FastString,
    module_code: ModuleSourceCode,
  ) -> Result<CustomModuleEvaluationKind, JsErrorBox> {
    if module_type != "bar" {
      return Err(JsErrorBox::generic(format!(
        "Can't load '{}' module",
        module_type
      )));
    }

    let buf = match module_code {
      ModuleSourceCode::Bytes(buf) => buf.to_vec(),
      ModuleSourceCode::String(str_) => str_.as_bytes().to_vec(),
    };
    let buf_len: usize = buf.len();
    let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(buf);
    let backing_store_shared = backing_store.make_shared();
    let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared);
    let uint8_array = v8::Uint8Array::new(scope, ab, 0, buf_len).unwrap();
    let value: v8::Local<v8::Value> = uint8_array.into();
    let value_global = v8::Global::new(scope, value);
    Ok(CustomModuleEvaluationKind::Synthetic(value_global))
  }

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    custom_module_evaluation_cb: Some(Box::new(custom_eval_cb)),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  let err = {
    deno_core::scope!(scope, runtime);
    let specifier_a = ascii_str!("file:///a.png").into();
    module_map
      .new_module(
        scope,
        true,
        false,
        ModuleSource {
          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
          module_url_found: None,
          code_cache: None,
          module_url_specified: specifier_a,
          module_type: ModuleType::Other("foo".into()),
        },
      )
      .unwrap_err()
  };

  match err {
    ModuleError::Core(err) => {
      assert_eq!(err.to_string(), "Can't load 'foo' module");
    }
    _ => unreachable!(),
  };

  {
    deno_core::scope!(scope, runtime);
    let specifier_a = ascii_str!("file:///b.png").into();
    module_map
      .new_module(
        scope,
        true,
        false,
        ModuleSource {
          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
          module_url_found: None,
          code_cache: None,
          module_url_specified: specifier_a,
          module_type: ModuleType::Other("bar".into()),
        },
      )
      .unwrap()
  };
}

#[test]
fn test_custom_module_type_callback_computed() {
  fn custom_eval_cb(
    scope: &mut v8::PinScope,
    module_type: Cow<'_, str>,
    module_name: &FastString,
    module_code: ModuleSourceCode,
  ) -> Result<CustomModuleEvaluationKind, JsErrorBox> {
    if module_type != "foobar" {
      return Err(JsErrorBox::generic(format!(
        "Can't load '{}' module",
        module_type
      )));
    }

    let buf = match module_code {
      ModuleSourceCode::Bytes(buf) => buf.to_vec(),
      ModuleSourceCode::String(str_) => str_.as_bytes().to_vec(),
    };
    let buf_len: usize = buf.len();
    let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(buf);
    let backing_store_shared = backing_store.make_shared();
    let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared);
    let uint8_array = v8::Uint8Array::new(scope, ab, 0, buf_len).unwrap();
    let value: v8::Local<v8::Value> = uint8_array.into();
    let value_global = v8::Global::new(scope, value);

    let computed_src = format!(
      r#"
import bytes from "{}" with {{ type: "foobar-synth" }};

export const foo = bytes;
    "#,
      module_name.as_str()
    );
    Ok(CustomModuleEvaluationKind::ComputedAndSynthetic(
      computed_src.into(),
      value_global,
      ModuleType::Other("foobar-synth".into()),
    ))
  }

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    custom_module_evaluation_cb: Some(Box::new(custom_eval_cb)),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  let mod_id = {
    deno_core::scope!(scope, runtime);
    let specifier_a = ascii_str!("file:///b.png").into();
    module_map
      .new_module(
        scope,
        true,
        false,
        ModuleSource {
          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
          module_url_found: None,
          code_cache: None,
          module_url_specified: specifier_a,
          module_type: ModuleType::Other("foobar".into()),
        },
      )
      .unwrap()
  };

  let data = module_map.get_data();
  let data = data.borrow();
  let info = data.info.get(mod_id).unwrap();
  assert_eq!(
    info,
    &ModuleInfo {
      id: mod_id,
      main: true,
      name: "file:///b.png".into_module_name(),
      requests: vec![ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///b.png").unwrap(),
          requested_module_type: RequestedModuleType::Other(
            "foobar-synth".into()
          ),
        },
        specifier_key: Some("file:///b.png".to_string()),
        referrer_source_offset: Some(19),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      }],
      module_type: ModuleType::Other("foobar".into()),
    }
  );
  let info = data.info.get(mod_id - 1).unwrap();
  assert_eq!(
    info,
    &ModuleInfo {
      id: mod_id - 1,
      main: false,
      name: "file:///b.png".into_module_name(),
      requests: vec![],
      module_type: ModuleType::Other("foobar-synth".into()),
    }
  );
}

#[tokio::test]
async fn dyn_import_err() {
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  // Test an erroneous dynamic import where the specified module isn't found.
  poll_fn(move |cx| {
    runtime
      .execute_script(
        "file:///dyn_import2.js",
        r#"
      (async () => {
        await import("/foo.js");
      })();
      "#,
      )
      .unwrap();

    // We should get an error here.
    let result = runtime.poll_event_loop(cx, Default::default());
    assert!(matches!(result, Poll::Ready(Err(_))));
    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 1, 1, 1));
    Poll::Ready(())
  })
  .await;
}

#[tokio::test]
async fn dyn_import_recurse_err() {
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::default()));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  runtime
    .execute_script(
      "file:///dyn_import2.js",
      r#"
function bar(v) {
  bar(import(0));
}
bar("foo");
      "#,
    )
    .expect_err("should throw range error");
}

#[tokio::test]
async fn dyn_import_ok() {
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///b.js").unwrap(),
    ascii_str!("export function b() { return 'b' }"),
  )));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });
  poll_fn(move |cx| {
    // Dynamically import mod_b
    runtime
      .execute_script(
        "file:///dyn_import3.js",
        r#"
        (async () => {
          let mod = await import("./b.js");
          if (mod.b() !== 'b') {
            throw Error("bad1");
          }
          // And again!
          mod = await import("./b.js");
          if (mod.b() !== 'b') {
            throw Error("bad2");
          }
        })();
        "#,
      )
      .unwrap();

    assert!(matches!(
      runtime.poll_event_loop(cx, Default::default()),
      Poll::Ready(Ok(_))
    ));
    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(2, 1, 1, 1));
    assert!(matches!(
      runtime.poll_event_loop(cx, Default::default()),
      Poll::Ready(Ok(_))
    ));
    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(2, 1, 1, 1));
    Poll::Ready(())
  })
  .await;
}

#[tokio::test]
async fn dyn_import_borrow_mut_error() {
  // https://github.com/denoland/deno/issues/6054
  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///b.js").unwrap(),
    ascii_str!("export function b() { return 'b' }"),
  )));
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  poll_fn(move |cx| {
    runtime
      .execute_script(
        "file:///dyn_import3.js",
        r#"
        (async () => {
          let mod = await import("./b.js");
          if (mod.b() !== 'b') {
            throw Error("bad");
          }
        })();
        "#,
      )
      .unwrap();
    // TODO(mmastrac): These checks here are pretty broken and make a lot of assumptions
    // about the ordering of resolution and events. If these break, we can remove them.

    // Old comments that are likely wrong:
    // First poll runs `prepare_load` hook.
    let _ = runtime.poll_event_loop(cx, Default::default());
    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 1, 1, 1));
    // Second poll triggers error
    let _ = runtime.poll_event_loop(cx, Default::default());
    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 1, 1, 1));
    Poll::Ready(())
  })
  .await;
}

#[test]
fn test_circular_load() {
  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let fut = async move {
    let spec = resolve_url("file:///circular1.js").unwrap();
    let result = runtime.load_main_es_module(&spec).await;
    assert!(result.is_ok());
    let circular1_id = result.unwrap();
    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(circular1_id);
    runtime.run_event_loop(Default::default()).await.unwrap();

    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///circular1.js",
        "file:///circular2.js",
        "file:///circular3.js"
      ]
    );

    let module_map_rc = runtime.module_map();
    let modules = module_map_rc;

    assert_eq!(
      modules.get_id("file:///circular1.js", RequestedModuleType::None),
      Some(circular1_id)
    );
    let circular2_id = modules
      .get_id("file:///circular2.js", RequestedModuleType::None)
      .unwrap();

    assert_eq!(
      modules.get_requested_modules(circular1_id),
      Some(vec![ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///circular2.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/circular2.js".to_string()),
        referrer_source_offset: Some(8),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      }])
    );

    assert_eq!(
      modules.get_requested_modules(circular2_id),
      Some(vec![ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///circular3.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/circular3.js".to_string()),
        referrer_source_offset: Some(8),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      }])
    );

    assert!(
      modules
        .get_id("file:///circular3.js", RequestedModuleType::None)
        .is_some()
    );
    let circular3_id = modules
      .get_id("file:///circular3.js", RequestedModuleType::None)
      .unwrap();
    assert_eq!(
      modules.get_requested_modules(circular3_id),
      Some(vec![
        ModuleRequest {
          reference: crate::modules::ModuleReference {
            specifier: ModuleSpecifier::parse("file:///circular1.js").unwrap(),
            requested_module_type: RequestedModuleType::None,
          },
          specifier_key: Some("/circular1.js".to_string()),
          referrer_source_offset: Some(8),
          phase: crate::modules::ModuleImportPhase::Evaluation,
        },
        ModuleRequest {
          reference: crate::modules::ModuleReference {
            specifier: ModuleSpecifier::parse("file:///circular2.js").unwrap(),
            requested_module_type: RequestedModuleType::None,
          },
          specifier_key: Some("/circular2.js".to_string()),
          referrer_source_offset: Some(32),
          phase: crate::modules::ModuleImportPhase::Evaluation,
        }
      ])
    );
  }
  .boxed_local();

  futures::executor::block_on(fut);
}

#[test]
fn test_redirect_load() {
  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let fut = async move {
    let spec = resolve_url("file:///redirect1.js").unwrap();
    let result = runtime.load_main_es_module(&spec).await;
    assert!(result.is_ok());
    let redirect1_id = result.unwrap();
    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(redirect1_id);
    runtime.run_event_loop(Default::default()).await.unwrap();
    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///redirect1.js",
        "file:///redirect2.js",
        "file:///dir/redirect3.js"
      ]
    );

    let module_map_rc = runtime.module_map();
    let modules = module_map_rc;

    assert_eq!(
      modules.get_id("file:///redirect1.js", RequestedModuleType::None),
      Some(redirect1_id)
    );

    let redirect2_id = modules
      .get_id("file:///dir/redirect2.js", RequestedModuleType::None)
      .unwrap();
    assert!(
      modules.is_alias("file:///redirect2.js", RequestedModuleType::None)
    );
    assert!(
      !modules.is_alias("file:///dir/redirect2.js", RequestedModuleType::None)
    );
    assert_eq!(
      modules.get_id("file:///redirect2.js", RequestedModuleType::None),
      Some(redirect2_id)
    );

    let redirect3_id = modules
      .get_id("file:///redirect3.js", RequestedModuleType::None)
      .unwrap();
    assert!(
      modules.is_alias("file:///dir/redirect3.js", RequestedModuleType::None)
    );
    assert!(
      !modules.is_alias("file:///redirect3.js", RequestedModuleType::None)
    );
    assert_eq!(
      modules.get_id("file:///dir/redirect3.js", RequestedModuleType::None),
      Some(redirect3_id)
    );
  }
  .boxed_local();

  futures::executor::block_on(fut);
}

#[test]
fn test_concurrent_redirect_load() {
  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let fut = async move {
    let spec = resolve_url("file:///concurrent_redirect.js").unwrap();
    let result = runtime.load_main_es_module(&spec).await;
    assert!(result.is_ok());
    let concurrent_redirect = result.unwrap();
    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(concurrent_redirect);
    runtime.run_event_loop(Default::default()).await.unwrap();
    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///concurrent_redirect.js",
        "file:///redirect2.js",
        "file:///dir/redirect3.js"
      ]
    );

    let module_map_rc = runtime.module_map();
    let modules = module_map_rc;

    assert_eq!(
      modules.get_id("file:///redirect2.js", RequestedModuleType::None),
      modules.get_id("file:///dir/redirect2.js", RequestedModuleType::None)
    );
    assert!(
      modules.is_alias("file:///redirect2.js", RequestedModuleType::None)
    );
    assert!(
      !modules.is_alias("file:///dir/redirect2.js", RequestedModuleType::None)
    );
  }
  .boxed_local();

  futures::executor::block_on(fut);
}

#[tokio::test]
async fn slow_never_ready_modules() {
  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  poll_fn(move |cx| {
    let spec = resolve_url("file:///main.js").unwrap();
    let mut recursive_load = runtime.load_main_es_module(&spec).boxed_local();

    let result = recursive_load.poll_unpin(cx);
    assert!(result.is_pending());

    // TODO(ry) Arguably the first time we poll only the following modules
    // should be loaded:
    //      "file:///main.js",
    //      "file:///never_ready.js",
    //      "file:///slow.js"
    // But due to current task notification in DelayedSourceCodeFuture they
    // all get loaded in a single poll.

    for _ in 0..10 {
      let result = recursive_load.poll_unpin(cx);
      assert!(result.is_pending());
      let l = loads.lock();
      assert_eq!(
        l.to_vec(),
        vec![
          "file:///main.js",
          "file:///never_ready.js",
          "file:///slow.js",
          "file:///a.js",
          "file:///b.js",
          "file:///c.js",
          "file:///d.js"
        ]
      );
    }
    Poll::Ready(())
  })
  .await;
}

#[tokio::test]
async fn loader_disappears_after_error() {
  let loader = MockLoader::new();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let spec = resolve_url("file:///bad_import.js").unwrap();
  let result = runtime.load_main_es_module(&spec).await;

  let CoreErrorKind::JsBox(err) = result.unwrap_err().into_kind() else {
    unreachable!();
  };
  assert_eq!(
    err.get_ref().downcast_ref::<MockError>().unwrap(),
    &MockError::ResolveErr
  );
}

#[test]
fn recursive_load_main_with_code() {
  const MAIN_WITH_CODE_SRC: &str = r#"
    import { b } from "/b.js";
    import { c } from "/c.js";
    if (b() != 'b') throw Error();
    if (c() != 'c') throw Error();
    if (!import.meta.main) throw Error();
    if (import.meta.url != 'file:///main_with_code.js') throw Error();
  "#;

  let loader = MockLoader::new();
  let loads = loader.loads.clone();
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });
  // In default resolution code should be empty.
  // Instead we explicitly pass in our own code.
  // The behavior should be very similar to /a.js.
  let spec = resolve_url("file:///main_with_code.js").unwrap();
  let main_id_fut = runtime
    .load_main_es_module_from_code(&spec, MAIN_WITH_CODE_SRC)
    .boxed_local();
  let main_id = futures::executor::block_on(main_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(main_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();

  let l = loads.lock();
  assert_eq!(
    l.to_vec(),
    vec!["file:///b.js", "file:///c.js", "file:///d.js"]
  );

  let module_map_rc = runtime.module_map();
  let modules = module_map_rc;

  assert_eq!(
    modules.get_id("file:///main_with_code.js", RequestedModuleType::None),
    Some(main_id)
  );
  let b_id = modules
    .get_id("file:///b.js", RequestedModuleType::None)
    .unwrap();
  let c_id = modules
    .get_id("file:///c.js", RequestedModuleType::None)
    .unwrap();
  let d_id = modules
    .get_id("file:///d.js", RequestedModuleType::None)
    .unwrap();

  assert_eq!(
    modules.get_requested_modules(main_id),
    Some(vec![
      ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///b.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/b.js".to_string()),
        referrer_source_offset: Some(23),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      },
      ModuleRequest {
        reference: crate::modules::ModuleReference {
          specifier: ModuleSpecifier::parse("file:///c.js").unwrap(),
          requested_module_type: RequestedModuleType::None,
        },
        specifier_key: Some("/c.js".to_string()),
        referrer_source_offset: Some(54),
        phase: crate::modules::ModuleImportPhase::Evaluation,
      }
    ])
  );
  assert_eq!(
    modules.get_requested_modules(b_id),
    Some(vec![ModuleRequest {
      reference: crate::modules::ModuleReference {
        specifier: ModuleSpecifier::parse("file:///c.js").unwrap(),
        requested_module_type: RequestedModuleType::None,
      },
      specifier_key: Some("/c.js".to_string()),
      referrer_source_offset: Some(19),
      phase: crate::modules::ModuleImportPhase::Evaluation,
    }])
  );
  assert_eq!(
    modules.get_requested_modules(c_id),
    Some(vec![ModuleRequest {
      reference: crate::modules::ModuleReference {
        specifier: ModuleSpecifier::parse("file:///d.js").unwrap(),
        requested_module_type: RequestedModuleType::None,
      },
      specifier_key: Some("/d.js".to_string()),
      referrer_source_offset: Some(19),
      phase: crate::modules::ModuleImportPhase::Evaluation,
    }])
  );
  assert_eq!(modules.get_requested_modules(d_id), Some(vec![]));
}

#[test]
fn main_and_side_module() {
  let main_specifier = resolve_url("file:///main_module.js").unwrap();
  let side_specifier = resolve_url("file:///side_module.js").unwrap();

  let loader = StaticModuleLoader::new([
    (
      main_specifier.clone(),
      ascii_str!("if (!import.meta.main) throw Error();"),
    ),
    (
      side_specifier.clone(),
      ascii_str!("if (import.meta.main) throw Error();"),
    ),
  ]);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(Rc::new(loader)),
    ..Default::default()
  });

  let main_id_fut = runtime.load_main_es_module(&main_specifier).boxed_local();
  let main_id = futures::executor::block_on(main_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(main_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();

  // Try to add another main module - it should error.
  let side_id_fut = runtime.load_main_es_module(&side_specifier).boxed_local();
  futures::executor::block_on(side_id_fut).unwrap_err();

  // And now try to load it as a side module
  let side_id_fut = runtime.load_side_es_module(&side_specifier).boxed_local();
  let side_id = futures::executor::block_on(side_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(side_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();
}

#[test]
fn dynamic_imports_snapshot() {
  //TODO: Once the issue with the ModuleNamespaceEntryGetter is fixed, we can maintain a reference to the module
  // and use it when loading the snapshot
  let snapshot = {
    const MAIN_WITH_CODE_SRC: &str = r#"
      await import("./b.js");
    "#;

    let loader = MockLoader::new();
    let mut runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
      module_loader: Some(loader),
      ..Default::default()
    });
    // In default resolution code should be empty.
    // Instead we explicitly pass in our own code.
    // The behavior should be very similar to /a.js.
    let spec = resolve_url("file:///main_with_code.js").unwrap();
    let main_id_fut = runtime
      .load_main_es_module_from_code(&spec, MAIN_WITH_CODE_SRC)
      .boxed_local();
    let main_id = futures::executor::block_on(main_id_fut).unwrap();

    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(main_id);
    futures::executor::block_on(runtime.run_event_loop(Default::default()))
      .unwrap();
    runtime.snapshot()
  };

  let snapshot = Box::leak(snapshot);
  let mut runtime2 = JsRuntime::new(RuntimeOptions {
    startup_snapshot: Some(snapshot),
    ..Default::default()
  });

  //Evaluate the snapshot with an empty function
  runtime2.execute_script("check.js", "true").unwrap();
}

#[test]
fn import_meta_snapshot() {
  let snapshot = {
    const MAIN_WITH_CODE_SRC: &str = r#"
      if (import.meta.url != 'file:///main_with_code.js') throw Error();
      globalThis.meta = import.meta;
      globalThis.url = import.meta.url;
    "#;

    let loader = MockLoader::new();
    let mut runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
      module_loader: Some(loader),
      ..Default::default()
    });
    // In default resolution code should be empty.
    // Instead we explicitly pass in our own code.
    // The behavior should be very similar to /a.js.
    let spec = resolve_url("file:///main_with_code.js").unwrap();
    let main_id_fut = runtime
      .load_main_es_module_from_code(&spec, MAIN_WITH_CODE_SRC)
      .boxed_local();
    let main_id = futures::executor::block_on(main_id_fut).unwrap();

    #[allow(clippy::let_underscore_future, reason = "test code")]
    let eval_fut = runtime.mod_evaluate(main_id);
    futures::executor::block_on(runtime.run_event_loop(Default::default()))
      .unwrap();
    futures::executor::block_on(eval_fut).unwrap();
    runtime.snapshot()
  };

  let snapshot = Box::leak(snapshot);
  let mut runtime2 = JsRuntime::new(RuntimeOptions {
    startup_snapshot: Some(snapshot),
    ..Default::default()
  });

  runtime2
    .execute_script(
      "check.js",
      "if (globalThis.url !== 'file:///main_with_code.js') throw Error('x')",
    )
    .unwrap();
}

#[tokio::test]
async fn no_duplicate_loads() {
  // Both of these imports will cause redirects - ie. their "found" specifier
  // will be different than requested specifier.
  const MAIN_SRC: &str = r#"
  import "https://example.com/foo.js";
  import "https://example.com/bar.js";
  "#;

  // foo.js is importing bar.js that is already redirected
  const FOO_SRC: &str = r#"
  import "https://example.com/v1/bar.js";
  "#;

  struct ConsumingLoader {
    pub files: Rc<RefCell<HashMap<String, Option<String>>>>,
  }

  impl Default for ConsumingLoader {
    fn default() -> Self {
      let files = HashMap::from_iter([
        (
          "https://example.com/v1/foo.js".to_string(),
          Some(FOO_SRC.to_string()),
        ),
        (
          "https://example.com/v1/bar.js".to_string(),
          Some("".to_string()),
        ),
        ("file:///main.js".to_string(), Some(MAIN_SRC.to_string())),
      ]);
      Self {
        files: Rc::new(RefCell::new(files)),
      }
    }
  }

  impl ModuleLoader for ConsumingLoader {
    fn resolve(
      &self,
      specifier: &str,
      referrer: &str,
      _kind: ResolutionKind,
    ) -> ModuleResolveResponse {
      let referrer = if referrer == "." {
        "file:///"
      } else {
        referrer
      };

      resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
    }

    fn load(
      &self,
      module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      let found_specifier =
        if module_specifier.as_str() == "https://example.com/foo.js" {
          Some("https://example.com/v1/foo.js".to_string())
        } else if module_specifier.as_str() == "https://example.com/bar.js" {
          Some("https://example.com/v1/bar.js".to_string())
        } else {
          None
        };

      let mut files = self.files.borrow_mut();
      eprintln!(
        "getting specifier {} {:?}",
        module_specifier.as_str(),
        found_specifier
      );
      let source_code = if let Some(found) = &found_specifier {
        files.get_mut(found).unwrap().take().unwrap()
      } else {
        files
          .get_mut(module_specifier.as_str())
          .unwrap()
          .take()
          .unwrap()
      };
      let module_source = ModuleSource {
        code: ModuleSourceCode::String(ModuleCodeString::from(source_code)),
        module_type: ModuleType::JavaScript,
        code_cache: None,
        module_url_specified: module_specifier.clone().into(),
        module_url_found: found_specifier.map(|s| s.into()),
      };
      ModuleLoadResponse::Sync(Ok(module_source))
    }
  }

  let loader = Rc::new(ConsumingLoader::default());
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let spec = resolve_url("file:///main.js").unwrap();
  let a_id = runtime.load_main_es_module(&spec).await.unwrap();
  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(a_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
}

#[tokio::test]
async fn import_meta_resolve() {
  struct Loader;

  impl ModuleLoader for Loader {
    fn resolve(
      &self,
      specifier: &str,
      referrer: &str,
      _kind: ResolutionKind,
    ) -> ModuleResolveResponse {
      let referrer = if referrer == "." {
        "file:///"
      } else {
        referrer
      };
      resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
    }

    fn import_meta_resolve(
      &self,
      specifier: &str,
      _referrer: &str,
    ) -> Result<ModuleSpecifier, ModuleLoaderError> {
      if specifier == "foo" {
        return Ok(ModuleSpecifier::parse("foo:bar").unwrap());
      }

      if specifier == "./mod.js" {
        return Ok(ModuleSpecifier::parse("file:///mod.js").unwrap());
      }

      Err(JsErrorBox::generic("unexpected"))
    }

    fn load(
      &self,
      _module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      unreachable!();
    }
  }

  let loader = Rc::new(Loader);
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let spec = ModuleSpecifier::parse("file:///test.js").unwrap();
  let source = r#"
    if (import.meta.resolve("foo") !== "foo:bar") throw new Error("a");
    if (import.meta.resolve("./mod.js") !== "file:///mod.js") throw new Error("b");
    let caught = false;
    try {
      import.meta.resolve("boom!");
    } catch (e) {
      if (!(e instanceof Error)) throw new Error("c");
      caught = true;
    }
    if (!caught) throw new Error("d");
  "#;

  let a_id = runtime
    .load_main_es_module_from_code(&spec, source)
    .await
    .unwrap();
  let local = LocalSet::new();
  let a = local.spawn_local(runtime.mod_evaluate(a_id));
  let b = local.spawn_local(async move {
    runtime.run_event_loop(Default::default()).await
  });
  local.await;

  a.await.unwrap().unwrap();
  b.await.unwrap().unwrap();
}

#[test]
fn builtin_core_module() {
  let main_specifier = resolve_url("ext:///main_module.js").unwrap();

  let source_code = r#"
    import { core, primordials, internals } from "ext:core/mod.js";
    if (typeof core === "undefined") throw new Error("core missing");
    if (typeof primordials === "undefined") throw new Error("core missing");
    if (typeof internals === "undefined") throw new Error("core missing");
  "#;
  let loader = StaticModuleLoader::new([(main_specifier.clone(), source_code)]);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(Rc::new(loader)),
    ..Default::default()
  });
  runtime.module_map().set_loading_internal_modules(true);

  let main_id_fut = runtime.load_main_es_module(&main_specifier).boxed_local();
  let main_id = futures::executor::block_on(main_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(main_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();
  runtime.module_map().set_loading_internal_modules(false);
}

// An internal module that is lazily loaded at runtime gets its imports
// resolved by the embedder's loader, which in Deno applies the user's import
// map. Mapping `ext:core/mod.js` used to rewrite internal imports too and
// break instantiation. Regression test for
// https://github.com/denoland/deno/issues/36302.
#[test]
fn lazy_loaded_internal_module_ignores_loader_remapping() {
  /// Stands in for an import map that maps every specifier.
  struct RemappingLoader;

  impl ModuleLoader for RemappingLoader {
    fn resolve(
      &self,
      _specifier: &str,
      _referrer: &str,
      _kind: ResolutionKind,
    ) -> ModuleResolveResponse {
      Ok(ModuleSpecifier::parse("file:///remapped.js").unwrap())
    }

    fn load(
      &self,
      _module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      unreachable!();
    }
  }

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(Rc::new(RemappingLoader)),
    ..Default::default()
  });

  runtime
    .lazy_load_es_module_with_code(
      "ext:test/lazy.js",
      r#"
      import { core } from "ext:core/mod.js";
      if (typeof core === "undefined") throw new Error("core missing");
    "#,
    )
    .unwrap();
}

#[test]
fn import_meta_filename_dirname() {
  #[cfg(not(target_os = "windows"))]
  let main_specifier = resolve_url("file:///main_module.js").unwrap();
  #[cfg(not(target_os = "windows"))]
  let code = r#"if (import.meta.filename != '/main_module.js') throw Error();
    if (import.meta.dirname != '/') throw Error();
  "#;

  #[cfg(target_os = "windows")]
  let main_specifier = resolve_url("file:///C:/main_module.js").unwrap();
  #[cfg(target_os = "windows")]
  let code = r#"if (import.meta.filename != 'C:\\main_module.js') throw Error();
    if (import.meta.dirname != 'C:\\') throw Error();
  "#;

  let loader = StaticModuleLoader::new([(main_specifier.clone(), code)]);

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(Rc::new(loader)),
    ..Default::default()
  });

  let main_id_fut = runtime.load_main_es_module(&main_specifier).boxed_local();
  let main_id = futures::executor::block_on(main_id_fut).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(main_id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();
}

#[test]
fn test_load_with_code_cache() {
  let mut code_cache = {
    let loader = MockLoader::new();
    let loads = loader.loads.clone();
    let updated_code_cache = loader.updated_code_cache.clone();
    let mut runtime = JsRuntime::new(RuntimeOptions {
      module_loader: Some(loader),
      ..Default::default()
    });
    let spec = resolve_url("file:///a.js").unwrap();
    let a_id_fut = runtime.load_main_es_module(&spec);
    let a_id = futures::executor::block_on(a_id_fut).unwrap();

    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(a_id);
    futures::executor::block_on(runtime.run_event_loop(Default::default()))
      .unwrap();
    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///a.js",
        "file:///b.js",
        "file:///c.js",
        "file:///d.js"
      ]
    );

    let c = updated_code_cache.lock();
    let mut keys = c.keys().collect::<Vec<_>>();
    keys.sort();
    assert_eq!(
      keys,
      vec![
        "file:///a.js",
        "file:///b.js",
        "file:///c.js",
        "file:///d.js"
      ]
    );
    c.clone()
  };

  {
    // Create another runtime and try to use the code cache.
    let loader = MockLoader::new_with_code_cache(code_cache.clone());
    let loads = loader.loads.clone();
    let updated_code_cache = loader.updated_code_cache.clone();
    let mut runtime = JsRuntime::new(RuntimeOptions {
      module_loader: Some(loader),
      ..Default::default()
    });
    let spec = resolve_url("file:///a.js").unwrap();
    let a_id_fut = runtime.load_main_es_module(&spec);
    let a_id = futures::executor::block_on(a_id_fut).unwrap();

    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(a_id);
    futures::executor::block_on(runtime.run_event_loop(Default::default()))
      .unwrap();
    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///a.js",
        "file:///b.js",
        "file:///c.js",
        "file:///d.js"
      ]
    );

    // Verify that code cache was not updated, which means that provided code cache was used.
    let c = updated_code_cache.lock();
    assert!(c.is_empty());
  }

  {
    // Create another runtime, and only use code cache for c.js and d.js.
    code_cache.remove("file:///a.js");
    code_cache.remove("file:///b.js");

    let loader = MockLoader::new_with_code_cache(code_cache);
    let loads = loader.loads.clone();
    let updated_code_cache = loader.updated_code_cache.clone();
    let mut runtime = JsRuntime::new(RuntimeOptions {
      module_loader: Some(loader),
      ..Default::default()
    });
    let spec = resolve_url("file:///a.js").unwrap();
    let a_id_fut = runtime.load_main_es_module(&spec);
    let a_id = futures::executor::block_on(a_id_fut).unwrap();

    #[allow(clippy::let_underscore_future, reason = "test code")]
    let _ = runtime.mod_evaluate(a_id);
    futures::executor::block_on(runtime.run_event_loop(Default::default()))
      .unwrap();
    let l = loads.lock();
    assert_eq!(
      l.to_vec(),
      vec![
        "file:///a.js",
        "file:///b.js",
        "file:///c.js",
        "file:///d.js"
      ]
    );

    // Verify that we only used code cache for c.js and d.js, and created new cache for a.js and b.js.
    let c = updated_code_cache.lock();
    let mut keys = c.keys().collect::<Vec<_>>();
    keys.sort();
    assert_eq!(keys, vec!["file:///a.js", "file:///b.js",]);
  }
}

// Regression test for https://github.com/denoland/deno/issues/31766.
//
// JSON modules do not produce a V8 code cache, so even when the loader
// provides `SourceCodeCacheInfo` for one, `code_cache_ready` must never be
// invoked for it. The `deno compile` runtime relies on this: it counts every
// code cache request and only serializes its cache once a matching number of
// `code_cache_ready` callbacks fire, so a JSON (or Wasm) module that requested
// a cache but never produced one would otherwise stall serialization forever.
#[test]
fn test_code_cache_not_created_for_json_module() {
  struct JsonCodeCacheLoader {
    code_cache_ready_calls: Arc<Mutex<Vec<String>>>,
  }

  impl ModuleLoader for JsonCodeCacheLoader {
    fn resolve(
      &self,
      specifier: &str,
      referrer: &str,
      _kind: ResolutionKind,
    ) -> ModuleResolveResponse {
      Ok(resolve_import(specifier, referrer).unwrap())
    }

    fn load(
      &self,
      module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      let (module_type, code) = if module_specifier.as_str().ends_with(".json")
      {
        (ModuleType::Json, FastString::from_static("{}"))
      } else {
        (
          ModuleType::JavaScript,
          FastString::from_static(
            "import \"./data.json\" with { type: \"json\" };",
          ),
        )
      };
      // Provide code cache info (with no cached data, forcing creation) for
      // *every* module, including the JSON one.
      ModuleLoadResponse::Sync(Ok(ModuleSource::new(
        module_type,
        ModuleSourceCode::String(code),
        module_specifier,
        Some(SourceCodeCacheInfo {
          hash: 0,
          data: None,
        }),
      )))
    }

    fn code_cache_ready(
      &self,
      module_specifier: ModuleSpecifier,
      _hash: u64,
      _code_cache: &[u8],
    ) -> Pin<Box<dyn Future<Output = ()>>> {
      self
        .code_cache_ready_calls
        .lock()
        .push(module_specifier.to_string());
      async {}.boxed_local()
    }
  }

  let code_cache_ready_calls = Arc::new(Mutex::new(Vec::new()));
  let loader = Rc::new(JsonCodeCacheLoader {
    code_cache_ready_calls: code_cache_ready_calls.clone(),
  });
  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });
  let spec = resolve_url("file:///main.js").unwrap();
  let id =
    futures::executor::block_on(runtime.load_main_es_module(&spec)).unwrap();

  #[allow(clippy::let_underscore_future, reason = "test code")]
  let _ = runtime.mod_evaluate(id);
  futures::executor::block_on(runtime.run_event_loop(Default::default()))
    .unwrap();

  // `code_cache_ready` must fire for the JS module, but not for the JSON one.
  let calls = code_cache_ready_calls.lock();
  assert_eq!(calls.to_vec(), vec!["file:///main.js".to_string()]);
}

#[test]
fn ext_module_loader_relative() {
  let loader = ExtModuleLoader::new(vec![], None);
  let cases = [
    (
      ("../../foo.js", "ext:test/nested/mod/bar.js"),
      "ext:test/foo.js",
    ),
    (("./foo.js", "ext:test/bar.js"), "ext:test/foo.js"),
    ((".././foo.js", "ext:test/bar.js"), "ext:foo.js"),
    (("./foo.js", "ext:bar.js"), "ext:foo.js"),
  ];
  for ((specifier, referrer), expected) in cases {
    let result = loader
      .resolve(specifier, referrer, ResolutionKind::Import)
      .unwrap();
    assert_eq!(result.as_str(), expected);
  }
}

#[test]
fn module_map_rejects_user_ext_imports_after_resolution() {
  struct ExtResolvingLoader;

  impl ModuleLoader for ExtResolvingLoader {
    fn resolve(
      &self,
      specifier: &str,
      referrer: &str,
      _kind: ResolutionKind,
    ) -> ModuleResolveResponse {
      if specifier == "mapped" {
        return Ok(ModuleSpecifier::parse("ext:core/ops").unwrap());
      }
      let referrer = if referrer == "." {
        "file:///"
      } else {
        referrer
      };
      resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
    }

    fn load(
      &self,
      _module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      unreachable!()
    }
  }

  let runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(Rc::new(ExtResolvingLoader)),
    ..Default::default()
  });
  let module_map = runtime.module_map();

  for specifier in ["Ext:core/ops", "mapped"] {
    let error = module_map
      .resolve(specifier, "file:///main.js", ResolutionKind::DynamicImport)
      .unwrap_err();
    let message = error.to_string();
    assert!(message.contains("Importing ext: modules is only allowed"));
    assert!(message.contains("ext:core/ops"));
  }
}

#[test]
fn invalid_utf8_module() {
  let get_string_source = ModuleSource::get_string_source(
    ModuleSourceCode::Bytes(ModuleCodeBytes::Static(b"// \xFE\xFE\xFF\xFF")),
  );
  assert_eq!(
    get_string_source,
    FastString::from_static("// \u{FFFD}\u{FFFD}\u{FFFD}\u{FFFD}")
  );
}

#[tokio::test]
async fn evaluate_already_evaluated_module() {
  // This test verifies that calling mod_evaluate on an already-evaluated module
  // doesn't panic, but instead returns Ok(()) immediately. This can happen when
  // the same module is specified both as a preload module and as the main module.

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///main.js").unwrap(),
    ascii_str!(
      "globalThis.executionCount = (globalThis.executionCount || 0) + 1;"
    ),
  )));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  let spec = resolve_url("file:///main.js").unwrap();

  // Load and evaluate the module for the first time
  let mod_id = runtime.load_main_es_module(&spec).await.unwrap();
  let receiver = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  receiver.await.unwrap();

  // Verify it executed once
  runtime
    .execute_script("check1", "if (globalThis.executionCount !== 1) throw new Error('Expected 1 execution')")
    .unwrap();

  // Try to evaluate the same module again - this should not panic
  let receiver2 = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  receiver2.await.unwrap();

  // Verify it still only executed once (module was not re-executed)
  runtime
    .execute_script("check2", "if (globalThis.executionCount !== 1) throw new Error('Expected still 1 execution')")
    .unwrap();
}

#[tokio::test]
async fn evaluate_already_evaluated_module_sync() {
  // This test verifies that calling mod_evaluate_sync on an already-evaluated module
  // doesn't panic, but instead returns Ok(()) immediately.

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///main.js").unwrap(),
    ascii_str!(
      "globalThis.syncExecutionCount = (globalThis.syncExecutionCount || 0) + 1;"
    ),
  )));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader.clone()),
    ..Default::default()
  });

  let spec = resolve_url("file:///main.js").unwrap();

  // Load the module
  let mod_id = runtime.load_main_es_module(&spec).await.unwrap();

  // Evaluate synchronously using scope
  {
    let module_map = runtime.module_map();
    deno_core::scope!(scope, runtime);
    module_map.mod_evaluate_sync(scope, mod_id).unwrap();
  }

  // Verify it executed once
  runtime
    .execute_script("check1", "if (globalThis.syncExecutionCount !== 1) throw new Error('Expected 1 execution')")
    .unwrap();

  // Try to evaluate the same module again synchronously - should not panic
  {
    let module_map = runtime.module_map();
    deno_core::scope!(scope, runtime);
    module_map.mod_evaluate_sync(scope, mod_id).unwrap();
  }

  // Verify it still only executed once (module was not re-executed)
  runtime
    .execute_script("check2", "if (globalThis.syncExecutionCount !== 1) throw new Error('Expected still 1 execution')")
    .unwrap();
}

#[tokio::test]
async fn test_native_source_map_with_trailing_content() {
  // This test verifies that source maps work even with trailing newlines and comments
  // after the sourceMappingURL directive (https://github.com/denoland/deno/issues/21988)

  // Source map for TypeScript code that throws an error on line 2:
  // function greet(name: string) {
  //   throw new Error("Test error");
  // }
  //
  // greet("World");
  let source_map_base64 = "eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGU6Ly8vdGVzdC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJmdW5jdGlvbiBncmVldChuYW1lOiBzdHJpbmcpIHtcbiAgdGhyb3cgbmV3IEVycm9yKFwiVGVzdCBlcnJvclwiKTtcbn1cblxuZ3JlZXQoXCJXb3JsZFwiKTtcbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIn0=";

  let code = format!(
    r#"export function greet(name) {{
  throw new Error("Test error");
}}

greet("World");

//# sourceMappingURL=data:application/json;base64,{}
// This is a comment after the sourceMappingURL

// Multiple blank lines follow


"#,
    source_map_base64
  );

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///test.js").unwrap(),
    code,
  )));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let spec = resolve_url("file:///test.js").unwrap();
  let mod_id = runtime.load_main_es_module(&spec).await.unwrap();
  let _receiver = runtime.mod_evaluate(mod_id);

  // Run event loop - it will propagate the module error
  let event_loop_result = runtime.run_event_loop(Default::default()).await;

  // The event loop returns the error from the uncaught promise rejection
  assert!(event_loop_result.is_err());
  let error = event_loop_result.unwrap_err();
  let error_string = error.to_string();

  // Verify that the error references the original TypeScript file and line number
  // The error should be thrown from line 2 of test.ts (not test.js)
  assert!(
    error_string.contains("test.ts"),
    "Error should reference source file test.ts, got: {}",
    error_string
  );
  assert!(
    error_string.contains("Test error"),
    "Error should contain the error message, got: {}",
    error_string
  );
}

// Module loader that supports loading external source maps
struct ExternalSourceMapLoader {
  module_code: String,
  source_map_content: String,
}

impl ModuleLoader for ExternalSourceMapLoader {
  fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
    _kind: ResolutionKind,
  ) -> ModuleResolveResponse {
    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
  }

  fn load(
    &self,
    _module_specifier: &ModuleSpecifier,
    _maybe_referrer: Option<&ModuleLoadReferrer>,
    _options: ModuleLoadOptions,
  ) -> ModuleLoadResponse {
    let code = ModuleCodeString::from(self.module_code.clone());
    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
      ModuleType::JavaScript,
      ModuleSourceCode::String(code),
      _module_specifier,
      None,
    )))
  }

  fn load_external_source_map(
    &self,
    source_map_url: &str,
  ) -> Option<Cow<'_, [u8]>> {
    // Check if this is the source map we're providing
    if source_map_url.ends_with("test.js.map")
      || source_map_url.contains("test.js.map")
    {
      Some(Cow::Borrowed(self.source_map_content.as_bytes()))
    } else {
      None
    }
  }
}

#[tokio::test]
async fn test_native_external_source_map_with_relative_path() {
  // Create a source map file for TypeScript code that throws an error
  // Original TypeScript:
  // export function throwError() {
  //   throw new Error("External source map test");
  // }
  // throwError();
  let source_map_content = r#"{
  "version": 3,
  "sources": ["file:///test.ts"],
  "sourcesContent": ["export function throwError() {\n  throw new Error(\"External source map test\");\n}\nthrowError();\n"],
  "names": [],
  "mappings": "AAAA;AACA;AACA;AACA"
}"#;

  // Create a JavaScript file that references the external source map with a relative path
  let js_code = r#"export function throwError() {
  throw new Error("External source map test");
}
throwError();

//# sourceMappingURL=./test.js.map"#;

  let file_url = Url::parse("file:///test.js").unwrap();

  let loader = Rc::new(TestingModuleLoader::new(ExternalSourceMapLoader {
    module_code: js_code.to_string(),
    source_map_content: source_map_content.to_string(),
  }));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime.load_main_es_module(&file_url).await.unwrap();
  let _receiver = runtime.mod_evaluate(mod_id);

  // Run event loop - it will propagate the module error
  let event_loop_result = runtime.run_event_loop(Default::default()).await;

  // The event loop returns the error from the uncaught promise rejection
  assert!(event_loop_result.is_err());

  let error = event_loop_result.unwrap_err();
  let error_string = error.to_string();

  // Verify that the error references the original TypeScript file (not the .js file)
  // External source maps are loaded lazily when an error occurs
  assert!(
    error_string.contains("test.ts"),
    "Error should reference source file test.ts, got: {}",
    error_string
  );
  assert!(
    error_string.contains("External source map test"),
    "Error should contain the error message, got: {}",
    error_string
  );
}

#[tokio::test]
async fn test_native_source_map_with_absolute_path_outside_cwd() {
  // Test that source maps with absolute paths outside the cwd display as absolute
  // (not relative with excessive ../ sequences)

  // Create source map with absolute path outside typical cwd
  let source_map_base64 = "eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGU6Ly8vYWJzb2x1dGUvcGF0aC9vdXRzaWRlL29yaWdpbmFsLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIE9yaWdpbmFsIGZpbGUgb3V0c2lkZSBjd2RcbmZ1bmN0aW9uIHRocm93RXJyb3IoKSB7XG4gIHRocm93IG5ldyBFcnJvcihcIkVycm9yIGZyb20gZmlsZSBvdXRzaWRlIGN3ZFwiKTtcbn1cblxudGhyb3dFcnJvcigpO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSJ9";

  let code = format!(
    r#"function throwError() {{
  throw new Error("Error from file outside cwd");
}}

throwError();

//# sourceMappingURL=data:application/json;base64,{}"#,
    source_map_base64
  );

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///test.js").unwrap(),
    code,
  )));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime
    .load_main_es_module(&Url::parse("file:///test.js").unwrap())
    .await
    .unwrap();

  // The module should load successfully even though the source map points
  // to a file outside the cwd. Error paths will show absolute paths.
  let _receiver = runtime.mod_evaluate(mod_id);
  let result = runtime.run_event_loop(Default::default()).await;

  // Expect an error since the code throws
  assert!(result.is_err());

  let err = result.unwrap_err();
  let err_str = err.to_string();

  // Verify the error contains the absolute path from the source map
  // (not relative with excessive ../)
  assert!(
    err_str.contains("/absolute/path/outside/original.ts"),
    "Error should contain absolute path: {}",
    err_str
  );
}

#[tokio::test]
async fn test_native_source_map_synthetic_bundle_path() {
  // Test that bundled code with synthetic paths (like file:///a.ts) displays correctly
  // This is common with bundlers like esbuild

  let source_map_base64 = "eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGU6Ly8vYS50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBCdW5kbGVkIGZpbGVcbmZ1bmN0aW9uIHRocm93RXJyb3IoKSB7XG4gIHRocm93IG5ldyBFcnJvcihcIkVycm9yIGZyb20gYnVuZGxlZCBmaWxlXCIpO1xufVxuXG50aHJvd0Vycm9yKCk7XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIn0=";

  let code = format!(
    r#"function throwError() {{
  throw new Error("Error from bundled file");
}}

throwError();

//# sourceMappingURL=data:application/json;base64,{}"#,
    source_map_base64
  );

  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::with(
    Url::parse("file:///bundle.js").unwrap(),
    code,
  )));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    module_loader: Some(loader),
    ..Default::default()
  });

  let mod_id = runtime
    .load_main_es_module(&Url::parse("file:///bundle.js").unwrap())
    .await
    .unwrap();

  let _receiver = runtime.mod_evaluate(mod_id);
  let result = runtime.run_event_loop(Default::default()).await;

  // Expect an error
  assert!(result.is_err());

  let err = result.unwrap_err();
  let err_str = err.to_string();

  // Verify the error shows the synthetic path from the bundle
  // Should show as absolute: /a.ts (not with excessive ../)
  assert!(
    err_str.contains("file:///a.ts"),
    "Error should contain synthetic bundle path: {}",
    err_str
  );

  // Should NOT contain excessive parent directory traversals
  assert!(
    !err_str.contains("../../../"),
    "Error should not contain excessive ../ sequences: {}",
    err_str
  );
}

/// Regression test for https://github.com/denoland/deno/issues/32758
///
/// When two lazy-loaded ESM modules are triggered during the same
/// module evaluation, and one of the importing modules has a top-level
/// await on an eagerly-resolved async op, the `perform_microtask_checkpoint()`
/// inside `lazy_load_es_module_with_code()` can prematurely resolve the main
/// module's evaluation promise while `mod_evaluate()` has not yet set up
/// its `.then()` handlers. This leaves `pending_mod_evaluation = true`
/// permanently, causing the event loop to panic with
/// "Expected at least one stalled top-level await".
#[tokio::test]
async fn test_lazy_loaded_esm_with_tla_no_panic() {
  // An async op with no .await points
  #[op2]
  #[allow(clippy::unused_async, reason = "eagerly resolves on first poll")]
  async fn op_eager_resolve() -> u32 {
    42
  }

  deno_core::extension!(
    test_ext,
    ops = [op_eager_resolve],
    lazy_loaded_esm = [
      dir "modules/testdata",
      "lazy_loaded.js",
      "lazy_loaded_2.js",
    ]
  );

  let loader = Rc::new(TestingModuleLoader::new(NoopModuleLoader));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  // Build the module graph manually:
  //
  //   main.js  (main module)
  //     ├── tla_mod.js   -- triggers lazy_loaded.js, awaits op_eager_resolve
  //     └── lazy_mod.js  -- triggers lazy_loaded_2.js
  //
  // The key is that tla_mod.js's `await` resolves eagerly, and the
  // microtask checkpoint inside lazy_loaded_2.js's lazy load can
  // prematurely resolve the main module evaluation promise.

  let (mod_main, mod_tla, mod_lazy) = {
    deno_core::scope!(scope, runtime);

    let mod_tla = module_map
      .new_es_module(
        scope,
        false,
        ascii_str!("file:///tla_mod.js").into(),
        ascii_str!(
          r#"
          const lazy1 = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded.js")();
          if (lazy1.foo !== "foo") throw new Error("lazy1.foo: " + lazy1.foo);
          if (lazy1.bar !== 123) throw new Error("lazy1.bar: " + lazy1.bar);
          const result = await Deno.core.ops.op_eager_resolve();
          if (result !== 42) throw new Error("unexpected: " + result);
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    let mod_lazy = module_map
      .new_es_module(
        scope,
        false,
        ascii_str!("file:///lazy_mod.js").into(),
        ascii_str!(
          r#"
          const lazy2 = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded_2.js")();
          if (lazy2.baz !== "baz") throw new Error("lazy2.baz: " + lazy2.baz);
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    let mod_main = module_map
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///main.js").into(),
        ascii_str!(
          r#"
          import "./tla_mod.js";
          import "./lazy_mod.js";
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    (mod_main, mod_tla, mod_lazy)
  };

  runtime.instantiate_module(mod_tla).unwrap();
  runtime.instantiate_module(mod_lazy).unwrap();
  runtime.instantiate_module(mod_main).unwrap();

  // This should not panic with "Expected at least one stalled top-level await"
  let receiver = runtime.mod_evaluate(mod_main);
  runtime.run_event_loop(Default::default()).await.unwrap();
  receiver.await.unwrap();
}

/// Regression test for https://github.com/denoland/deno/issues/32821
///
/// When an async module graph has TLA and `has_tick_scheduled` is set during
/// module evaluation (e.g. CJS `process.nextTick()` calls), the
/// `has_tick_scheduled` guard on the microtask checkpoint in `mod_evaluate`
/// could prevent TLA resume microtasks from being drained. For large module
/// graphs where the TLA op resolves during event loop processing (not during
/// `module.evaluate()` itself), this leaves the evaluation promise permanently
/// stuck in Pending, causing a panic at
/// "Expected at least one stalled top-level await".
#[tokio::test]
async fn test_tla_with_tick_scheduled_no_hang() {
  // An async op that resolves on the next event loop iteration (not eagerly)
  #[op2]
  async fn op_async_resolve() -> u32 {
    // Yield to the event loop so this does NOT resolve eagerly
    tokio::task::yield_now().await;
    42
  }

  deno_core::extension!(
    test_ext,
    ops = [op_async_resolve],
    lazy_loaded_esm = [
      dir "modules/testdata",
      "lazy_loaded.js",
      "lazy_loaded_2.js",
    ]
  );

  let loader = Rc::new(TestingModuleLoader::new(NoopModuleLoader));

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(loader),
    ..Default::default()
  });

  let module_map = runtime.module_map().clone();

  // Build a module graph where:
  //   main.js (main module)
  //     +-- tick_mod.js  -- sets has_tick_scheduled (simulates CJS nextTick)
  //     +-- tla_mod.js   -- has TLA on an async op + triggers lazy load
  //
  // The key scenario: tick_mod.js sets has_tick_scheduled = true during
  // module evaluation, causing the checkpoint in mod_evaluate to be
  // skipped. tla_mod.js has a TLA that resolves during event loop
  // processing, not during module.evaluate(). Without the fix, the
  // evaluation promise stays Pending forever.

  let (mod_main, mod_tick, mod_tla) = {
    deno_core::scope!(scope, runtime);

    let mod_tick = module_map
      .new_es_module(
        scope,
        false,
        ascii_str!("file:///tick_mod.js").into(),
        ascii_str!(
          r#"
          Deno.core.setHasTickScheduled(true);
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    let mod_tla = module_map
      .new_es_module(
        scope,
        false,
        ascii_str!("file:///tla_mod.js").into(),
        ascii_str!(
          r#"
          const lazy1 = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded.js")();
          if (lazy1.foo !== "foo") throw new Error("lazy1.foo: " + lazy1.foo);
          const result = await Deno.core.ops.op_async_resolve();
          if (result !== 42) throw new Error("unexpected: " + result);
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    let mod_main = module_map
      .new_es_module(
        scope,
        true,
        ascii_str!("file:///main.js").into(),
        ascii_str!(
          r#"
          import "./tick_mod.js";
          import "./tla_mod.js";
          "#
        )
        .into(),
        false,
        None,
      )
      .unwrap();

    (mod_main, mod_tick, mod_tla)
  };

  runtime.instantiate_module(mod_tick).unwrap();
  runtime.instantiate_module(mod_tla).unwrap();
  runtime.instantiate_module(mod_main).unwrap();

  // This should not panic or hang
  let receiver = runtime.mod_evaluate(mod_main);
  runtime.run_event_loop(Default::default()).await.unwrap();
  receiver.await.unwrap();
}

/// Regression test for https://github.com/denoland/deno/issues/34466
///
/// When a dynamic import evaluates an async module graph whose
/// dependencies trigger nested `lazy_load_esm_module` calls (e.g. an npm
/// CJS package whose `require()` chain pulls in a now-lazy `node:*`
/// polyfill), the inner `perform_microtask_checkpoint` was firing while
/// V8 was still inside the outer `module.evaluate()` on the async graph.
/// That drained the TLA dependency's await-resume microtask too early
/// and wedged V8's async module state machine — the parent module's
/// evaluation promise stayed Pending forever and the runtime reported
/// "Top-level await promise never resolved" at the dynamic import site.
///
/// The fix is to mark `evaluating_top_level` true around
/// `module.evaluate()` in `dynamic_import_module_evaluate`, matching
/// `mod_evaluate`, so nested lazy ESM loads skip their post-evaluate
/// microtask checkpoint.
#[tokio::test]
async fn test_dyn_import_async_graph_with_lazy_esm_no_stall() {
  deno_core::extension!(
    test_ext,
    lazy_loaded_esm = [
      dir "modules/testdata",
      "lazy_loaded.js",
    ]
  );

  // mod.js mimics an npm package side-effect import: it has a TLA dep
  // (tla.js) and a sibling that triggers a lazy ESM load (the CJS
  // require-of-node:* case in the original bug).
  struct Loader;
  impl ModuleLoader for Loader {
    fn resolve(
      &self,
      specifier: &str,
      referrer: &str,
      _kind: ResolutionKind,
    ) -> Result<ModuleSpecifier, ModuleLoaderError> {
      resolve_import(specifier, referrer)
        .map_err(|e| deno_error::JsErrorBox::generic(e.to_string()))
    }
    fn load(
      &self,
      module_specifier: &ModuleSpecifier,
      _maybe_referrer: Option<&ModuleLoadReferrer>,
      _options: ModuleLoadOptions,
    ) -> ModuleLoadResponse {
      let s = module_specifier.as_str();
      let code = match s {
        "file:///main.js" => {
          r#"await import("./mod.js"); globalThis.done = true;"#
        }
        // mod.js mirrors the npm-package shape from the original bug:
        // a TLA dep imported first, then a sibling whose evaluation
        // triggers a `lazy_load_esm_module` call (the CJS-require-of-a-
        // now-lazy-node:*-polyfill case).
        "file:///mod.js" => r#"import "./tla.js"; import "./helper.js";"#,
        "file:///tla.js" => r#"await Promise.resolve();"#,
        "file:///helper.js" => {
          r#"
          const lazy = Deno.core.createLazyLoader("ext:test_ext/lazy_loaded.js")();
          if (lazy.foo !== "foo") throw new Error("lazy.foo: " + lazy.foo);
          "#
        }
        _ => panic!("unexpected: {s}"),
      };
      ModuleLoadResponse::Sync(Ok(ModuleSource::new(
        ModuleType::JavaScript,
        ModuleSourceCode::String(FastString::from_static(code)),
        module_specifier,
        None,
      )))
    }
  }

  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions: vec![test_ext::init()],
    module_loader: Some(Rc::new(Loader)),
    ..Default::default()
  });

  let main_url = Url::parse("file:///main.js").unwrap();
  let mod_id = runtime.load_main_es_module(&main_url).await.unwrap();
  let receiver = runtime.mod_evaluate(mod_id);
  runtime.run_event_loop(Default::default()).await.unwrap();
  receiver.await.unwrap();

  // Confirm main.js's TLA actually completed (not just that we didn't
  // hit a stall error).
  deno_core::scope!(scope, runtime);
  let g = scope.get_current_context().global(scope);
  let key = v8::String::new(scope, "done").unwrap();
  let v = g.get(scope, key.into()).unwrap();
  assert!(v.is_true(), "main.js await import never resolved");
}
