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

use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use std::thread;

use console_static_text::ansi::strip_ansi_codes;
use deno_ast::MediaType;
use deno_ast::diagnostics::Diagnostic as _;
use deno_core::ModuleSpecifier;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::futures::StreamExt;
use deno_core::parking_lot::RwLock;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::url::Url;
use deno_error::JsErrorClass;
use deno_graph::Resolution;
use deno_graph::ResolutionError;
use deno_graph::SpecifierError;
use deno_graph::fast_check::FastCheckDiagnostic;
use deno_graph::source::ResolveError;
use deno_lib::args::CaData;
use deno_resolver::deno_json::CompilerOptionsKey;
use deno_resolver::graph::enhanced_resolution_error_message;
use deno_resolver::workspace::sloppy_imports_resolve;
use deno_runtime::deno_node;
use deno_runtime::tokio_util::create_basic_runtime;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReq;
use import_map::ImportMap;
use import_map::ImportMapDiagnostic;
use import_map::ImportMapErrorKind;
use log::error;
use lsp_types::Uri;
use node_resolver::NodeResolutionKind;
use text_size::TextSize;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
use tower_lsp::lsp_types as lsp;

use super::analysis;
use super::analysis::import_map_lookup_for_diagnostic;
use super::client::Client;
use super::config::ConfigWatchedFileType;
use super::documents::Document;
use super::documents::DocumentModule;
use super::documents::DocumentModules;
use super::language_server;
use super::language_server::StateSnapshot;
use super::performance::Performance;
use super::ts_server::TsServer;
use crate::args::Flags;
use crate::args::InternalFlags;
use crate::args::PermissionFlags;
use crate::args::TypeCheckMode;
use crate::factory::CliFactory;
use crate::graph_util::CreatePublishGraphOptions;
use crate::lsp::documents::OpenDocument;
use crate::lsp::language_server::OnceCellMap;
use crate::lsp::lint::LspLinter;
use crate::lsp::logging::lsp_warn;
use crate::lsp::urls::uri_to_url;
use crate::sys::CliSys;
use crate::tools::lint::collect_no_slow_type_diagnostics;
use crate::type_checker::ambient_modules_to_regex_string;
use crate::util::path::to_percent_decoded_str;

#[derive(Debug)]
pub struct DiagnosticsUpdateMessage {
  pub snapshot: Arc<StateSnapshot>,
}

#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub enum DiagnosticSource {
  Deno,
  Doc,
  Lint,
  Ts,
}

impl DiagnosticSource {
  pub fn as_lsp_source(&self) -> &'static str {
    match self {
      Self::Deno => "deno",
      Self::Doc => "deno-doc",
      Self::Lint => "deno-lint",
      Self::Ts => "deno-ts",
    }
  }
}

/// No-slow-types (fast check) diagnostics are computed for an entire
/// publishable JSR package at once and can land on any module within it, so
/// they're cached per package scope and then distributed to the individual
/// documents they belong to. The inner map is keyed by the specifier of the
/// module each diagnostic points at.
pub type NoSlowTypesDiagnosticsCache = OnceCellMap<
  Arc<ModuleSpecifier>,
  Arc<HashMap<ModuleSpecifier, Vec<lsp::Diagnostic>>>,
>;

fn should_send_diagnostic_batch_notifications() -> bool {
  deno_lib::args::has_flag_env_var(
    &CliSys::default(),
    "DENO_INTERNAL_DIAGNOSTIC_BATCH_NOTIFICATIONS",
  )
}

#[derive(Debug)]
struct DocumentDiagnosticsState {
  version: i32,
  ts_diagnostics: Arc<Vec<lsp::Diagnostic>>,
  no_cache_diagnostics: Arc<Vec<lsp::Diagnostic>>,
}

#[derive(Debug, Default)]
pub struct DiagnosticsState {
  documents: RwLock<HashMap<Uri, DocumentDiagnosticsState>>,
}

impl DiagnosticsState {
  fn update(&self, uri: &Uri, version: i32, diagnostics: &[lsp::Diagnostic]) {
    let mut specifiers = self.documents.write();
    let current_version = specifiers.get(uri).map(|s| s.version);
    if let Some(current_version) = current_version
      && version < current_version
    {
      return;
    }
    let mut ts_diagnostics = vec![];
    let mut no_cache_diagnostics = vec![];
    for diagnostic in diagnostics {
      if diagnostic.source.as_deref()
        == Some(DiagnosticSource::Ts.as_lsp_source())
      {
        ts_diagnostics.push(diagnostic.clone());
      }
      if diagnostic.code
        == Some(lsp::NumberOrString::String("no-cache".to_string()))
        || diagnostic.code
          == Some(lsp::NumberOrString::String("not-installed-jsr".to_string()))
        || diagnostic.code
          == Some(lsp::NumberOrString::String("not-installed-npm".to_string()))
      {
        no_cache_diagnostics.push(diagnostic.clone());
      }
    }
    specifiers.insert(
      uri.clone(),
      DocumentDiagnosticsState {
        version,
        ts_diagnostics: Arc::new(ts_diagnostics),
        no_cache_diagnostics: Arc::new(no_cache_diagnostics),
      },
    );
  }

  pub fn clear(&self, uri: &Uri) {
    self.documents.write().remove(uri);
  }

  pub fn ts_diagnostics(&self, uri: &Uri) -> Arc<Vec<lsp::Diagnostic>> {
    self
      .documents
      .read()
      .get(uri)
      .map(|s| s.ts_diagnostics.clone())
      .unwrap_or_default()
  }

  pub fn has_no_cache_diagnostics(&self, uri: &Uri) -> bool {
    self
      .documents
      .read()
      .get(uri)
      .map(|s| !s.no_cache_diagnostics.is_empty())
      .unwrap_or(false)
  }

  pub fn no_cache_diagnostics(&self, uri: &Uri) -> Arc<Vec<lsp::Diagnostic>> {
    self
      .documents
      .read()
      .get(uri)
      .map(|s| s.no_cache_diagnostics.clone())
      .unwrap_or_default()
  }
}

pub struct DiagnosticsServer {
  channel: Option<mpsc::UnboundedSender<DiagnosticsUpdateMessage>>,
  client: Client,
  performance: Arc<Performance>,
  ts_server: Arc<TsServer>,
  pub state: Arc<DiagnosticsState>,
}

impl std::fmt::Debug for DiagnosticsServer {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("DiagnosticsServer")
      .field("channel", &self.channel)
      .field("client", &self.client)
      .field("performance", &self.performance)
      .field("ts_server", &self.ts_server)
      .field("state", &self.state)
      .finish()
  }
}

impl DiagnosticsServer {
  pub fn new(
    client: Client,
    performance: Arc<Performance>,
    ts_server: Arc<TsServer>,
  ) -> Self {
    DiagnosticsServer {
      channel: Default::default(),
      client,
      performance,
      ts_server,
      state: Default::default(),
    }
  }

  pub fn start(&mut self) {
    let (tx, mut rx) = mpsc::unbounded_channel::<DiagnosticsUpdateMessage>();
    self.channel = Some(tx);
    let client = self.client.clone();
    let state = self.state.clone();
    let performance = self.performance.clone();
    let ts_server = self.ts_server.clone();
    let should_send_batch_notifications =
      should_send_diagnostic_batch_notifications();

    let _join_handle = thread::spawn(move || {
      let runtime = create_basic_runtime();

      runtime.block_on(async {
        let ambient_modules_regex_cache = Arc::new(OnceCellMap::<
          (CompilerOptionsKey, Option<Arc<Uri>>),
          Option<regex::Regex>,
        >::new());
        let mut _previous_handle;
        while let Some(message) = rx.recv().await {
          let _mark = performance.measure_scope("lsp.update_diagnostics");
          let client = client.clone();
          let state = state.clone();
          let ts_server = ts_server.clone();
          let ambient_modules_regex_cache = ambient_modules_regex_cache.clone();
          let join_handle = tokio::task::spawn(async move {
            let token = CancellationToken::new();
            let _drop_guard = token.drop_guard_ref();
            let DiagnosticsUpdateMessage { snapshot } = message;
            if should_send_batch_notifications {
              client.send_diagnostic_batch_start_notification();
            }
            // Computed lazily and shared across all documents of a package so
            // the (potentially expensive) graph build only happens once per
            // package scope per diagnostics run.
            let no_slow_types_cache =
              Arc::new(NoSlowTypesDiagnosticsCache::default());
            let open_docs =
              snapshot.document_modules.documents.open_docs().cloned();
            deno_core::futures::stream::iter(open_docs.map(|document| {
              let snapshot = snapshot.clone();
              let ts_server = ts_server.clone();
              let ambient_modules_regex_cache =
                ambient_modules_regex_cache.clone();
              let no_slow_types_cache = no_slow_types_cache.clone();
              let token = token.clone();
              AbortOnDropHandle::new(tokio::task::spawn(async move {
                let diagnostics = generate_document_diagnostics(
                  &document,
                  &snapshot,
                  &ts_server,
                  &ambient_modules_regex_cache,
                  &no_slow_types_cache,
                  &token,
                )
                .await
                .unwrap_or_else(|err| {
                  lsp_warn!(
                    "Couldn't generate diagnostics for \"{}\": {err:#}",
                    document.uri.as_str()
                  );
                  vec![]
                });
                (document, diagnostics)
              }))
            }))
            .buffered(
              std::thread::available_parallelism()
                .map(From::from)
                .unwrap_or(8),
            )
            .for_each(|result| async {
              let (document, diagnostics) = match result {
                Ok(r) => r,
                Err(err) => {
                  lsp_warn!("Diagnostics task join error: {err:#}");
                  return;
                }
              };
              publish_document_diagnostics(
                &document,
                diagnostics,
                &client,
                &state,
                &token,
              )
              .await
            })
            .await;
            if should_send_batch_notifications {
              client.send_diagnostic_batch_end_notification();
            }
          });
          _previous_handle = AbortOnDropHandle::new(join_handle);
        }
      })
    });
  }

  pub fn update(
    &self,
    message: DiagnosticsUpdateMessage,
  ) -> Result<(), AnyError> {
    // todo(dsherret): instead of queuing up messages, it would be better to
    // instead only store the latest message (ex. maybe using a
    // tokio::sync::watch::channel)
    if let Some(tx) = &self.channel {
      tx.send(message).map_err(|err| err.into())
    } else {
      Err(anyhow!("diagnostics server not started"))
    }
  }
}

impl<'a> From<&'a crate::tsc::DiagnosticCategory> for lsp::DiagnosticSeverity {
  fn from(category: &'a crate::tsc::DiagnosticCategory) -> Self {
    match category {
      crate::tsc::DiagnosticCategory::Error => lsp::DiagnosticSeverity::ERROR,
      crate::tsc::DiagnosticCategory::Warning => {
        lsp::DiagnosticSeverity::WARNING
      }
      crate::tsc::DiagnosticCategory::Suggestion => {
        lsp::DiagnosticSeverity::HINT
      }
      crate::tsc::DiagnosticCategory::Message => {
        lsp::DiagnosticSeverity::INFORMATION
      }
    }
  }
}

impl<'a> From<&'a crate::tsc::Position> for lsp::Position {
  fn from(pos: &'a crate::tsc::Position) -> Self {
    Self {
      line: pos.line as u32,
      character: pos.character as u32,
    }
  }
}

fn get_diagnostic_message(diagnostic: &crate::tsc::Diagnostic) -> String {
  if let Some(message) = diagnostic.message_text.clone() {
    message
  } else if let Some(message_chain) = diagnostic.message_chain.clone() {
    message_chain.format_message(0)
  } else {
    "[missing message]".to_string()
  }
}

fn to_lsp_range(
  start: &crate::tsc::Position,
  end: &crate::tsc::Position,
) -> lsp::Range {
  lsp::Range {
    start: start.into(),
    end: end.into(),
  }
}

fn to_lsp_related_information(
  related_information: &Option<Vec<crate::tsc::Diagnostic>>,
  module: &DocumentModule,
  document_modules: &DocumentModules,
) -> Option<Vec<lsp::DiagnosticRelatedInformation>> {
  related_information.as_ref().map(|related| {
    related
      .iter()
      .filter_map(|ri| {
        if let (Some(file_name), Some(start), Some(end)) =
          (&ri.file_name, &ri.start, &ri.end)
        {
          let uri = resolve_url(file_name)
            .ok()
            .and_then(|s| {
              document_modules.module_for_specifier(
                &s,
                module.scope.as_deref(),
                Some(&module.compiler_options_key),
              )
            })
            .map(|m| m.uri.as_ref().clone())
            .unwrap_or_else(|| Uri::from_str("unknown:").unwrap());
          Some(lsp::DiagnosticRelatedInformation {
            location: lsp::Location {
              uri,
              range: to_lsp_range(start, end),
            },
            message: get_diagnostic_message(ri),
          })
        } else {
          None
        }
      })
      .collect()
  })
}

pub fn ts_json_to_diagnostics(
  diagnostics: Vec<crate::tsc::Diagnostic>,
  module: &DocumentModule,
  document_modules: &DocumentModules,
) -> Vec<lsp::Diagnostic> {
  diagnostics
    .iter()
    .filter_map(|d| {
      if let (Some(start), Some(end)) = (&d.start, &d.end) {
        Some(lsp::Diagnostic {
          range: to_lsp_range(start, end),
          severity: Some((&d.category).into()),
          code: Some(lsp::NumberOrString::Number(d.code as i32)),
          code_description: None,
          source: Some(DiagnosticSource::Ts.as_lsp_source().to_string()),
          message: get_diagnostic_message(d),
          related_information: to_lsp_related_information(
            &d.related_information,
            module,
            document_modules,
          ),
          tags: match d.code {
            // These are codes that indicate the variable is unused.
            2695 | 6133 | 6138 | 6192 | 6196 | 6198 | 6199 | 6205 | 7027
            | 7028 => Some(vec![lsp::DiagnosticTag::UNNECESSARY]),
            // These are codes that indicated the variable is deprecated.
            2789 | 6385 | 6387 => Some(vec![lsp::DiagnosticTag::DEPRECATED]),
            _ => None,
          },
          data: None,
        })
      } else {
        None
      }
    })
    .collect()
}

/// Generates lint diagnostics for a module, caching them on the module
/// instance. Lint diagnostics depend only on the module's own contents, so a
/// given module instance always produces the same result and can be cached.
///
/// Returns `Err` if linting was cancelled, in which case nothing is cached and
/// the result is recomputed on the next request.
fn generate_document_lint_diagnostics(
  module: &DocumentModule,
  linter: &LspLinter,
  token: CancellationToken,
) -> Result<Arc<Vec<lsp::Diagnostic>>, AnyError> {
  module
    .lint_diagnostics
    .get_or_try_init(|| {
      compute_document_lint_diagnostics(module, linter, token).map(Arc::new)
    })
    .cloned()
}

fn compute_document_lint_diagnostics(
  module: &DocumentModule,
  linter: &LspLinter,
  token: CancellationToken,
) -> Result<Vec<lsp::Diagnostic>, AnyError> {
  if !module.is_diagnosable()
    || !linter
      .lint_config
      .files
      .matches_specifier(&module.specifier)
  {
    return Ok(Vec::new());
  }
  match &module
    .open_data
    .as_ref()
    .and_then(|d| d.parsed_source.as_ref())
  {
    Some(Ok(parsed_source)) => {
      let references =
        analysis::get_lint_references(parsed_source, &linter.inner, token)?;
      Ok(
        references
          .into_iter()
          .map(|r| r.to_diagnostic())
          .collect::<Vec<_>>(),
      )
    }
    Some(Err(_)) => Ok(Vec::new()),
    None => {
      error!("Missing file contents for: {}", &module.specifier);
      Ok(Vec::new())
    }
  }
}

/// Computes an LSP range to highlight for a `deno doc --lint` diagnostic.
///
/// `deno_doc` only reports the byte offset of the start of the offending
/// symbol, so highlight the identifier-like token that begins there (falling
/// back to a single character) to produce a visible squiggle.
fn doc_diagnostic_range(
  module: &DocumentModule,
  byte_index: usize,
) -> lsp::Range {
  let text = &*module.text;
  let bytes = text.as_bytes();
  let start = byte_index.min(bytes.len());
  let mut end = start;
  while end < bytes.len() {
    let b = bytes[end];
    if b.is_ascii_alphanumeric() || b == b'_' || b == b'$' {
      end += 1;
    } else {
      break;
    }
  }
  if end == start {
    // Ensure a non-empty range so the diagnostic renders.
    end = (start + 1).min(bytes.len());
    while end < bytes.len() && !text.is_char_boundary(end) {
      end += 1;
    }
  }
  lsp::Range {
    start: module
      .line_index
      .position_utf16(TextSize::from(start as u32)),
    end: module.line_index.position_utf16(TextSize::from(end as u32)),
  }
}

/// Generates `deno doc --lint` diagnostics for a module that is a published
/// package's export entrypoint, caching them on the module instance. Like lint
/// diagnostics, these are derived solely from the module's own contents, so a
/// given module instance always produces the same result.
///
/// Returns `Err` if generation was cancelled, in which case nothing is cached
/// and the result is recomputed on the next request.
fn generate_document_doc_diagnostics(
  module: &DocumentModule,
  token: &CancellationToken,
) -> Result<Arc<Vec<lsp::Diagnostic>>, AnyError> {
  module
    .doc_diagnostics
    .get_or_try_init(|| {
      compute_document_doc_diagnostics(module, token).map(Arc::new)
    })
    .cloned()
}

fn compute_document_doc_diagnostics(
  module: &DocumentModule,
  token: &CancellationToken,
) -> Result<Vec<lsp::Diagnostic>, AnyError> {
  if token.is_cancelled() {
    return Err(anyhow!("cancelled"));
  }
  if !module.is_diagnosable() {
    return Ok(Vec::new());
  }
  // Only open documents have parsed sources to lint against.
  if !module
    .open_data
    .as_ref()
    .and_then(|d| d.parsed_source.as_ref())
    .is_some_and(|r| r.is_ok())
  {
    return Ok(Vec::new());
  }
  let analyzer = deno_graph::ast::CapturingModuleAnalyzer::default();
  let specifier = module.specifier.as_ref().clone();
  let loader = deno_graph::source::MemoryLoader::new(
    vec![(
      specifier.to_string(),
      deno_graph::source::Source::Module {
        specifier: specifier.to_string(),
        content: module.text.to_string(),
        maybe_headers: None,
      },
    )],
    Vec::new(),
  );
  let roots = vec![specifier.clone()];
  let mut graph =
    deno_graph::ModuleGraph::new(deno_graph::GraphKind::TypesOnly);
  deno_core::futures::executor::block_on(graph.build(
    roots.clone(),
    Vec::new(),
    &loader,
    deno_graph::BuildOptions {
      is_dynamic: false,
      skip_dynamic_deps: false,
      passthrough_jsr_specifiers: false,
      executor: Default::default(),
      file_system: &deno_graph::source::NullFileSystem,
      jsr_metadata_store: None,
      jsr_url_provider: Default::default(),
      jsr_version_resolver: Default::default(),
      locker: None,
      module_analyzer: &analyzer,
      module_info_cacher: Default::default(),
      npm_resolver: None,
      reporter: None,
      resolver: None,
      unstable_bytes_imports: false,
      unstable_text_imports: false,
      unstable_css_imports: false,
      unstable_config_imports: false,
    },
  ));
  if token.is_cancelled() {
    return Err(anyhow!("cancelled"));
  }
  let doc_parser = match deno_doc::DocParser::new(
    &graph,
    &analyzer,
    &roots,
    deno_doc::DocParserOptions {
      diagnostics: true,
      private: false,
    },
  ) {
    Ok(doc_parser) => doc_parser,
    Err(err) => {
      lsp_warn!("Failed to create doc parser for \"{specifier}\": {err:#}");
      return Ok(Vec::new());
    }
  };
  if let Err(err) = doc_parser.parse() {
    lsp_warn!("Failed to parse docs for \"{specifier}\": {err:#}");
    return Ok(Vec::new());
  }
  let specifier_str = specifier.as_str();
  Ok(
    doc_parser
      .take_diagnostics()
      .into_iter()
      // Only report diagnostics that point at the linted module itself. With a
      // single-module graph cross-module references aren't resolved, so any
      // diagnostic for another file would be spurious.
      .filter(|diagnostic| {
        diagnostic.location.filename.as_ref() == specifier_str
      })
      .map(|diagnostic| {
        use deno_ast::diagnostics::Diagnostic;
        lsp::Diagnostic {
          range: doc_diagnostic_range(module, diagnostic.location.byte_index),
          severity: Some(lsp::DiagnosticSeverity::WARNING),
          code: Some(lsp::NumberOrString::String(
            diagnostic.code().into_owned(),
          )),
          code_description: None,
          source: Some(DiagnosticSource::Doc.as_lsp_source().to_string()),
          message: diagnostic.message().into_owned(),
          related_information: None,
          tags: None,
          data: None,
        }
      })
      .collect(),
  )
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticDataSpecifier {
  pub specifier: ModuleSpecifier,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DiagnosticDataStrSpecifier {
  pub specifier: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DiagnosticDataRedirect {
  pub redirect: ModuleSpecifier,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DiagnosticDataNoLocal {
  pub to: ModuleSpecifier,
  pub message: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DiagnosticDataImportMapRemap {
  pub from: String,
  pub to: String,
}

/// An enum which represents diagnostic errors which originate from Deno itself.
pub enum DenoDiagnostic {
  /// A `x-deno-warning` is associated with the specifier and should be displayed
  /// as a warning to the user.
  DenoWarn(String),
  /// The import map file has invalid JSON or invalid import map entries.
  ImportMapDiagnostic(String),
  /// An informational diagnostic that indicates an existing specifier can be
  /// remapped to an import map import specifier.
  ImportMapRemap { from: String, to: String },
  /// The import assertion type is incorrect.
  InvalidAttributeType(String),
  /// A module requires an attribute type to be a valid import.
  NoAttributeType,
  /// A remote module was not found in the cache.
  NoCache(ModuleSpecifier),
  /// A jsr package reference was not found in the cache.
  NotInstalledJsr(PackageReq, ModuleSpecifier),
  /// An npm package reference was not found in the cache.
  NotInstalledNpm(PackageReq, ModuleSpecifier),
  /// An npm package reference was not exported by its package.
  NoExportNpm(NpmPackageReqReference),
  /// A local module was not found on the local file system.
  NoLocal(ModuleSpecifier),
  /// An error occurred when resolving the specifier string. The second field
  /// holds the names of packages importable by bare specifier (workspace
  /// members and packages linked via the "links" field), used to enhance the
  /// error with a better hint.
  ResolutionError(deno_graph::ResolutionError, Vec<String>),
  /// Unknown `node:` specifier.
  UnknownNodeSpecifier(ModuleSpecifier),
  /// Bare specifier is used for `node:` specifier
  BareNodeSpecifier(String),
}

impl DenoDiagnostic {
  fn code(&self) -> &str {
    match self {
      Self::DenoWarn(_) => "deno-warn",
      Self::ImportMapDiagnostic(_) => "import-map-diagnostic",
      Self::ImportMapRemap { .. } => "import-map-remap",
      Self::InvalidAttributeType(_) => "invalid-attribute-type",
      Self::NoAttributeType => "no-attribute-type",
      Self::NoCache(_) => "no-cache",
      Self::NotInstalledJsr(_, _) => "not-installed-jsr",
      Self::NotInstalledNpm(_, _) => "not-installed-npm",
      Self::NoExportNpm(_) => "no-export-npm",
      Self::NoLocal(_) => "no-local",
      Self::ResolutionError(err, _) => {
        if deno_resolver::graph::get_resolution_error_bare_node_specifier(err)
          .is_some()
        {
          "import-node-prefix-missing"
        } else {
          match err {
            ResolutionError::InvalidDowngrade { .. } => "invalid-downgrade",
            ResolutionError::InvalidJsrHttpsTypesImport { .. } => {
              "invalid-jsr-https-types-import"
            }
            ResolutionError::InvalidLocalImport { .. } => {
              "invalid-local-import"
            }
            ResolutionError::InvalidSpecifier { error, .. } => match error {
              SpecifierError::ImportPrefixMissing { .. } => {
                "import-prefix-missing"
              }
              SpecifierError::InvalidUrl(_) => "invalid-url",
            },
            ResolutionError::ResolverError { .. } => "resolver-error",
          }
        }
      }
      Self::UnknownNodeSpecifier(_) => "resolver-error",
      Self::BareNodeSpecifier(_) => "import-node-prefix-missing",
    }
  }

  /// A "static" method which for a diagnostic that originated from the
  /// structure returns a code action which can resolve the diagnostic.
  pub fn get_code_action(
    uri: &Uri,
    specifier: &ModuleSpecifier,
    diagnostic: &lsp::Diagnostic,
  ) -> Result<lsp::CodeAction, AnyError> {
    if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code {
      let code_action = match code.as_str() {
        "import-map-remap" => {
          let data = diagnostic
            .data
            .clone()
            .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
          let DiagnosticDataImportMapRemap { from, to } =
            serde_json::from_value(data)?;
          lsp::CodeAction {
            title: format!("Update \"{from}\" to \"{to}\" to use import map."),
            kind: Some(lsp::CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            edit: Some(lsp::WorkspaceEdit {
              changes: Some(HashMap::from([(
                uri.clone(),
                vec![lsp::TextEdit {
                  new_text: format!("\"{to}\""),
                  range: diagnostic.range,
                }],
              )])),
              ..Default::default()
            }),
            ..Default::default()
          }
        }
        "no-attribute-type" => lsp::CodeAction {
          title: "Insert import attribute.".to_string(),
          kind: Some(lsp::CodeActionKind::QUICKFIX),
          diagnostics: Some(vec![diagnostic.clone()]),
          edit: Some(lsp::WorkspaceEdit {
            changes: Some(HashMap::from([(
              uri.clone(),
              vec![lsp::TextEdit {
                new_text: " with { type: \"json\" }".to_string(),
                range: lsp::Range {
                  start: diagnostic.range.end,
                  end: diagnostic.range.end,
                },
              }],
            )])),
            ..Default::default()
          }),
          ..Default::default()
        },
        "no-cache" | "not-installed-jsr" | "not-installed-npm" => {
          let data = diagnostic
            .data
            .clone()
            .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
          let data: DiagnosticDataSpecifier = serde_json::from_value(data)?;
          let title = if matches!(
            code.as_str(),
            "not-installed-jsr" | "not-installed-npm"
          ) {
            format!("Install \"{}\" and its dependencies.", data.specifier)
          } else {
            format!("Cache \"{}\" and its dependencies.", data.specifier)
          };
          lsp::CodeAction {
            title,
            kind: Some(lsp::CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            command: Some(lsp::Command {
              title: "".to_string(),
              command: "deno.cache".to_string(),
              arguments: Some(vec![json!([data.specifier]), json!(&specifier)]),
            }),
            ..Default::default()
          }
        }
        "no-local" => {
          let data = diagnostic
            .data
            .clone()
            .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
          let data: DiagnosticDataNoLocal = serde_json::from_value(data)?;
          lsp::CodeAction {
            title: data.message,
            kind: Some(lsp::CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            edit: Some(lsp::WorkspaceEdit {
              changes: Some(HashMap::from([(
                uri.clone(),
                vec![lsp::TextEdit {
                  new_text: format!(
                    "\"{}\"",
                    relative_specifier(&data.to, specifier)
                  ),
                  range: diagnostic.range,
                }],
              )])),
              ..Default::default()
            }),
            ..Default::default()
          }
        }
        "redirect" => {
          let data = diagnostic
            .data
            .clone()
            .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
          let data: DiagnosticDataRedirect = serde_json::from_value(data)?;
          lsp::CodeAction {
            title: "Update specifier to its redirected specifier.".to_string(),
            kind: Some(lsp::CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            edit: Some(lsp::WorkspaceEdit {
              changes: Some(HashMap::from([(
                uri.clone(),
                vec![lsp::TextEdit {
                  new_text: format!(
                    "\"{}\"",
                    specifier_text_for_redirected(&data.redirect, specifier)
                  ),
                  range: diagnostic.range,
                }],
              )])),
              ..Default::default()
            }),
            ..Default::default()
          }
        }
        "import-node-prefix-missing" => {
          let data = diagnostic
            .data
            .clone()
            .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
          let data: DiagnosticDataStrSpecifier = serde_json::from_value(data)?;
          lsp::CodeAction {
            title: format!("Update specifier to node:{}", data.specifier),
            kind: Some(lsp::CodeActionKind::QUICKFIX),
            diagnostics: Some(vec![diagnostic.clone()]),
            edit: Some(lsp::WorkspaceEdit {
              changes: Some(HashMap::from([(
                uri.clone(),
                vec![lsp::TextEdit {
                  new_text: format!("\"node:{}\"", data.specifier),
                  range: diagnostic.range,
                }],
              )])),
              ..Default::default()
            }),
            ..Default::default()
          }
        }
        _ => {
          return Err(anyhow!(
            "Unsupported diagnostic code (\"{}\") provided.",
            code
          ));
        }
      };
      Ok(code_action)
    } else {
      Err(anyhow!("Unsupported diagnostic code provided."))
    }
  }

  /// Given a reference to the code from an LSP diagnostic, determine if the
  /// diagnostic is fixable or not
  pub fn is_fixable(diagnostic: &lsp_types::Diagnostic) -> bool {
    if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code {
      match code.as_str() {
        "import-map-remap"
        | "no-cache"
        | "not-installed-jsr"
        | "not-installed-npm"
        | "no-attribute-type"
        | "redirect"
        | "import-node-prefix-missing" => true,
        "no-local" => diagnostic.data.is_some(),
        _ => false,
      }
    } else {
      false
    }
  }

  /// Convert to an lsp Diagnostic when the range the diagnostic applies to is
  /// provided.
  pub fn to_lsp_diagnostic(&self, range: &lsp::Range) -> lsp::Diagnostic {
    fn no_local_message(
      specifier: &ModuleSpecifier,
      suggestion_message: Option<String>,
    ) -> String {
      let mut message = format!(
        "Unable to load a local module: {}\n",
        to_percent_decoded_str(specifier.as_ref())
      );
      if let Some(suggestion_message) = suggestion_message {
        message.push_str(&suggestion_message);
        message.push('.');
      } else {
        message.push_str("Please check the file path.");
      }
      message
    }

    let (severity, message, data) = match self {
      Self::DenoWarn(message) => (lsp::DiagnosticSeverity::WARNING, message.to_string(), None),
      Self::ImportMapDiagnostic(message) => (lsp::DiagnosticSeverity::ERROR, message.to_string(), None),
      Self::ImportMapRemap { from, to } => (lsp::DiagnosticSeverity::HINT, format!("The import specifier can be remapped to \"{to}\" which will resolve it via the active import map."), Some(json!({ "from": from, "to": to }))),
      Self::InvalidAttributeType(assert_type) => (lsp::DiagnosticSeverity::ERROR, format!("The module is a JSON module and expected an attribute type of \"json\". Instead got \"{assert_type}\"."), None),
      Self::NoAttributeType => (lsp::DiagnosticSeverity::ERROR, "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement.".to_string(), None),
      Self::NoCache(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing remote URL: {specifier}"), Some(json!({ "specifier": specifier }))),
      Self::NotInstalledJsr(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("JSR package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))),
      Self::NotInstalledNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("npm package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))),
      Self::NoExportNpm(pkg_ref) => (lsp::DiagnosticSeverity::ERROR, format!("NPM package \"{}\" does not define an export \"{}\".", pkg_ref.req(), pkg_ref.sub_path().unwrap_or(".")), None),
      Self::NoLocal(specifier) => {
        let sloppy_resolution = sloppy_imports_resolve(specifier, deno_resolver::workspace::ResolutionKind::Execution, CliSys::default());
        let data = sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| {
          json!({
            "specifier": specifier,
            "to": resolved,
            "message": sloppy_reason.quick_fix_message_for_specifier(resolved),
          })
        });
        (lsp::DiagnosticSeverity::ERROR, no_local_message(specifier, sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| sloppy_reason.suggestion_message_for_specifier(resolved))), data)
      },
      Self::ResolutionError(err, bare_importable_pkg_names) => {
        let message = strip_ansi_codes(&enhanced_resolution_error_message(err, bare_importable_pkg_names)).into_owned();
        (
        lsp::DiagnosticSeverity::ERROR,
        message,
        deno_resolver::graph::get_resolution_error_bare_node_specifier(err)
          .map(|specifier| json!({ "specifier": specifier }))
      )},
      Self::UnknownNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("No such built-in module: node:{}", specifier.path()), None),
      Self::BareNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::WARNING, format!("\"{0}\" is resolved to \"node:{0}\". If you want to use a built-in Node module, add a \"node:\" prefix.", specifier), Some(json!({ "specifier": specifier }))),
    };
    lsp::Diagnostic {
      range: *range,
      severity: Some(severity),
      code: Some(lsp::NumberOrString::String(self.code().to_string())),
      source: Some(DiagnosticSource::Deno.as_lsp_source().to_string()),
      message,
      data,
      ..Default::default()
    }
  }
}

fn specifier_text_for_redirected(redirect: &Url, referrer: &Url) -> String {
  if redirect.scheme() == "file" && referrer.scheme() == "file" {
    // use a relative specifier when it's going to a file url
    relative_specifier(redirect, referrer)
  } else {
    redirect.to_string()
  }
}

fn relative_specifier(specifier: &Url, referrer: &Url) -> String {
  match referrer.make_relative(specifier) {
    Some(relative) => {
      if relative.starts_with('.') {
        relative
      } else {
        format!("./{}", relative)
      }
    }
    None => specifier.to_string(),
  }
}

fn import_map_resolves_dependency_key(
  import_map: &ImportMap,
  dependency_key: &str,
  resolved: &Url,
  referrer: &Url,
) -> bool {
  for entry in import_map.entries_for_referrer(referrer) {
    let Some(address) = entry.value else {
      continue;
    };
    let raw_key = entry.raw_key;
    let is_match = dependency_key == raw_key
      || (raw_key.ends_with('/') && dependency_key.starts_with(raw_key));
    if is_match
      && let Ok(specifier) = import_map.resolve(dependency_key, referrer)
      && specifier == *resolved
      && (dependency_key == raw_key || address.as_str().ends_with('/'))
    {
      return true;
    }
  }
  false
}

fn maybe_ambient_import_specifier(
  diagnostic: &DenoDiagnostic,
) -> Option<String> {
  match diagnostic {
    DenoDiagnostic::NoCache(url) | DenoDiagnostic::NoLocal(url) => {
      Some(url.to_string())
    }
    DenoDiagnostic::ResolutionError(err, _) => {
      maybe_ambient_specifier_resolution_err(err)
    }
    _ => None,
  }
}

fn maybe_ambient_specifier_resolution_err(
  err: &ResolutionError,
) -> Option<String> {
  match err {
    ResolutionError::InvalidDowngrade { .. }
    | ResolutionError::InvalidJsrHttpsTypesImport { .. }
    | ResolutionError::InvalidLocalImport { .. } => None,
    ResolutionError::InvalidSpecifier { error, .. } => match error {
      SpecifierError::InvalidUrl(..) => None,
      SpecifierError::ImportPrefixMissing { specifier, .. } => {
        Some(specifier.to_string())
      }
    },
    ResolutionError::ResolverError { error, .. } => match &**error {
      ResolveError::Specifier(specifier_error) => match specifier_error {
        SpecifierError::InvalidUrl(..) => None,
        SpecifierError::ImportPrefixMissing { specifier, .. } => {
          Some(specifier.to_string())
        }
      },
      ResolveError::Other(err) => {
        let import_map_error =
          err.get_ref().downcast_ref::<import_map::ImportMapError>()?;
        match import_map_error.as_kind() {
          ImportMapErrorKind::UnmappedBareSpecifier(spec, _) => {
            Some(spec.clone())
          }
          ImportMapErrorKind::JsonParse(_)
          | ImportMapErrorKind::ImportMapNotObject
          | ImportMapErrorKind::ImportsFieldNotObject
          | ImportMapErrorKind::ScopesFieldNotObject
          | ImportMapErrorKind::ScopePrefixNotObject(_)
          | ImportMapErrorKind::BlockedByNullEntry(_)
          | ImportMapErrorKind::SpecifierResolutionFailure { .. }
          | ImportMapErrorKind::SpecifierBacktracksAbovePrefix { .. } => None,
        }
      }
    },
  }
}

#[allow(clippy::too_many_arguments, reason = "private code")]
fn diagnose_resolution(
  snapshot: &language_server::StateSnapshot,
  dependency_key: &str,
  resolution: &Resolution,
  is_dynamic: bool,
  maybe_assert_type: Option<&str>,
  referrer_module: &DocumentModule,
  import_map: Option<&ImportMap>,
) -> (Vec<DenoDiagnostic>, Vec<DenoDiagnostic>) {
  let mut diagnostics = vec![];
  let mut deferred_diagnostics = vec![];
  match resolution {
    Resolution::Ok(resolved) => {
      let specifier = &resolved.specifier;
      let scoped_resolver = snapshot
        .resolver
        .get_scoped_resolver(referrer_module.scope.as_deref());
      let managed_npm_resolver =
        scoped_resolver.as_maybe_managed_npm_resolver();
      for (_, headers) in scoped_resolver.redirect_chain_headers(specifier) {
        if let Some(message) = headers.get("x-deno-warning") {
          diagnostics.push(DenoDiagnostic::DenoWarn(message.clone()));
        }
      }
      match snapshot.document_modules.module_for_specifier(
        specifier,
        referrer_module.scope.as_deref(),
        Some(&referrer_module.compiler_options_key),
      ) {
        Some(module) => {
          if let Some(headers) = &module.headers
            && let Some(message) = headers.get("x-deno-warning")
          {
            diagnostics.push(DenoDiagnostic::DenoWarn(message.clone()));
          }
          if module.media_type == MediaType::Json {
            match maybe_assert_type {
              // The module has the correct assertion type, no diagnostic
              Some("json" | "text" | "bytes") => (),
              // The dynamic import statement is missing an attribute type, which
              // we might not be able to statically detect, therefore we will
              // not provide a potentially incorrect diagnostic.
              None if is_dynamic => (),
              // The module has an incorrect assertion type, diagnostic
              Some(assert_type) => diagnostics.push(
                DenoDiagnostic::InvalidAttributeType(assert_type.to_string()),
              ),
              // The module is missing an attribute type, diagnostic
              None => diagnostics.push(DenoDiagnostic::NoAttributeType),
            }
          }
        }
        _ => {
          match JsrPackageReqReference::from_specifier(specifier) {
            Ok(pkg_ref) => {
              let req = pkg_ref.into_inner().req;
              diagnostics
                .push(DenoDiagnostic::NotInstalledJsr(req, specifier.clone()));
            }
            _ => {
              match NpmPackageReqReference::from_specifier(specifier) {
                Ok(pkg_ref) => {
                  if let Some(npm_resolver) = managed_npm_resolver {
                    // show diagnostics for npm package references that aren't cached
                    let req = pkg_ref.req();
                    if !npm_resolver.is_pkg_req_folder_cached(req) {
                      diagnostics.push(DenoDiagnostic::NotInstalledNpm(
                        req.clone(),
                        specifier.clone(),
                      ));
                    } else {
                      let resolution_kinds = [
                        NodeResolutionKind::Types,
                        NodeResolutionKind::Execution,
                      ];
                      if resolution_kinds.into_iter().all(|k| {
                        scoped_resolver
                          .npm_to_file_url(
                            &pkg_ref,
                            &referrer_module.specifier,
                            k,
                            referrer_module.resolution_mode,
                          )
                          .is_none()
                      }) {
                        diagnostics
                          .push(DenoDiagnostic::NoExportNpm(pkg_ref.clone()));
                      }
                    }
                  }
                }
                _ => {
                  if let Some(module_name) =
                    specifier.as_str().strip_prefix("node:")
                  {
                    if !deno_node::is_builtin_node_module(module_name) {
                      diagnostics.push(DenoDiagnostic::UnknownNodeSpecifier(
                        specifier.clone(),
                      ));
                    } else if module_name == dependency_key {
                      let mut is_mapped = false;
                      if let Some(import_map) = import_map
                        && let Resolution::Ok(resolved) = &resolution
                        && import_map
                          .resolve(module_name, &resolved.specifier)
                          .is_ok()
                      {
                        is_mapped = true;
                      }
                      // show diagnostics for bare node specifiers that aren't mapped by import map
                      if !is_mapped {
                        diagnostics.push(DenoDiagnostic::BareNodeSpecifier(
                          module_name.to_string(),
                        ));
                      }
                    }
                  } else {
                    // When the document is not available, it means that it cannot be found
                    // in the cache or locally on the disk, so we want to issue a diagnostic
                    // about that.
                    // these may be invalid, however, if this is an ambient module with
                    // no real source (as in the case of a virtual module).
                    let deno_diagnostic = match specifier.scheme() {
                      "file" => DenoDiagnostic::NoLocal(specifier.clone()),
                      _ => DenoDiagnostic::NoCache(specifier.clone()),
                    };
                    deferred_diagnostics.push(deno_diagnostic);
                  }
                }
              }
            }
          }
        }
      }
    }
    // The specifier resolution resulted in an error, so we want to issue a
    // diagnostic for that.
    Resolution::Err(err) => {
      // Names of packages importable by bare specifier (workspace members and
      // packages linked via the "links" field) in the referrer's scope, used
      // to enhance the error with a better hint.
      let bare_importable_pkg_names = snapshot
        .config
        .tree
        .data_for_specifier(&referrer_module.specifier)
        .map(|d| {
          d.member_dir
            .workspace
            .resolver_jsr_pkgs()
            .map(|pkg| pkg.name)
            .collect::<Vec<_>>()
        })
        .unwrap_or_default();
      if maybe_ambient_specifier_resolution_err(err).is_none() {
        diagnostics.push(DenoDiagnostic::ResolutionError(
          *err.clone(),
          bare_importable_pkg_names,
        ))
      } else {
        deferred_diagnostics.push(DenoDiagnostic::ResolutionError(
          *err.clone(),
          bare_importable_pkg_names,
        ));
      }
    }
    _ => (),
  }
  (diagnostics, deferred_diagnostics)
}

/// Surface an unresolved import of a `.d.ts` entrypoint as a resolution error.
///
/// A `.d.ts` document is itself an entrypoint in the editor, so its own
/// unresolved imports should be reported (e.g. `TS2307` from `deno check`).
/// However deno_graph records a bare specifier in a `.d.ts` as
/// `Resolution::None` (a `.ts` file records `Resolution::Err`), which
/// `diagnose_resolution()` ignores, and tsc skips them under `skipLibCheck`. So
/// handle them explicitly here, mirroring `cli/type_checker.rs`.
fn diagnose_dts_entrypoint_unresolved_import(
  diagnostics: &mut Vec<lsp::Diagnostic>,
  snapshot: &language_server::StateSnapshot,
  referrer_module: &DocumentModule,
  dependency: &deno_graph::Dependency,
) {
  // Only handle imports deno_graph left fully unresolved; a bare specifier in a
  // `.d.ts` lands here as `None`/`None`. Resolved (`Ok`) and errored (`Err`)
  // deps are already surfaced by `diagnose_resolution()`, so handling them here
  // too would double report.
  if !matches!(dependency.maybe_code, Resolution::None)
    || !matches!(dependency.maybe_type, Resolution::None)
  {
    return;
  }
  // Names of packages importable by bare specifier (workspace members and
  // packages linked via the "links" field) in the referrer's scope, used to
  // enhance the error with a better hint.
  let bare_importable_pkg_names = snapshot
    .config
    .tree
    .data_for_specifier(&referrer_module.specifier)
    .map(|d| {
      d.member_dir
        .workspace
        .resolver_jsr_pkgs()
        .map(|pkg| pkg.name)
        .collect::<Vec<_>>()
    })
    .unwrap_or_default();
  for import in &dependency.imports {
    // Only surface real `import`/`export`/`import =` statements. Triple slash
    // `/// <reference />` directives, JSDoc and `@jsxImportSource` imports are
    // intentionally left to tsc's `skipLibCheck` handling.
    if !matches!(
      import.kind,
      deno_graph::ImportKind::Es
        | deno_graph::ImportKind::TsType
        | deno_graph::ImportKind::Require
    ) {
      continue;
    }
    // A bare specifier in a `.d.ts` lands here as fully unresolved, so building
    // the resolution error reuses the same message and quick fixes as a `.ts`
    // file's `Resolution::Err`.
    if let Err(error) = deno_path_util::resolve_import(
      &import.specifier,
      &referrer_module.specifier,
    ) {
      let resolution_error = ResolutionError::InvalidSpecifier {
        error,
        range: import.specifier_range.clone(),
      };
      diagnostics.push(
        DenoDiagnostic::ResolutionError(
          resolution_error,
          bare_importable_pkg_names.clone(),
        )
        .to_lsp_diagnostic(&language_server::to_lsp_range(
          &import.specifier_range,
        )),
      );
    }
  }
}

/// Generate diagnostics related to a dependency. The dependency is analyzed to
/// determine if it can be remapped to the active import map as well as surface
/// any diagnostics related to the resolved code or type dependency.
fn diagnose_dependency(
  diagnostics: &mut Vec<lsp::Diagnostic>,
  deferred_diagnostics: &mut Vec<(String, lsp::Diagnostic)>,
  snapshot: &language_server::StateSnapshot,
  referrer_module: &DocumentModule,
  dependency_key: &str,
  dependency: &deno_graph::Dependency,
) {
  if snapshot
    .resolver
    .in_node_modules(&referrer_module.specifier)
  {
    return; // ignore, surface typescript errors instead
  }

  if referrer_module.media_type.is_declaration() {
    // A `.d.ts` document opened in the editor is itself an entrypoint, so its
    // own unresolved imports should be surfaced (matching `deno check`, which
    // reports `TS2307` for them). This must happen regardless of `skipLibCheck`
    // and before the early return below, mirroring `cli/type_checker.rs`.
    diagnose_dts_entrypoint_unresolved_import(
      diagnostics,
      snapshot,
      referrer_module,
      dependency,
    );

    let compiler_options_data = snapshot
      .compiler_options_resolver
      .for_key(&referrer_module.compiler_options_key);
    if compiler_options_data.is_none() {
      lsp_warn!(
        "Key was not in sync with resolver while checking `skipLibCheck`. This should be impossible."
      );
      #[cfg(debug_assertions)]
      unreachable!();
    }
    if compiler_options_data.is_some_and(|d| d.skip_lib_check) {
      return;
    }
  }

  let import_map = snapshot
    .resolver
    .get_scoped_resolver(referrer_module.scope.as_deref())
    .as_workspace_resolver()
    .maybe_import_map();
  if let Some(import_map) = import_map {
    let resolved = dependency
      .maybe_code
      .ok()
      .or_else(|| dependency.maybe_type.ok());
    if let Some(resolved) = resolved
      && let Some(to) = import_map_lookup_for_diagnostic(
        import_map,
        &resolved.specifier,
        &referrer_module.specifier,
      )
      && dependency_key != to
      && !import_map_resolves_dependency_key(
        import_map,
        dependency_key,
        &resolved.specifier,
        &referrer_module.specifier,
      )
    {
      diagnostics.push(
        DenoDiagnostic::ImportMapRemap {
          from: dependency_key.to_string(),
          to,
        }
        .to_lsp_diagnostic(&language_server::to_lsp_range(&resolved.range)),
      );
    }
  }

  let import_ranges: Vec<_> = dependency
    .imports
    .iter()
    .map(|i| language_server::to_lsp_range(&i.specifier_range))
    .collect();
  // TODO(nayeemrmn): This is a crude way of detecting `@ts-types` which has
  // a different specifier and therefore needs a separate call to
  // `diagnose_resolution()`. It would be much cleaner if that were modelled as
  // a separate dependency: https://github.com/denoland/deno_graph/issues/247.
  let is_types_deno_types = !dependency.maybe_type.is_none()
    && !dependency.imports.iter().any(|i| {
      dependency
        .maybe_type
        .includes(i.specifier_range.range.start)
        .is_some()
    });
  let resolution = if dependency.maybe_code.is_none()
  // If not @ts-types, diagnose the types if the code errored because
  // it's likely resolving into the node_modules folder, which might be
  // erroring correctly due to resolution only being for bundlers. Let this
  // fail at runtime if necessary, but don't bother erroring in the editor
  || !is_types_deno_types && matches!(dependency.maybe_type, Resolution::Ok(_))
    && matches!(dependency.maybe_code, Resolution::Err(_))
  {
    &dependency.maybe_type
  } else {
    &dependency.maybe_code
  };
  let (resolution_diagnostics, deferred) = diagnose_resolution(
    snapshot,
    dependency_key,
    resolution,
    dependency.is_dynamic,
    dependency.maybe_attribute_type.as_deref(),
    referrer_module,
    import_map,
  );
  diagnostics.extend(resolution_diagnostics.iter().flat_map(|diag| {
    import_ranges
      .iter()
      .map(|range| diag.to_lsp_diagnostic(range))
  }));
  deferred_diagnostics.extend(
    deferred
      .iter()
      .filter_map(|diag| {
        maybe_ambient_import_specifier(diag).map(|spec| {
          import_ranges
            .iter()
            .map(move |range| (spec.clone(), diag.to_lsp_diagnostic(range)))
        })
      })
      .flatten(),
  );

  if is_types_deno_types {
    let range = match &dependency.maybe_type {
      Resolution::Ok(resolved) => {
        language_server::to_lsp_range(&resolved.range)
      }
      Resolution::Err(error) => language_server::to_lsp_range(error.range()),
      Resolution::None => unreachable!(),
    };
    let (resolution_diagnostics, deferred) = diagnose_resolution(
      snapshot,
      dependency_key,
      &dependency.maybe_type,
      dependency.is_dynamic,
      dependency.maybe_attribute_type.as_deref(),
      referrer_module,
      import_map,
    );
    diagnostics.extend(
      resolution_diagnostics
        .iter()
        .map(|diag| diag.to_lsp_diagnostic(&range)),
    );
    deferred_diagnostics.extend(Box::new(deferred.iter().filter_map(|diag| {
      maybe_ambient_import_specifier(diag)
        .map(|spec| (spec, diag.to_lsp_diagnostic(&range)))
    })));
  }
}

fn import_map_diagnostic_range(
  document: &OpenDocument,
  text: &str,
  maybe_text: Option<&str>,
) -> lsp::Range {
  if let Some(text_to_find) = maybe_text
    && let Some(index) = text.find(text_to_find)
  {
    let start = TextSize::from(index as u32);
    let end = TextSize::from((index + text_to_find.len()) as u32);
    return lsp::Range {
      start: document.line_index.position_utf16(start),
      end: document.line_index.position_utf16(end),
    };
  }
  lsp::Range::default()
}

fn json_error_range(err: &serde_json::Error) -> lsp::Range {
  let line = err.line().saturating_sub(1) as u32;
  let character = err.column().saturating_sub(1) as u32;
  lsp::Range {
    start: lsp::Position { line, character },
    end: lsp::Position { line, character },
  }
}

fn import_map_parse_diagnostic_to_lsp(
  diagnostic: &ImportMapDiagnostic,
) -> lsp::Diagnostic {
  DenoDiagnostic::ImportMapDiagnostic(diagnostic.to_string())
    .to_lsp_diagnostic(&lsp::Range::default())
}

fn diagnose_import_map_entry_value(
  diagnostics: &mut Vec<lsp::Diagnostic>,
  document: &OpenDocument,
  text: &str,
  scope: &ModuleSpecifier,
  snapshot: &StateSnapshot,
  raw_value: Option<&str>,
  value: Option<&ModuleSpecifier>,
) {
  let Some(value) = value else {
    return;
  };
  let diagnostic = match value.scheme() {
    "http" | "https" => {
      let http_cache = snapshot.cache.for_specifier(Some(scope));
      if http_cache.contains(value) {
        return;
      }
      DenoDiagnostic::NoCache(value.clone())
    }
    "jsr" => {
      let Ok(pkg_ref) = JsrPackageReqReference::from_specifier(value) else {
        return;
      };
      let scoped_resolver = snapshot.resolver.get_scoped_resolver(Some(scope));
      if scoped_resolver.jsr_to_resource_url(&pkg_ref).is_some() {
        return;
      }
      let req = pkg_ref.into_inner().req;
      DenoDiagnostic::NotInstalledJsr(req, value.clone())
    }
    "npm" => {
      let Ok(pkg_ref) = NpmPackageReqReference::from_specifier(value) else {
        return;
      };
      let req = pkg_ref.req().clone();
      let scoped_resolver = snapshot.resolver.get_scoped_resolver(Some(scope));
      if scoped_resolver
        .as_maybe_managed_npm_resolver()
        .is_some_and(|npm_resolver| npm_resolver.is_pkg_req_folder_cached(&req))
      {
        return;
      }
      DenoDiagnostic::NotInstalledNpm(req, value.clone())
    }
    _ => return,
  };
  let range = import_map_diagnostic_range(document, text, raw_value);
  diagnostics.push(diagnostic.to_lsp_diagnostic(&range));
}

pub(crate) fn generate_import_map_diagnostics(
  document: &Arc<OpenDocument>,
  snapshot: &StateSnapshot,
) -> Vec<lsp::Diagnostic> {
  let specifier = uri_to_url(&document.uri);
  let Some((scope, ConfigWatchedFileType::ImportMap)) =
    snapshot.config.tree.watched_file_type(&specifier)
  else {
    return Vec::new();
  };
  let text = document.text.as_ref();
  let parse_result = import_map::parse_from_json(specifier.clone(), text);
  match parse_result {
    Ok(import_map) => {
      let mut diagnostics = import_map
        .diagnostics
        .iter()
        .map(import_map_parse_diagnostic_to_lsp)
        .collect::<Vec<_>>();
      for entry in import_map.import_map.imports().entries() {
        diagnose_import_map_entry_value(
          &mut diagnostics,
          document,
          text,
          scope,
          snapshot,
          entry.raw_value,
          entry.value,
        );
      }
      for scope_entry in import_map.import_map.scopes() {
        for entry in scope_entry.imports.entries() {
          diagnose_import_map_entry_value(
            &mut diagnostics,
            document,
            text,
            scope,
            snapshot,
            entry.raw_value,
            entry.value,
          );
        }
      }
      diagnostics
    }
    Err(err) => match err.as_kind() {
      ImportMapErrorKind::JsonParse(err) => vec![
        DenoDiagnostic::ImportMapDiagnostic(format!(
          "Unable to parse import map JSON: {err}"
        ))
        .to_lsp_diagnostic(&json_error_range(err)),
      ],
      _ => vec![
        DenoDiagnostic::ImportMapDiagnostic(err.to_string())
          .to_lsp_diagnostic(&lsp::Range::default()),
      ],
    },
  }
}

async fn publish_document_diagnostics(
  document: &Arc<OpenDocument>,
  diagnostics: Vec<lsp::Diagnostic>,
  client: &Client,
  state: &DiagnosticsState,
  _token: &CancellationToken,
) {
  state.update(&document.uri, document.version, &diagnostics);
  client
    .publish_diagnostics(
      document.uri.as_ref().clone(),
      diagnostics,
      Some(document.version),
    )
    .await;
}

async fn generate_document_diagnostics(
  document: &Arc<OpenDocument>,
  snapshot: &Arc<StateSnapshot>,
  ts_server: &TsServer,
  ambient_modules_regex_cache: &OnceCellMap<
    (CompilerOptionsKey, Option<Arc<Uri>>),
    Option<regex::Regex>,
  >,
  no_slow_types_cache: &NoSlowTypesDiagnosticsCache,
  token: &CancellationToken,
) -> Result<Vec<lsp::Diagnostic>, AnyError> {
  let mut import_map_diagnostics =
    generate_import_map_diagnostics(document, snapshot);
  if !document.is_diagnosable() {
    return Ok(import_map_diagnostics);
  }
  let document_ = Document::Open(document.clone());
  let module = match snapshot.document_modules.primary_module(&document_) {
    Some(module) => module,
    None => {
      let url = uri_to_url(document_.uri());
      if url.scheme() == "file"
        && !snapshot.resolver.in_node_modules(&url)
        && !snapshot.cache.in_cache_directory(&url)
      {
        return Ok(Vec::new());
      }
      // If this document represents a non-local module, the module may not be
      // retrievable until its referrer is known through some other request.
      // Wait and try one more time.
      tokio::time::sleep(std::time::Duration::from_millis(200)).await;
      let Some(module) = snapshot.document_modules.primary_module(&document_)
      else {
        return Ok(Vec::new());
      };
      module
    }
  };
  if !snapshot.config.specifier_enabled(&module.specifier) {
    return Ok(Vec::new());
  }
  generate_module_diagnostics(
    &module,
    snapshot,
    ts_server,
    ambient_modules_regex_cache,
    no_slow_types_cache,
    token,
  )
  .await
  .map(|mut diagnostics| {
    import_map_diagnostics.append(&mut diagnostics);
    import_map_diagnostics
  })
}

pub async fn generate_module_diagnostics(
  module: &Arc<DocumentModule>,
  snapshot: &Arc<StateSnapshot>,
  ts_server: &TsServer,
  ambient_modules_regex_cache: &OnceCellMap<
    (CompilerOptionsKey, Option<Arc<Uri>>),
    Option<regex::Regex>,
  >,
  no_slow_types_cache: &NoSlowTypesDiagnosticsCache,
  token: &CancellationToken,
) -> Result<Vec<lsp::Diagnostic>, AnyError> {
  let deps_handle = tokio::task::spawn_blocking({
    let snapshot = snapshot.clone();
    let module = module.clone();
    let token = token.clone();
    move || {
      let mut diagnostics = Vec::new();
      let mut deferred = Vec::new();
      for (dependency_key, dependency) in module.dependencies.iter() {
        if token.is_cancelled() {
          return (Vec::new(), Vec::new());
        }
        diagnose_dependency(
          &mut diagnostics,
          &mut deferred,
          &snapshot,
          &module,
          dependency_key,
          dependency,
        );
      }
      (diagnostics, deferred)
    }
  });

  let lint_handle = tokio::task::spawn_blocking({
    let snapshot = snapshot.clone();
    let module = module.clone();
    let token = token.clone();
    move || -> Arc<Vec<lsp::Diagnostic>> {
      // TODO(nayeemrmn): Support linting notebooks cells. Will require
      // stitching cells from the same notebook into one module, linting it
      // and then splitting/relocating the diagnostics to each cell.
      if token.is_cancelled()
        || module.notebook_uri.is_some()
        || module.specifier.scheme() != "file"
        || snapshot.resolver.in_node_modules(&module.specifier)
      {
        return Default::default();
      }
      let settings = snapshot
        .config
        .workspace_settings_for_specifier(&module.specifier);
      if !settings.lint {
        return Default::default();
      }
      let linter = snapshot.linter_resolver.for_module(&module);
      generate_document_lint_diagnostics(&module, &linter, token)
        .unwrap_or_default()
    }
  });

  let doc_handle = tokio::task::spawn_blocking({
    let snapshot = snapshot.clone();
    let module = module.clone();
    let token = token.clone();
    move || -> Arc<Vec<lsp::Diagnostic>> {
      if token.is_cancelled()
        || module.notebook_uri.is_some()
        || module.specifier.scheme() != "file"
        || snapshot.resolver.in_node_modules(&module.specifier)
      {
        return Default::default();
      }
      let settings = snapshot
        .config
        .workspace_settings_for_specifier(&module.specifier);
      if !settings.lint {
        return Default::default();
      }
      if !module_is_package_export(&snapshot, &module) {
        return Default::default();
      }
      generate_document_doc_diagnostics(&module, &token).unwrap_or_default()
    }
  });

  let mut diagnostics = ts_server
    .provide_diagnostics(module, snapshot.clone(), token)
    .await?;

  let (deps_diagnostics, deferred_deps_diagnostics) = deps_handle
    .await
    .inspect_err(|err| {
      lsp_warn!("Deps diagnostics task join error: {err:#}");
    })
    .unwrap_or_default();
  diagnostics.extend(deps_diagnostics);
  let ambient_modules_regex_cell = ambient_modules_regex_cache
    .entry((
      module.compiler_options_key.clone(),
      module.notebook_uri.clone(),
    ))
    .or_default()
    .clone();
  let ambient_modules_regex = ambient_modules_regex_cell
    .get_or_init(async || {
      ts_server
        .get_ambient_modules(
          &module.compiler_options_key,
          module.notebook_uri.as_ref(),
          snapshot.clone(),
          token,
        )
        .await
        .inspect_err(|err| {
          if !token.is_cancelled() {
            lsp_warn!("Unable to get ambient modules: {:#}", err);
          }
        })
        .ok()
        .filter(|a| !a.is_empty())
        .and_then(|ambient_modules| {
          let regex_string = ambient_modules_to_regex_string(&ambient_modules);
          regex::Regex::new(&regex_string).inspect_err(|err| {
            lsp_warn!("Failed to compile ambient modules pattern: {err:#} (pattern is {regex_string:?})");
          }).ok()
        })
    }).await;
  if let Some(ambient_modules_regex) = ambient_modules_regex {
    diagnostics.extend(deferred_deps_diagnostics.into_iter().filter_map(
      |(import_url, diag)| {
        if ambient_modules_regex.is_match(import_url.as_str()) {
          return None;
        }
        Some(diag)
      },
    ));
  } else {
    diagnostics.extend(deferred_deps_diagnostics.into_iter().map(|(_, d)| d));
  }

  let lint_diagnostics = lint_handle
    .await
    .inspect_err(|err| {
      lsp_warn!("Lint task join error: {err:#}");
    })
    .unwrap_or_default();
  diagnostics.extend(lint_diagnostics.iter().cloned());

  let doc_diagnostics = doc_handle
    .await
    .inspect_err(|err| {
      lsp_warn!("Doc lint task join error: {err:#}");
    })
    .unwrap_or_default();
  diagnostics.extend(doc_diagnostics.iter().cloned());

  diagnostics.extend(
    generate_no_slow_types_diagnostics(module, snapshot, no_slow_types_cache)
      .await,
  );

  Ok(diagnostics)
}

/// Returns the `no-slow-types` (fast check) diagnostics that land on `module`.
///
/// These diagnostics are reported by `deno publish` / `deno lint` for
/// publishable JSR packages and require a fully built fast check module graph
/// of the whole package. Because a single slow type can produce diagnostics on
/// any module reachable from a package's exports, the graph is built once per
/// package scope (cached in `no_slow_types_cache`) and the resulting
/// diagnostics are distributed to the document they point at.
async fn generate_no_slow_types_diagnostics(
  module: &Arc<DocumentModule>,
  snapshot: &Arc<StateSnapshot>,
  no_slow_types_cache: &NoSlowTypesDiagnosticsCache,
) -> Vec<lsp::Diagnostic> {
  if module.specifier.scheme() != "file"
    || module.notebook_uri.is_some()
    || snapshot.resolver.in_node_modules(&module.specifier)
  {
    return Vec::new();
  }
  let settings = snapshot
    .config
    .workspace_settings_for_specifier(&module.specifier);
  if !settings.lint {
    return Vec::new();
  }
  let Some(config_data) =
    snapshot.config.tree.data_for_specifier(&module.specifier)
  else {
    return Vec::new();
  };
  // Only publishable JSR packages have a public API to check for slow types.
  if config_data.member_dir.jsr_packages_for_publish().is_empty() {
    return Vec::new();
  }
  // Respect the lint configuration: if `no-slow-types` is excluded (or not
  // otherwise enabled) for this document, don't report its diagnostics. This
  // keeps the LSP in sync with `deno lint`, which filters the rule out.
  let linter = snapshot.linter_resolver.for_module(module);
  if !linter.inner.has_package_rule("no-slow-types") {
    return Vec::new();
  }
  let scope = config_data.scope.clone();
  let cell = no_slow_types_cache
    .entry(scope.clone())
    .or_default()
    .clone();
  let diagnostics_by_specifier = cell
    .get_or_init(|| {
      let snapshot = snapshot.clone();
      async move {
        Arc::new(
          tokio::task::spawn_blocking(move || {
            collect_scope_no_slow_types_diagnostics(&snapshot, &scope)
          })
          .await
          .inspect_err(|err| {
            lsp_warn!("No-slow-types diagnostics task join error: {err:#}");
          })
          .unwrap_or_default(),
        )
      }
    })
    .await;
  diagnostics_by_specifier
    .get(&module.specifier)
    .cloned()
    .unwrap_or_default()
}

/// Builds the fast check graph for the publishable packages within `scope` and
/// collects the resulting `no-slow-types` diagnostics, grouped by the specifier
/// of the module each one points at. Runs on a blocking thread since building
/// the graph involves I/O and uses `!Send` types internally.
fn collect_scope_no_slow_types_diagnostics(
  snapshot: &Arc<StateSnapshot>,
  scope: &ModuleSpecifier,
) -> HashMap<ModuleSpecifier, Vec<lsp::Diagnostic>> {
  let result = (|| -> Result<_, AnyError> {
    let Some(config_data) = snapshot.config.tree.data_for_specifier(scope)
    else {
      return Ok(HashMap::new());
    };
    let packages = config_data.member_dir.jsr_packages_for_publish();
    if packages.is_empty() {
      return Ok(HashMap::new());
    }
    let workspace_settings =
      snapshot.config.workspace_settings_for_specifier(scope);
    let mut factory = CliFactory::from_flags(Arc::new(Flags {
      internal: InternalFlags {
        cache_path: Some(snapshot.cache.deno_dir().root.clone()),
        lockfile_skip_write: true,
        ..Default::default()
      },
      ca_stores: workspace_settings.certificate_stores.clone(),
      ca_data: workspace_settings.tls_certificate.clone().map(CaData::File),
      unsafely_ignore_certificate_errors: workspace_settings
        .unsafely_ignore_certificate_errors
        .clone(),
      type_check_mode: TypeCheckMode::None,
      permissions: PermissionFlags {
        // allow remote import permissions in the lsp for now
        allow_import: Some(vec![]),
        ..Default::default()
      },
      ..Default::default()
    }));
    factory.set_initial_cwd(config_data.member_dir.dir_path());
    factory.set_workspace_dir(config_data.member_dir.clone());

    let runtime = create_basic_runtime();
    let local_set = tokio::task::LocalSet::new();
    local_set.block_on(&runtime, async move {
      let module_graph_creator = factory.module_graph_creator().await?;
      let graph = module_graph_creator
        .create_publish_graph(CreatePublishGraphOptions {
          packages: &packages,
          build_fast_check_graph: true,
          validate_graph: false,
          skip_unanalyzable_exports: true,
        })
        .await?;
      let mut diagnostics_by_specifier: HashMap<
        ModuleSpecifier,
        Vec<lsp::Diagnostic>,
      > = HashMap::new();
      for package in &packages {
        let export_urls = package.config_file.resolve_export_value_urls()?;
        for diagnostic in collect_no_slow_type_diagnostics(&graph, &export_urls)
        {
          if let Some(lsp_diagnostic) =
            no_slow_types_diagnostic_to_lsp(&diagnostic)
          {
            diagnostics_by_specifier
              .entry(diagnostic.specifier().clone())
              .or_default()
              .push(lsp_diagnostic);
          }
        }
      }
      Ok(diagnostics_by_specifier)
    })
  })();
  result.unwrap_or_else(|err: AnyError| {
    lsp_warn!("Failed to generate no-slow-types diagnostics: {err:#}");
    HashMap::new()
  })
}

fn no_slow_types_diagnostic_to_lsp(
  diagnostic: &FastCheckDiagnostic,
) -> Option<lsp::Diagnostic> {
  let range = diagnostic.range()?;
  let mut message = diagnostic.message().to_string();
  if let Some(hint) = diagnostic.hint() {
    message.push_str("\n\n");
    message.push_str(&hint);
  }
  Some(lsp::Diagnostic {
    range: analysis::source_range_to_lsp_range(&range.range, &range.text_info),
    severity: Some(lsp::DiagnosticSeverity::WARNING),
    code: Some(lsp::NumberOrString::String("no-slow-types".to_string())),
    code_description: diagnostic
      .docs_url()
      .and_then(|url| Uri::from_str(&url).ok())
      .map(|href| lsp::CodeDescription { href }),
    source: Some(DiagnosticSource::Lint.as_lsp_source().to_string()),
    message,
    related_information: None,
    tags: None,
    data: None,
  })
}

/// Returns whether the module is an export entrypoint of a publishable
/// (JSR) package in the workspace, i.e. a file that `deno doc --lint` and
/// `deno publish` would check for documentation issues.
fn module_is_package_export(
  snapshot: &StateSnapshot,
  module: &DocumentModule,
) -> bool {
  let Some(config_file) = snapshot
    .config
    .tree
    .data_for_specifier(&module.specifier)
    .and_then(|d| d.maybe_deno_json())
  else {
    return false;
  };
  if !config_file.is_package() || !config_file.should_publish() {
    return false;
  }
  match config_file.resolve_export_value_urls() {
    Ok(urls) => urls.iter().any(|url| url == module.specifier.as_ref()),
    Err(err) => {
      lsp_warn!(
        "Failed to resolve export URLs for \"{}\": {err:#}",
        config_file.specifier,
      );
      false
    }
  }
}

#[cfg(test)]
mod tests {
  use std::str::FromStr;
  use std::sync::Arc;

  use deno_config::deno_json::ConfigFile;
  use pretty_assertions::assert_eq;
  use test_util::TempDir;

  use super::*;
  use crate::lsp::cache::LspCache;
  use crate::lsp::compiler_options::LspCompilerOptionsResolver;
  use crate::lsp::config::Config;
  use crate::lsp::config::WorkspaceSettings;
  use crate::lsp::documents::DocumentModules;
  use crate::lsp::documents::LanguageId;
  use crate::lsp::language_server::StateSnapshot;
  use crate::lsp::lint::LspLinterResolver;
  use crate::lsp::resolver::LspResolver;
  use crate::lsp::urls::url_to_uri;

  async fn setup(
    sources: &[(&str, &str, i32, LanguageId)],
    maybe_import_map: Option<(&str, &str)>,
  ) -> (TempDir, StateSnapshot) {
    let temp_dir = TempDir::new();
    let root_url = temp_dir.url();
    let cache = LspCache::new(Some(root_url.join(".deno_dir").unwrap()));
    let mut config = Config::new_with_roots([root_url.clone()]);
    let enabled_settings =
      serde_json::from_str::<WorkspaceSettings>(r#"{ "enable": true }"#)
        .unwrap();
    config.set_workspace_settings(
      enabled_settings.clone(),
      vec![(Arc::new(root_url.clone()), enabled_settings)],
    );
    if let Some((relative_path, json_string)) = maybe_import_map {
      let base_url = root_url.join(relative_path).unwrap();
      let config_file = ConfigFile::new(json_string, base_url).unwrap();
      config.tree.inject_config_file(config_file).await;
    }
    let resolver =
      Arc::new(LspResolver::from_config(&config, &cache, None).await);
    let compiler_options_resolver =
      Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None));
    resolver.set_compiler_options_resolver(&compiler_options_resolver.inner);
    let linter_resolver = Arc::new(LspLinterResolver::new(
      &config,
      &compiler_options_resolver,
      &resolver,
    ));
    let mut document_modules = DocumentModules::default();
    document_modules.update_config(
      &config,
      &compiler_options_resolver,
      &resolver,
      &cache,
      &Default::default(),
    );
    for (relative_path, source, version, language_id) in sources {
      let specifier = root_url.join(relative_path).unwrap();
      document_modules.open_document(
        url_to_uri(&specifier).unwrap(),
        *version,
        *language_id,
        (*source).into(),
        None,
      );
    }
    (
      temp_dir,
      StateSnapshot {
        project_version: 0,
        document_modules,
        config: Arc::new(config),
        compiler_options_resolver,
        linter_resolver,
        resolver,
        cache: Arc::new(cache),
        client_needs_file_uris_for_virtual_documents: false,
      },
    )
  }

  fn generate_all_deno_diagnostics(
    snapshot: &StateSnapshot,
  ) -> Vec<(Uri, Vec<lsp::Diagnostic>)> {
    snapshot
      .document_modules
      .documents
      .open_docs()
      .filter_map(|doc| {
        if !doc.is_diagnosable() {
          return None;
        }
        let module = snapshot
          .document_modules
          .primary_module(&Document::Open(doc.clone()))?;
        if !snapshot.config.specifier_enabled(&module.specifier) {
          return None;
        }
        let mut diagnostics = Vec::new();
        let mut deferred = Vec::new();
        for (dependency_key, dependency) in module.dependencies.iter() {
          diagnose_dependency(
            &mut diagnostics,
            &mut deferred,
            snapshot,
            &module,
            dependency_key,
            dependency,
          );
        }
        diagnostics.extend(deferred.into_iter().map(|(_, d)| d));
        Some((doc.uri.as_ref().clone(), diagnostics))
      })
      .collect::<Vec<_>>()
  }

  fn generate_all_import_map_diagnostics(
    snapshot: &StateSnapshot,
  ) -> Vec<(Uri, Vec<lsp::Diagnostic>)> {
    snapshot
      .document_modules
      .documents
      .open_docs()
      .map(|doc| {
        (
          doc.uri.as_ref().clone(),
          generate_import_map_diagnostics(doc, snapshot),
        )
      })
      .collect::<Vec<_>>()
  }

  #[tokio::test]
  async fn test_import_map_document_diagnostics() {
    let (temp_dir, snapshot) = setup(
      &[(
        "import-map.json",
        r#"{
  "imports": {
    "remote": "https://deno.land/x/example/mod.ts",
    "bad": null
  },
  "unexpected": true
}"#,
        1,
        LanguageId::Json,
      )],
      Some((
        "deno.json",
        r#"{
        "importMap": "./import-map.json"
      }"#,
      )),
    )
    .await;
    let actual = generate_all_import_map_diagnostics(&snapshot);
    assert_eq!(
      json!(actual),
      json!([
        [
          url_to_uri(&temp_dir.url().join("import-map.json").unwrap()).unwrap(),
          [
            {
              "range": {
                "start": { "line": 0, "character": 0 },
                "end": { "line": 0, "character": 0 },
              },
              "severity": 1,
              "code": "import-map-diagnostic",
              "source": "deno",
              "message": "Invalid address \"null\" for the specifier key \"bad\". Addresses must be strings.",
            },
            {
              "range": {
                "start": { "line": 0, "character": 0 },
                "end": { "line": 0, "character": 0 },
              },
              "severity": 1,
              "code": "import-map-diagnostic",
              "source": "deno",
              "message": "Invalid top-level key \"unexpected\". Only \"imports\" and \"scopes\" can be present.",
            },
            {
              "range": {
                "start": { "line": 2, "character": 15 },
                "end": { "line": 2, "character": 49 },
              },
              "severity": 1,
              "code": "no-cache",
              "source": "deno",
              "message": "Uncached or missing remote URL: https://deno.land/x/example/mod.ts",
              "data": {
                "specifier": "https://deno.land/x/example/mod.ts",
              },
            },
          ],
        ],
      ]),
    );
  }

  #[tokio::test]
  async fn test_import_map_document_json_parse_diagnostic() {
    let (temp_dir, snapshot) = setup(
      &[("import-map.json", r#"{ "imports": "#, 1, LanguageId::Json)],
      Some((
        "deno.json",
        r#"{
        "importMap": "./import-map.json"
      }"#,
      )),
    )
    .await;
    let actual = generate_all_import_map_diagnostics(&snapshot);
    assert_eq!(
      json!(actual),
      json!([
        [
          url_to_uri(&temp_dir.url().join("import-map.json").unwrap()).unwrap(),
          [
            {
              "range": {
                "start": { "line": 0, "character": 12 },
                "end": { "line": 0, "character": 12 },
              },
              "severity": 1,
              "code": "import-map-diagnostic",
              "source": "deno",
              "message": "Unable to parse import map JSON: EOF while parsing a value at line 1 column 13",
            },
          ],
        ],
      ]),
    );
  }

  #[tokio::test]
  async fn test_deno_diagnostics_with_import_map() {
    let (temp_dir, snapshot) = setup(
      &[
        (
          "std/assert/mod.ts",
          "export function assert() {}",
          1,
          LanguageId::TypeScript,
        ),
        (
          "a/file.ts",
          "import { assert } from \"../std/assert/mod.ts\";\n\nassert();\nexport function a() {}",
          1,
          LanguageId::TypeScript,
        ),
        (
          "a/file2.ts",
          "import { a } from './file.ts';\nconsole.log(a);\n",
          1,
          LanguageId::TypeScript,
        ),
      ],
      Some((
        "a/deno.json",
        r#"{
        "imports": {
          "/~/std/": "../std/",
          "a/": "../a/",
          "$@/": "./",
          "$a/": "../a/"
        }
      }"#,
      )),
    )
    .await;
    let actual = generate_all_deno_diagnostics(&snapshot);
    assert_eq!(
      json!(actual),
      json!([
        [
          url_to_uri(&temp_dir.url().join("std/assert/mod.ts").unwrap()).unwrap(),
          [],
        ],
        [
          url_to_uri(&temp_dir.url().join("a/file.ts").unwrap()).unwrap(),
          [
            {
              "range": {
                "start": { "line": 0, "character": 23 },
                "end": { "line": 0, "character": 45 },
              },
              "severity": 4,
              "code": "import-map-remap",
              "source": "deno",
              "message": "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.",
              "data": {
                "from": "../std/assert/mod.ts",
                "to": "/~/std/assert/mod.ts",
              },
            },
          ],
        ],
        [
          url_to_uri(&temp_dir.url().join("a/file2.ts").unwrap()).unwrap(),
          [],
        ],
      ]),
    );
  }

  #[test]
  fn test_get_code_action_import_map_remap() {
    let uri = Uri::from_str("file:///a/file.ts").unwrap();
    let specifier = ModuleSpecifier::parse("file:///a/file.ts").unwrap();
    let result = DenoDiagnostic::get_code_action(&uri, &specifier, &lsp::Diagnostic {
      range: lsp::Range {
        start: lsp::Position { line: 0, character: 23 },
        end: lsp::Position { line: 0, character: 50 },
      },
      severity: Some(lsp::DiagnosticSeverity::HINT),
      code: Some(lsp::NumberOrString::String("import-map-remap".to_string())),
      source: Some("deno".to_string()),
      message: "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.".to_string(),
      data: Some(json!({
        "from": "../std/assert/mod.ts",
        "to": "/~/std/assert/mod.ts"
      })),
      ..Default::default()
    });
    assert!(result.is_ok());
    let actual = result.unwrap();
    assert_eq!(
      json!(actual),
      json!({
        "title": "Update \"../std/assert/mod.ts\" to \"/~/std/assert/mod.ts\" to use import map.",
        "kind": "quickfix",
        "diagnostics": [
          {
            "range": {
              "start": {
                "line": 0,
                "character": 23
              },
              "end": {
                "line": 0,
                "character": 50
              }
            },
            "severity": 4,
            "code": "import-map-remap",
            "source": "deno",
            "message": "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.",
            "data": {
              "from": "../std/assert/mod.ts",
              "to": "/~/std/assert/mod.ts"
            }
          }
        ],
        "edit": {
          "changes": {
            "file:///a/file.ts": [
              {
                "range": {
                  "start": {
                    "line": 0,
                    "character": 23
                  },
                  "end": {
                    "line": 0,
                    "character": 50
                  }
                },
                "newText": "\"/~/std/assert/mod.ts\""
              }
            ]
          }
        }
      })
    );
  }

  #[tokio::test]
  async fn duplicate_diagnostics_for_duplicate_imports() {
    let (temp_dir, snapshot) = setup(
      &[(
        "a.ts",
        r#"
        // @ts-types="bad.d.ts"
        import "bad.js";
        import "bad.js";
        "#,
        1,
        LanguageId::TypeScript,
      )],
      None,
    )
    .await;
    let actual = generate_all_deno_diagnostics(&snapshot);
    assert_eq!(
      json!(actual),
      json!([
        [
          url_to_uri(&temp_dir.url().join("a.ts").unwrap()).unwrap(),
          [
            {
              "range": {
                "start": { "line": 2, "character": 15 },
                "end": { "line": 2, "character": 23 },
              },
              "severity": 1,
              "code": "import-prefix-missing",
              "source": "deno",
              "message": "Import \"bad.js\" not a dependency",
            },
            {
              "range": {
                "start": { "line": 3, "character": 15 },
                "end": { "line": 3, "character": 23 },
              },
              "severity": 1,
              "code": "import-prefix-missing",
              "source": "deno",
              "message": "Import \"bad.js\" not a dependency",
            },
            {
              "range": {
                "start": { "line": 1, "character": 21 },
                "end": { "line": 1, "character": 31 },
              },
              "severity": 1,
              "code": "import-prefix-missing",
              "source": "deno",
              "message": "Import \"bad.d.ts\" not a dependency",
            },
          ],
        ],
      ]),
    );
  }

  #[tokio::test]
  async fn unable_to_load_a_local_module() {
    let (temp_dir, snapshot) = setup(
      &[(
        "a.ts",
        r#"
        import { 東京 } from "./🦕.ts";
        "#,
        1,
        LanguageId::TypeScript,
      )],
      None,
    )
    .await;
    let actual = generate_all_deno_diagnostics(&snapshot);
    assert_eq!(
      json!(actual),
      json!([
        [
          url_to_uri(&temp_dir.url().join("a.ts").unwrap()).unwrap(),
          [
            {
              "range": {
                "start": { "line": 1, "character": 27 },
                "end": { "line": 1, "character": 35 },
              },
              "severity": 1,
              "code": "no-local",
              "source": "deno",
              "message": format!(
                "Unable to load a local module: {}🦕.ts\nPlease check the file path.",
                temp_dir.url(),
              ),
            },
          ],
        ],
      ]),
    );
  }

  #[test]
  fn test_specifier_text_for_redirected() {
    #[track_caller]
    fn run_test(specifier: &str, referrer: &str, expected: &str) {
      let result = specifier_text_for_redirected(
        &ModuleSpecifier::parse(specifier).unwrap(),
        &ModuleSpecifier::parse(referrer).unwrap(),
      );
      assert_eq!(result, expected);
    }

    run_test("file:///a/a.ts", "file:///a/mod.ts", "./a.ts");
    run_test("file:///a/a.ts", "file:///a/sub_dir/mod.ts", "../a.ts");
    run_test(
      "file:///a/sub_dir/a.ts",
      "file:///a/mod.ts",
      "./sub_dir/a.ts",
    );
    run_test(
      "https://deno.land/x/example/mod.ts",
      "file:///a/sub_dir/a.ts",
      "https://deno.land/x/example/mod.ts",
    );
  }
}
