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

use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;

use deno_core::FastString;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
use deno_core::OpState;
use deno_core::RuntimeOptions;
use deno_core::ToV8;
use deno_core::located_script_name;
use deno_core::op2;
use deno_core::serde::Deserialize;
use deno_core::serde_json::json;
use deno_core::url::Url;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use deno_lib::util::hash::FastInsecureHasher;
use deno_lib::worker::create_isolate_create_params;
use deno_path_util::resolve_url_or_path;
use deno_resolver::deno_json::JsxImportSourceConfigResolver;
use node_resolver::ResolutionMode;

use super::ResolveArgs;
use super::ResolveError;
use crate::args::TypeCheckMode;
use crate::tsc::Diagnostics;
use crate::tsc::ExecError;
use crate::tsc::LoadError;
use crate::tsc::Request;
use crate::tsc::RequestNpmState;
use crate::tsc::Response;
use crate::tsc::Stats;
use crate::tsc::get_hash;

#[op2]
#[string]
fn op_remap_specifier(
  state: &mut OpState,
  #[string] specifier: &str,
) -> Option<String> {
  let state = state.borrow::<State>();
  state
    .maybe_remapped_specifier(specifier)
    .map(|url| url.to_string())
}

#[op2]
fn op_libs() -> Vec<String> {
  crate::tsc::lib_names()
}

#[op2]
fn op_resolve(
  state: &mut OpState,
  #[string] base: &str,
  #[scoped] specifiers: Vec<(bool, String)>,
) -> Result<Vec<(String, Option<&'static str>)>, ResolveError> {
  op_resolve_inner(state, ResolveArgs { base, specifiers })
}

#[derive(Debug, ToV8)]
pub struct TscConstants {
  types_node_ignorable_names: Vec<&'static str>,
  node_only_globals: Vec<&'static str>,
  ignored_diagnostic_codes: Vec<u64>,
}

impl TscConstants {
  pub fn new() -> Self {
    Self {
      types_node_ignorable_names: super::TYPES_NODE_IGNORABLE_NAMES.to_vec(),
      node_only_globals: super::NODE_ONLY_GLOBALS.to_vec(),
      ignored_diagnostic_codes: super::IGNORED_DIAGNOSTIC_CODES
        .iter()
        .copied()
        .collect(),
    }
  }
}

#[op2]
fn op_tsc_constants() -> TscConstants {
  TscConstants::new()
}

#[inline]
fn op_resolve_inner(
  state: &mut OpState,
  args: ResolveArgs<'_>,
) -> Result<Vec<(String, Option<&'static str>)>, ResolveError> {
  let state = state.borrow_mut::<State>();
  let mut resolved: Vec<(String, Option<&'static str>)> =
    Vec::with_capacity(args.specifiers.len());
  let referrer = if let Some(remapped_specifier) =
    state.maybe_remapped_specifier(args.base)
  {
    remapped_specifier.clone()
  } else {
    resolve_url_or_path(args.base, &state.current_dir)?
  };
  let referrer_module = state.graph.get(&referrer);
  for (is_cjs, specifier) in args.specifiers {
    let result = super::resolve_specifier_for_tsc(
      specifier,
      &referrer,
      &state.graph,
      if is_cjs {
        ResolutionMode::Require
      } else {
        ResolutionMode::Import
      },
      state.maybe_npm.as_ref(),
      referrer_module,
      &mut state.remapped_specifiers,
    )?;
    resolved.push(result);
  }

  Ok(resolved)
}

#[op2]
#[string]
fn op_resolve_jsx_import_source(
  state: &mut OpState,
  #[string] referrer: &str,
) -> Option<String> {
  let state = state.borrow::<State>();
  let referrer = if let Some(remapped_specifier) =
    state.maybe_remapped_specifier(referrer)
  {
    Cow::Borrowed(remapped_specifier)
  } else {
    Cow::Owned(resolve_url_or_path(referrer, &state.current_dir).ok()?)
  };
  state
    .jsx_import_source_config_resolver
    .for_specifier(&referrer)?
    .specifier()
    .map(|s| s.to_string())
}

deno_core::extension!(deno_cli_tsc,
  ops = [
    op_create_hash,
    op_emit,
    op_is_node_file,
    op_load,
    op_remap_specifier,
    op_resolve,
    op_resolve_jsx_import_source,
    op_tsc_constants,
    op_respond,
    op_libs,
  ],
  options = {
    request: Request,
    root_map: HashMap<String, Url>,
    remapped_specifiers: HashMap<String, Url>,
  },
  state = |state, options| {
    state.put(State::new(
      options.request.graph,
      options.request.jsx_import_source_config_resolver,
      options.request.hash_data,
      options.request.maybe_npm,
      options.request.maybe_tsbuildinfo,
      options.root_map,
      options.remapped_specifiers,
      options.request.initial_cwd,
      options.request.capture_emitted_files,
    ));
  },
  customizer = |ext: &mut deno_core::Extension| {
    use deno_core::ExtensionFileSource;
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/99_main_compiler.js", crate::tsc::MAIN_COMPILER_SOURCE.as_str().into()));
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/97_ts_host.js", crate::tsc::TS_HOST_SOURCE.as_str().into()));
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/98_lsp.js", crate::tsc::LSP_SOURCE.as_str().into()));
    ext.js_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/00_typescript.js", crate::tsc::TYPESCRIPT_SOURCE.as_str().into()));
    ext.esm_entry_point = Some("ext:deno_cli_tsc/99_main_compiler.js");
  }
);
// TODO(bartlomieju): this mechanism is questionable.
// Can't we use something more efficient here?
#[op2]
fn op_respond(state: &mut OpState, #[serde] args: RespondArgs) {
  op_respond_inner(state, args)
}

#[inline]
fn op_respond_inner(state: &mut OpState, args: RespondArgs) {
  let state = state.borrow_mut::<State>();
  state.maybe_response = Some(args);
}

#[op2]
#[string]
fn op_create_hash(s: &mut OpState, #[string] text: &str) -> String {
  op_create_hash_inner(s, text)
}

#[inline]
fn op_create_hash_inner(s: &mut OpState, text: &str) -> String {
  let state = s.borrow_mut::<State>();
  get_hash(text, state.hash_data)
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmitArgs {
  /// The text data/contents of the file.
  data: String,
  /// The _internal_ filename for the file.  This will be used to determine how
  /// the file is cached and stored.
  file_name: String,
}

#[op2(fast)]
fn op_emit(
  state: &mut OpState,
  #[string] data: String,
  #[string] file_name: String,
) -> bool {
  op_emit_inner(state, EmitArgs { data, file_name })
}

#[inline]
fn op_emit_inner(state: &mut OpState, args: EmitArgs) -> bool {
  let state = state.borrow_mut::<State>();
  match args.file_name.as_ref() {
    "internal:///.tsbuildinfo" => state.maybe_tsbuildinfo = Some(args.data),
    name
      if name.ends_with(".d.ts")
        || name.ends_with(".d.ts.map")
        || name.ends_with(".d.mts")
        || name.ends_with(".d.mts.map")
        || name.ends_with(".d.cts")
        || name.ends_with(".d.cts.map") =>
    {
      if state.capture_emitted_files {
        state.emitted_files.insert(args.file_name, args.data);
      }
    }
    _ => {
      if cfg!(debug_assertions) {
        panic!("Unhandled emit write: {}", args.file_name);
      }
    }
  }

  true
}

#[op2(fast)]
fn op_is_node_file(state: &mut OpState, #[string] path: &str) -> bool {
  let state = state.borrow::<State>();
  ModuleSpecifier::parse(path)
    .ok()
    .map(|specifier| {
      state
        .maybe_npm
        .as_ref()
        .map(|n| n.node_resolver.in_npm_package(&specifier))
        .unwrap_or(false)
        || specifier.as_str().starts_with("asset:///node/")
    })
    .unwrap_or(false)
}

#[derive(Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct RespondArgs {
  pub diagnostics: Diagnostics,
  pub ambient_modules: Vec<String>,
  pub stats: Stats,
}

impl super::LoadContent for FastString {
  fn from_static(source: &'static str) -> Self {
    FastString::from_static(source)
  }
  fn from_string(source: String) -> Self {
    FastString::from(source)
  }
  fn from_arc_str(source: Arc<str>) -> Self {
    FastString::from(source)
  }
}

#[derive(Debug, ToV8)]
#[cfg_attr(test, derive(serde::Serialize))]
#[cfg_attr(test, serde(rename_all = "camelCase"))]
struct LoadResponse {
  data: FastString,
  version: Option<String>,
  script_kind: i32,
  is_cjs: bool,
}

#[op2]
fn op_load(
  state: &mut OpState,
  #[string] load_specifier: &str,
) -> Result<Option<LoadResponse>, LoadError> {
  op_load_inner(state, load_specifier)
}

impl super::Mapper for State {
  fn maybe_remapped_specifier(
    &self,
    specifier: &str,
  ) -> Option<&ModuleSpecifier> {
    self.maybe_remapped_specifier(specifier)
  }
}

fn op_load_inner(
  state: &mut OpState,
  load_specifier: &str,
) -> Result<Option<LoadResponse>, LoadError> {
  let state = state.borrow::<State>();
  Ok(
    super::load_for_tsc::<FastString, _>(
      load_specifier,
      state.maybe_npm.as_ref(),
      &state.current_dir,
      &state.graph,
      state.maybe_tsbuildinfo.as_deref(),
      state.hash_data,
      state,
    )?
    .map(|res| LoadResponse {
      data: res.data,
      version: res.version,
      is_cjs: res.is_cjs,
      script_kind: super::as_ts_script_kind(res.media_type),
    }),
  )
}

pub fn exec_request(
  request: Request,
  root_names: Vec<String>,
  root_map: HashMap<String, ModuleSpecifier>,
  remapped_specifiers: HashMap<String, ModuleSpecifier>,
  code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
) -> Result<Response, ExecError> {
  let request_value = json!({
    "config": request.config,
    "debug": request.debug,
    "rootNames": root_names,
    "localOnly": request.check_mode == TypeCheckMode::Local,
  });
  let exec_source = format!("globalThis.exec({request_value})");

  let mut extensions =
    deno_runtime::snapshot_info::get_extensions_in_snapshot();
  extensions.push(deno_cli_tsc::init(request, root_map, remapped_specifiers));
  let extension_code_cache = code_cache.map(|cache| {
    Rc::new(TscExtCodeCache::new(cache)) as Rc<dyn deno_core::ExtCodeCache>
  });
  let mut runtime = JsRuntime::new(RuntimeOptions {
    extensions,
    create_params: create_isolate_create_params(&crate::sys::CliSys::default()),
    startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
    extension_code_cache,
    // The TSC isolate shares CLI_SNAPSHOT, which under node-defer leaves
    // node:process (and the rest of node's `lazy_loaded_esm` set) outside
    // the snapshot blob. The compiler reads `process` (isNodeLikeSystem),
    // which routes through the lazy `process` accessor and would otherwise
    // fail with "Specifier \"node:process\" cannot be lazy-loaded".
    residual_lazy_esm_sources: deno_snapshots::RESIDUAL_LAZY_ESM,
    residual_lazy_js_sources: deno_snapshots::RESIDUAL_LAZY_JS,
    ..Default::default()
  });

  runtime
    .execute_script(located_script_name!(), exec_source)
    .map_err(ExecError::Js)?;

  let op_state = runtime.op_state();
  let mut op_state = op_state.borrow_mut();
  let state = op_state.take::<State>();

  if let Some(response) = state.maybe_response {
    let diagnostics = response.diagnostics;
    let ambient_modules = response.ambient_modules;
    let maybe_tsbuildinfo = state.maybe_tsbuildinfo;
    let stats = response.stats;

    Ok(Response {
      diagnostics,
      ambient_modules,
      maybe_tsbuildinfo,
      stats,
      emitted_files: state.emitted_files,
    })
  } else {
    Err(ExecError::ResponseNotSet)
  }
}

pub struct TscExtCodeCache {
  cache: Arc<dyn deno_runtime::code_cache::CodeCache>,
}

impl TscExtCodeCache {
  pub fn new(cache: Arc<dyn deno_runtime::code_cache::CodeCache>) -> Self {
    Self { cache }
  }
}

impl deno_core::ExtCodeCache for TscExtCodeCache {
  fn get_code_cache_info(
    &self,
    specifier: &ModuleSpecifier,
    code: &deno_core::ModuleSourceCode,
    esm: bool,
  ) -> deno_core::SourceCodeCacheInfo {
    use deno_runtime::code_cache::CodeCacheType;
    let code_hash = FastInsecureHasher::new_deno_versioned()
      .write_hashable(code)
      .finish();
    let data = self
      .cache
      .get_sync(
        specifier,
        if esm {
          CodeCacheType::EsModule
        } else {
          CodeCacheType::Script
        },
        code_hash,
      )
      .map(Cow::from)
      .inspect(|_| {
        log::debug!(
          "V8 code cache hit for Extension module: {specifier}, [{code_hash:?}]"
        );
      });
    deno_core::SourceCodeCacheInfo {
      hash: code_hash,
      data,
    }
  }

  fn code_cache_ready(
    &self,
    specifier: ModuleSpecifier,
    source_hash: u64,
    code_cache: &[u8],
    esm: bool,
  ) {
    use deno_runtime::code_cache::CodeCacheType;

    log::debug!(
      "Updating V8 code cache for Extension module: {specifier}, [{source_hash:?}]"
    );
    self.cache.set_sync(
      specifier,
      if esm {
        CodeCacheType::EsModule
      } else {
        CodeCacheType::Script
      },
      source_hash,
      code_cache,
    );
  }
}

// TODO(bartlomieju): we have similar struct in `tsc.rs` - maybe at least change
// the name of the struct to avoid confusion?
#[derive(Debug)]
struct State {
  hash_data: u64,
  graph: Arc<ModuleGraph>,
  jsx_import_source_config_resolver: Arc<JsxImportSourceConfigResolver>,
  maybe_tsbuildinfo: Option<String>,
  maybe_response: Option<RespondArgs>,
  maybe_npm: Option<RequestNpmState>,
  // todo(dsherret): it looks like the remapped_specifiers and
  // root_map could be combined... what is the point of the separation?
  remapped_specifiers: HashMap<String, ModuleSpecifier>,
  root_map: HashMap<String, ModuleSpecifier>,
  current_dir: PathBuf,
  capture_emitted_files: bool,
  emitted_files: BTreeMap<String, String>,
}

impl Default for State {
  fn default() -> Self {
    Self {
      hash_data: Default::default(),
      graph: Arc::new(ModuleGraph::new(GraphKind::All)),
      jsx_import_source_config_resolver: Default::default(),
      maybe_tsbuildinfo: Default::default(),
      maybe_response: Default::default(),
      maybe_npm: Default::default(),
      remapped_specifiers: Default::default(),
      root_map: Default::default(),
      current_dir: Default::default(),
      capture_emitted_files: false,
      emitted_files: Default::default(),
    }
  }
}

impl State {
  #[allow(clippy::too_many_arguments, reason = "construction")]
  pub fn new(
    graph: Arc<ModuleGraph>,
    jsx_import_source_config_resolver: Arc<JsxImportSourceConfigResolver>,
    hash_data: u64,
    maybe_npm: Option<RequestNpmState>,
    maybe_tsbuildinfo: Option<String>,
    root_map: HashMap<String, ModuleSpecifier>,
    remapped_specifiers: HashMap<String, ModuleSpecifier>,
    current_dir: PathBuf,
    capture_emitted_files: bool,
  ) -> Self {
    State {
      hash_data,
      graph,
      jsx_import_source_config_resolver,
      maybe_npm,
      maybe_tsbuildinfo,
      maybe_response: None,
      remapped_specifiers,
      root_map,
      current_dir,
      capture_emitted_files,
      emitted_files: Default::default(),
    }
  }

  pub fn maybe_remapped_specifier(
    &self,
    specifier: &str,
  ) -> Option<&ModuleSpecifier> {
    self
      .remapped_specifiers
      .get(specifier)
      .or_else(|| self.root_map.get(specifier))
  }
}

#[cfg(test)]
mod tests {
  use deno_ast::MediaType;
  use deno_core::OpState;
  use deno_core::futures::future;
  use deno_core::parking_lot::Mutex;
  use deno_core::serde_json;
  use deno_error::JsErrorBox;
  use deno_graph::GraphKind;
  use deno_graph::ModuleGraph;
  use deno_runtime::code_cache::CodeCacheType;
  use test_util::PathRef;

  use super::super::Diagnostic;
  use super::super::DiagnosticCategory;
  use super::*;
  use crate::args::CompilerOptions;
  use crate::tsc::MISSING_DEPENDENCY_SPECIFIER;
  use crate::tsc::get_lazily_loaded_asset;
  use crate::util::env::resolve_cwd;

  #[derive(Debug, Default)]
  pub struct MockLoader {
    pub fixtures: PathRef,
  }

  impl deno_graph::source::Loader for MockLoader {
    fn load(
      &self,
      specifier: &ModuleSpecifier,
      _options: deno_graph::source::LoadOptions,
    ) -> deno_graph::source::LoadFuture {
      let specifier_text = specifier
        .to_string()
        .replace(":///", "_")
        .replace("://", "_")
        .replace('/', "-");
      let source_path = self.fixtures.join(specifier_text);
      let response = source_path
        .read_to_bytes_if_exists()
        .map(|c| {
          Some(deno_graph::source::LoadResponse::Module {
            specifier: specifier.clone(),
            mtime: None,
            maybe_headers: None,
            content: c.into(),
          })
        })
        .map_err(|e| {
          deno_graph::source::LoadError::Other(Arc::new(JsErrorBox::generic(
            e.to_string(),
          )))
        });
      Box::pin(future::ready(response))
    }
  }

  async fn setup(
    maybe_specifier: Option<ModuleSpecifier>,
    maybe_hash_data: Option<u64>,
    maybe_tsbuildinfo: Option<String>,
  ) -> OpState {
    let specifier = maybe_specifier
      .unwrap_or_else(|| ModuleSpecifier::parse("file:///main.ts").unwrap());
    let hash_data = maybe_hash_data.unwrap_or(0);
    let fixtures = test_util::testdata_path().join("tsc2");
    let loader = MockLoader { fixtures };
    let mut graph = ModuleGraph::new(GraphKind::TypesOnly);
    graph
      .build(vec![specifier], Vec::new(), &loader, Default::default())
      .await;
    let state = State::new(
      Arc::new(graph),
      Default::default(),
      hash_data,
      None,
      maybe_tsbuildinfo,
      HashMap::new(),
      HashMap::new(),
      resolve_cwd(None).unwrap().into_owned(),
      false,
    );
    let mut op_state = OpState::new(None);
    op_state.put(state);
    op_state
  }

  async fn test_exec(
    specifier: &ModuleSpecifier,
  ) -> Result<Response, ExecError> {
    test_exec_with_cache(specifier, None).await
  }

  async fn test_exec_with_cache(
    specifier: &ModuleSpecifier,
    code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
  ) -> Result<Response, ExecError> {
    let hash_data = 123; // something random
    let fixtures = test_util::testdata_path().join("tsc2");
    let loader = MockLoader { fixtures };
    let mut graph = ModuleGraph::new(GraphKind::TypesOnly);
    graph
      .build(
        vec![specifier.clone()],
        Vec::new(),
        &loader,
        Default::default(),
      )
      .await;
    let config = Arc::new(CompilerOptions::new(json!({
      "allowJs": true,
      "checkJs": false,
      "esModuleInterop": true,
      "emitDecoratorMetadata": false,
      "incremental": true,
      "jsx": "react",
      "jsxFactory": "React.createElement",
      "jsxFragmentFactory": "React.Fragment",
      "lib": ["deno.window", "node"],
      "noEmit": true,
      "outDir": "internal:///",
      "strict": true,
      "target": "esnext",
      "tsBuildInfoFile": "internal:///.tsbuildinfo",
    })));
    let request = Request {
      config,
      debug: false,
      graph: Arc::new(graph),
      jsx_import_source_config_resolver: Default::default(),
      hash_data,
      maybe_npm: None,
      maybe_tsbuildinfo: None,
      root_names: vec![(specifier.clone(), MediaType::TypeScript)],
      check_mode: TypeCheckMode::All,
      initial_cwd: resolve_cwd(None).unwrap().into_owned(),
      capture_emitted_files: false,
    };
    crate::tsc::exec(request, code_cache)
  }

  #[tokio::test]
  async fn test_create_hash() {
    let mut state = setup(None, Some(123), None).await;
    let actual = op_create_hash_inner(&mut state, "some sort of content");
    assert_eq!(actual, "11905938177474799758");
  }

  #[tokio::test]
  async fn test_hash_url() {
    let specifier = deno_core::resolve_url(
      "data:application/javascript,console.log(\"Hello%20Deno\");",
    )
    .unwrap();
    assert_eq!(
      crate::tsc::hash_url(&specifier, MediaType::JavaScript),
      "data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"
    );
  }

  #[tokio::test]
  async fn test_emit_tsbuildinfo() {
    let mut state = setup(None, None, None).await;
    let actual = op_emit_inner(
      &mut state,
      EmitArgs {
        data: "some file content".to_string(),
        file_name: "internal:///.tsbuildinfo".to_string(),
      },
    );
    assert!(actual);
    let state = state.borrow::<State>();
    assert_eq!(
      state.maybe_tsbuildinfo,
      Some("some file content".to_string())
    );
  }

  #[tokio::test]
  async fn test_load() {
    let mut state = setup(
      Some(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
      None,
      Some("some content".to_string()),
    )
    .await;
    let actual =
      op_load_inner(&mut state, "https://deno.land/x/mod.ts").unwrap();
    assert_eq!(
      serde_json::to_value(actual).unwrap(),
      json!({
        "data": "console.log(\"hello deno\");\n",
        "version": "7821807483407828376",
        "scriptKind": 3,
        "isCjs": false,
      })
    );
  }

  #[tokio::test]
  async fn test_load_asset() {
    let mut state = setup(
      Some(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
      None,
      Some("some content".to_string()),
    )
    .await;
    let actual = op_load_inner(&mut state, "asset:///lib.dom.d.ts")
      .expect("should have invoked op")
      .expect("load should have succeeded");
    let expected = get_lazily_loaded_asset("lib.dom.d.ts").unwrap();
    assert_eq!(actual.data.to_string(), expected.to_string());
    assert!(actual.version.is_some());
    assert_eq!(actual.script_kind, 3);
  }

  #[tokio::test]
  async fn test_load_tsbuildinfo() {
    let mut state = setup(
      Some(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
      None,
      Some("some content".to_string()),
    )
    .await;
    let actual = op_load_inner(&mut state, "internal:///.tsbuildinfo")
      .expect("should have invoked op")
      .expect("load should have succeeded");
    assert_eq!(
      serde_json::to_value(actual).unwrap(),
      json!({
        "data": "some content",
        "version": null,
        "scriptKind": 0,
        "isCjs": false,
      })
    );
  }

  #[tokio::test]
  async fn test_load_missing_specifier() {
    let mut state = setup(None, None, None).await;
    let actual = op_load_inner(&mut state, "https://deno.land/x/mod.ts")
      .expect("should have invoked op");
    assert_eq!(serde_json::to_value(actual).unwrap(), json!(null));
  }

  #[tokio::test]
  async fn test_resolve() {
    let mut state = setup(
      Some(ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap()),
      None,
      None,
    )
    .await;
    let actual = op_resolve_inner(
      &mut state,
      ResolveArgs {
        base: "https://deno.land/x/a.ts",
        specifiers: vec![(false, "./b.ts".to_string())],
      },
    )
    .expect("should have invoked op");
    assert_eq!(
      actual,
      vec![("https://deno.land/x/b.ts".into(), Some(".ts"))]
    );
  }

  #[tokio::test]
  async fn test_resolve_empty() {
    let mut state = setup(
      Some(ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap()),
      None,
      None,
    )
    .await;
    let actual = op_resolve_inner(
      &mut state,
      ResolveArgs {
        base: "https://deno.land/x/a.ts",
        specifiers: vec![(false, "./bad.ts".to_string())],
      },
    )
    .expect("should have not errored");
    assert_eq!(
      actual,
      vec![(MISSING_DEPENDENCY_SPECIFIER.into(), Some(".d.ts"))]
    );
  }

  #[tokio::test]
  async fn test_respond() {
    let mut state = setup(None, None, None).await;
    let args = serde_json::from_value(json!({
      "diagnostics": [
        {
          "messageText": "Unknown compiler option 'invalid'.",
          "category": 1,
          "code": 5023
        }
      ],
      "stats": [["a", 12]],
      "ambientModules": []
    }))
    .unwrap();
    op_respond_inner(&mut state, args);
    let state = state.borrow::<State>();
    assert_eq!(
      state.maybe_response,
      Some(RespondArgs {
        diagnostics: Diagnostics::new(vec![Diagnostic {
          category: DiagnosticCategory::Error,
          code: 5023,
          start: None,
          end: None,
          original_source_start: None,
          message_text: Some(
            "Unknown compiler option \'invalid\'.".to_string()
          ),
          message_chain: None,
          source: None,
          source_line: None,
          file_name: None,
          related_information: None,
          reports_deprecated: None,
          reports_unnecessary: None,
          other: Default::default(),
          missing_specifier: None,
        }]),
        ambient_modules: vec![],
        stats: Stats(vec![("a".to_string(), 12)])
      })
    );
  }

  #[tokio::test]
  async fn test_exec_basic() {
    let specifier = ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap();
    let actual = test_exec(&specifier)
      .await
      .expect("exec should not have errored");
    assert!(
      !actual.diagnostics.has_diagnostic(),
      "unexpected diagnostics: {actual_diagnostics}",
      actual_diagnostics = actual.diagnostics
    );
    assert!(actual.maybe_tsbuildinfo.is_some());
    assert_eq!(actual.stats.0.len(), 12);
  }

  #[tokio::test]
  async fn test_exec_reexport_dts() {
    let specifier = ModuleSpecifier::parse("file:///reexports.ts").unwrap();
    let actual = test_exec(&specifier)
      .await
      .expect("exec should not have errored");
    assert!(
      !actual.diagnostics.has_diagnostic(),
      "unexpected diagnostics: {actual_diagnostics}",
      actual_diagnostics = actual.diagnostics
    );
    assert!(actual.maybe_tsbuildinfo.is_some());
    assert_eq!(actual.stats.0.len(), 12);
  }

  #[tokio::test]
  async fn fix_lib_ref() {
    let specifier = ModuleSpecifier::parse("file:///libref.ts").unwrap();
    // This test verifies that `/// <reference lib="dom" />` loads correctly.
    // Note: loading lib.dom alongside Deno + Node types produces expected
    // type conflicts (duplicate declarations, modifier mismatches), so we
    // only assert that exec succeeds, not that diagnostics are empty.
    let _actual = test_exec(&specifier)
      .await
      .expect("exec should not have errored");
  }

  pub type SpecifierWithType = (ModuleSpecifier, CodeCacheType);

  #[derive(Default)]
  struct TestExtCodeCache {
    cache: Mutex<HashMap<(SpecifierWithType, u64), Vec<u8>>>,

    hits: Mutex<HashMap<SpecifierWithType, usize>>,
    misses: Mutex<HashMap<SpecifierWithType, usize>>,
  }

  impl deno_runtime::code_cache::CodeCache for TestExtCodeCache {
    fn get_sync(
      &self,
      specifier: &ModuleSpecifier,
      code_cache_type: CodeCacheType,
      source_hash: u64,
    ) -> Option<Vec<u8>> {
      let result = self
        .cache
        .lock()
        .get(&((specifier.clone(), code_cache_type), source_hash))
        .cloned();
      if result.is_some() {
        *self
          .hits
          .lock()
          .entry((specifier.clone(), code_cache_type))
          .or_default() += 1;
      } else {
        *self
          .misses
          .lock()
          .entry((specifier.clone(), code_cache_type))
          .or_default() += 1;
      }
      result
    }

    fn set_sync(
      &self,
      specifier: ModuleSpecifier,
      code_cache_type: CodeCacheType,
      source_hash: u64,
      data: &[u8],
    ) {
      self
        .cache
        .lock()
        .insert(((specifier, code_cache_type), source_hash), data.to_vec());
    }
  }

  #[tokio::test]
  async fn test_exec_code_cache() {
    let code_cache = Arc::new(TestExtCodeCache::default());
    let specifier = ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap();
    let actual = test_exec_with_cache(&specifier, Some(code_cache.clone()))
      .await
      .expect("exec should not have errored");
    assert!(
      !actual.diagnostics.has_diagnostic(),
      "unexpected diagnostics: {actual_diagnostics}",
      actual_diagnostics = actual.diagnostics
    );

    let expect = [
      (
        "ext:deno_cli_tsc/99_main_compiler.js",
        CodeCacheType::EsModule,
      ),
      ("ext:deno_cli_tsc/98_lsp.js", CodeCacheType::EsModule),
      ("ext:deno_cli_tsc/97_ts_host.js", CodeCacheType::EsModule),
      ("ext:deno_cli_tsc/00_typescript.js", CodeCacheType::Script),
    ];

    {
      let mut files = HashMap::new();

      for (((specifier, ty), _), _) in code_cache.cache.lock().iter() {
        let specifier = specifier.to_string();
        if files.contains_key(&specifier) {
          panic!("should have only 1 entry per specifier");
        }
        files.insert(specifier, *ty);
      }

      // 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
      assert_eq!(files.len(), 4);
      assert_eq!(code_cache.hits.lock().len(), 0);
      assert_eq!(code_cache.misses.lock().len(), 4);

      for (specifier, ty) in &expect {
        assert_eq!(files.get(*specifier), Some(ty));
      }

      code_cache.hits.lock().clear();
      code_cache.misses.lock().clear();
    }

    {
      let _ = test_exec_with_cache(&specifier, Some(code_cache.clone()))
        .await
        .expect("exec should not have errored");

      // 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
      assert_eq!(code_cache.hits.lock().len(), 4);
      assert_eq!(code_cache.misses.lock().len(), 0);

      for (specifier, ty) in expect {
        let url = ModuleSpecifier::parse(specifier).unwrap();
        assert_eq!(code_cache.hits.lock().get(&(url, ty)), Some(&1));
      }
    }
  }
}
