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

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

use deno_cache_dir::file_fetcher::CacheSetting;
use deno_core::ModuleSpecifier;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::serde::Deserialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::url::ParseError;
use deno_core::url::Position;
use deno_core::url::Url;
use deno_graph::Dependency;
use deno_resolver::file_fetcher::FetchOptions;
use deno_resolver::file_fetcher::FetchPermissionsOptionRef;
use deno_resolver::loader::MemoryFilesRc;
use log::error;
use once_cell::sync::Lazy;
use serde::Serialize;
use tower_lsp::lsp_types as lsp;

use super::completions::IMPORT_COMMIT_CHARS;
use super::logging::lsp_log;
use super::path_to_regex::Compiler;
use super::path_to_regex::Key;
use super::path_to_regex::MatchResult;
use super::path_to_regex::Matcher;
use super::path_to_regex::StringOrNumber;
use super::path_to_regex::StringOrVec;
use super::path_to_regex::Token;
use super::path_to_regex::parse;
use super::path_to_regex::string_to_regex;
use crate::cache::GlobalHttpCache;
use crate::cache::HttpCache;
use crate::file_fetcher::CliFileFetcher;
use crate::file_fetcher::CreateCliFileFetcherOptions;
use crate::file_fetcher::TextDecodedFile;
use crate::file_fetcher::create_cli_file_fetcher;
use crate::http_util::HttpClientProvider;
use crate::lsp::completions::CompletionItemData;
use crate::sys::CliSys;

const CONFIG_PATH: &str = "/.well-known/deno-import-intellisense.json";
const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
  .add(b' ')
  .add(b'"')
  .add(b'#')
  .add(b'<')
  .add(b'>')
  .add(b'?')
  .add(b'`')
  .add(b'{')
  .add(b'}')
  .add(b'/')
  .add(b':')
  .add(b';')
  .add(b'=')
  .add(b'@')
  .add(b'[')
  .add(b'\\')
  .add(b']')
  .add(b'^')
  .add(b'|')
  .add(b'$')
  .add(b'&')
  .add(b'+')
  .add(b',');

const REGISTRY_IMPORT_COMMIT_CHARS: &[&str] = &["\"", "'"];

static REPLACEMENT_VARIABLE_RE: Lazy<regex::Regex> =
  lazy_regex::lazy_regex!(r"\$\{\{?(\w+)\}?\}");

fn base_url(url: &Url) -> String {
  url.origin().ascii_serialization()
}

#[derive(Debug)]
enum CompletionType {
  Literal(String),
  Key {
    key: Key,
    prefix: Option<String>,
    index: usize,
  },
}

/// Determine if a completion at a given offset is a string literal or a key/
/// variable.
fn get_completion_type(
  char_offset: usize,
  tokens: &[Token],
  match_result: &MatchResult,
) -> Option<CompletionType> {
  let mut char_count = 0_usize;
  for (index, token) in tokens.iter().enumerate() {
    match token {
      Token::String(s) => {
        char_count += s.chars().count();
        if char_offset < char_count {
          return Some(CompletionType::Literal(s.clone()));
        }
      }
      Token::Key(k) => {
        if let Some(prefix) = &k.prefix {
          char_count += prefix.chars().count();
          if char_offset < char_count {
            return Some(CompletionType::Key {
              key: k.clone(),
              prefix: Some(prefix.clone()),
              index,
            });
          }
        }
        if char_offset < char_count {
          return None;
        }
        if let StringOrNumber::String(name) = &k.name {
          let value = match_result
            .get(name)
            .map(|s| s.to_string(Some(k), false))
            .unwrap_or_default();
          char_count += value.chars().count();
          if char_offset <= char_count {
            return Some(CompletionType::Key {
              key: k.clone(),
              prefix: None,
              index,
            });
          }
        }
        if let Some(suffix) = &k.suffix {
          char_count += suffix.chars().count();
          if char_offset <= char_count {
            return Some(CompletionType::Literal(suffix.clone()));
          }
        }
      }
    }
  }

  None
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentationCompletionItemData {
  pub url: Url,
}

/// Generate a data value for a completion item that will instruct the client to
/// resolve the completion item to obtain further information, in this case, the
/// details/documentation endpoint for the item if it exists in the registry
/// configuration
fn get_data(
  registry: &RegistryConfiguration,
  base: &ModuleSpecifier,
  variable: &Key,
  value: &str,
) -> Option<DocumentationCompletionItemData> {
  let url = registry.get_documentation_url_for_key(variable)?;
  get_endpoint(url, base, variable, Some(value))
    .ok()
    .map(|specifier| DocumentationCompletionItemData { url: specifier })
}

/// Generate a data value for a completion item that will instruct the client to
/// resolve the completion item to obtain further information, in this case, the
/// details/documentation endpoint for the item if it exists in the registry
/// configuration when there is a match result that should be interpolated
fn get_data_with_match(
  registry: &RegistryConfiguration,
  base: &ModuleSpecifier,
  tokens: &[Token],
  match_result: &MatchResult,
  variable: &Key,
  value: &str,
) -> Option<DocumentationCompletionItemData> {
  let url = registry.get_documentation_url_for_key(variable)?;
  get_endpoint_with_match(
    variable,
    url,
    base,
    tokens,
    match_result,
    Some(value),
  )
  .ok()
  .map(|specifier| DocumentationCompletionItemData { url: specifier })
}

/// Convert a single variable templated string into a fully qualified URL which
/// can be fetched to provide additional data.
fn get_endpoint(
  url: &str,
  base: &Url,
  variable: &Key,
  maybe_value: Option<&str>,
) -> Result<ModuleSpecifier, AnyError> {
  let url = replace_variable(url, variable, maybe_value);
  parse_url_with_base(&url, base)
}

/// Convert a templated URL string into a fully qualified URL which can be
/// fetched to provide additional data. If `maybe_value` is some, then the
/// variable will replaced in the template prior to other matched variables
/// being replaced, otherwise the supplied variable will be blanked out if
/// present in the template.
fn get_endpoint_with_match(
  variable: &Key,
  url: &str,
  base: &Url,
  tokens: &[Token],
  match_result: &MatchResult,
  maybe_value: Option<&str>,
) -> Result<ModuleSpecifier, AnyError> {
  let mut url = url.to_string();
  let has_value = maybe_value.is_some();
  if has_value {
    url = replace_variable(&url, variable, maybe_value);
  }
  for (key, value) in match_result.params.iter() {
    if let StringOrNumber::String(name) = key {
      let maybe_key = tokens.iter().find_map(|t| match t {
        Token::Key(k) if k.name == *key => Some(k),
        _ => None,
      });
      url =
        url.replace(&format!("${{{name}}}"), &value.to_string(maybe_key, true));
      url = url.replace(
        &format!("${{{{{name}}}}}"),
        &percent_encoding::percent_encode(
          value.to_string(maybe_key, true).as_bytes(),
          COMPONENT,
        )
        .to_string(),
      );
    }
  }
  if !has_value {
    url = replace_variable(&url, variable, None);
  }
  parse_url_with_base(&url, base)
}

/// Based on the preselect response from the registry, determine if this item
/// should be preselected or not.
fn get_preselect(item: String, preselect: Option<String>) -> Option<bool> {
  if Some(item) == preselect {
    Some(true)
  } else {
    None
  }
}

fn parse_replacement_variables<S: AsRef<str>>(s: S) -> Vec<String> {
  REPLACEMENT_VARIABLE_RE
    .captures_iter(s.as_ref())
    .filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
    .collect()
}

/// Attempt to parse a URL along with a base, where the base will be used if the
/// URL requires one.
fn parse_url_with_base(
  url: &str,
  base: &ModuleSpecifier,
) -> Result<ModuleSpecifier, AnyError> {
  match Url::parse(url) {
    Ok(url) => Ok(url),
    Err(ParseError::RelativeUrlWithoutBase) => {
      base.join(url).map_err(|err| err.into())
    }
    Err(err) => Err(err.into()),
  }
}

/// Replaces a variable in a templated URL string with the supplied value or
/// "blank" it out if there is no value supplied.
fn replace_variable(
  url: &str,
  variable: &Key,
  maybe_value: Option<&str>,
) -> String {
  let url_str = url.to_string();
  let value = maybe_value.unwrap_or("");
  if let StringOrNumber::String(name) = &variable.name {
    url_str
      .replace(&format!("${{{name}}}"), value)
      .replace(&format! {"${{{{{name}}}}}"}, value)
  } else {
    url_str
  }
}

/// Validate a registry configuration JSON structure.
fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> {
  if config.version < 1 || config.version > 2 {
    return Err(anyhow!(
      "Invalid registry configuration. Expected version 1 or 2 got {}.",
      config.version
    ));
  }
  for registry in &config.registries {
    let (_, keys) = string_to_regex(&registry.schema, None)?;
    let key_names: Vec<String> = keys
      .map(|keys| {
        keys
          .iter()
          .filter_map(|k| {
            if let StringOrNumber::String(s) = &k.name {
              Some(s.clone())
            } else {
              None
            }
          })
          .collect()
      })
      .unwrap_or_default();

    for key_name in &key_names {
      if !registry
        .variables
        .iter()
        .map(|var| var.key.to_owned())
        .any(|x| x == *key_name)
      {
        return Err(anyhow!(
          "Invalid registry configuration. Registry with schema \"{}\" is missing variable declaration for key \"{}\".",
          registry.schema,
          key_name
        ));
      }
    }

    for variable in &registry.variables {
      let key_index = key_names.iter().position(|key| *key == variable.key);
      let key_index = key_index.ok_or_else(||anyhow!("Invalid registry configuration. Registry with schema \"{}\" is missing a path parameter in schema for variable \"{}\".", registry.schema, variable.key))?;

      let replacement_variables = parse_replacement_variables(&variable.url);
      let limited_keys = key_names.get(0..key_index).unwrap();
      for v in replacement_variables {
        if variable.key == v && config.version == 1 {
          return Err(anyhow!(
            "Invalid registry configuration. Url \"{}\" (for variable \"{}\" in registry with schema \"{}\") uses variable \"{}\", which is not allowed because that would be a self reference.",
            variable.url,
            variable.key,
            registry.schema,
            v
          ));
        }

        let key_index = limited_keys.iter().position(|key| key == &v);

        if key_index.is_none() && variable.key != v {
          return Err(anyhow!(
            "Invalid registry configuration. Url \"{}\" (for variable \"{}\" in registry with schema \"{}\") uses variable \"{}\", which is not allowed because the schema defines \"{}\" to the right of \"{}\".",
            variable.url,
            variable.key,
            registry.schema,
            v,
            v,
            variable.key
          ));
        }
      }
    }
  }

  Ok(())
}

#[derive(Debug, Clone, Deserialize)]
pub struct RegistryConfigurationVariable {
  /// The name of the variable.
  key: String,
  /// An optional URL/API endpoint that can provide optional documentation for a
  /// completion item when requested by the language server.
  documentation: Option<String>,
  /// The URL with variable substitutions of the endpoint that will provide
  /// completions for the variable.
  url: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RegistryConfiguration {
  /// A Express-like path which describes how URLs are composed for a registry.
  schema: String,
  /// The variables denoted in the `schema` should have a variable entry.
  variables: Vec<RegistryConfigurationVariable>,
}

impl RegistryConfiguration {
  fn get_url_for_key(&self, key: &Key) -> Option<&str> {
    self.variables.iter().find_map(|v| {
      if key.name == StringOrNumber::String(v.key.clone()) {
        Some(v.url.as_str())
      } else {
        None
      }
    })
  }

  fn get_documentation_url_for_key(&self, key: &Key) -> Option<&str> {
    self.variables.iter().find_map(|v| {
      if key.name == StringOrNumber::String(v.key.clone()) {
        v.documentation.as_deref()
      } else {
        None
      }
    })
  }
}

/// A structure that represents the configuration of an origin and its module
/// registries.
#[derive(Debug, Deserialize)]
struct RegistryConfigurationJson {
  version: u32,
  registries: Vec<RegistryConfiguration>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VariableItemsList {
  pub items: Vec<String>,
  #[serde(default)]
  pub is_incomplete: bool,
  pub preselect: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum VariableItems {
  Simple(Vec<String>),
  List(VariableItemsList),
}

/// A structure which holds the information about currently configured module
/// registries and can provide completion information for URLs that match
/// one of the enabled registries.
#[derive(Debug, Clone)]
pub struct ModuleRegistry {
  origins: HashMap<String, Vec<RegistryConfiguration>>,
  pub location: PathBuf,
  pub file_fetcher: Arc<CliFileFetcher>,
  http_cache: Arc<GlobalHttpCache>,
}

impl ModuleRegistry {
  pub fn new(
    location: PathBuf,
    http_client_provider: Arc<HttpClientProvider>,
  ) -> Self {
    // the http cache should always be the global one for registry completions
    let http_cache =
      Arc::new(GlobalHttpCache::new(CliSys::default(), location.clone()));
    let file_fetcher = create_cli_file_fetcher(
      Arc::new(deno_runtime::deno_web::BlobStore::default())
        as Arc<dyn deno_runtime::deno_web::BlobStoreTrait>,
      http_cache.clone().into(),
      http_client_provider,
      MemoryFilesRc::default(),
      CliSys::default(),
      CreateCliFileFetcherOptions {
        allow_remote: true,
        cache_setting: CacheSetting::RespectHeaders,
        download_log_level: super::logging::lsp_log_level(),
        progress_bar: None,
      },
    );

    Self {
      origins: HashMap::new(),
      location,
      file_fetcher: Arc::new(file_fetcher),
      http_cache,
    }
  }

  /// Disable a registry, removing its configuration, if any, from memory.
  pub fn disable(&mut self, origin: &str) {
    let Ok(origin_url) = Url::parse(origin) else {
      return;
    };
    let origin = base_url(&origin_url);
    self.origins.remove(&origin);
  }

  /// Check to see if the given origin has a registry configuration.
  pub async fn check_origin(&self, origin: &str) -> Result<(), AnyError> {
    let origin_url = Url::parse(origin)?;
    let specifier = origin_url.join(CONFIG_PATH)?;
    self.fetch_config(&specifier).await?;
    Ok(())
  }

  /// Fetch and validate the specifier to a registry configuration, resolving
  /// with the configuration if valid.
  async fn fetch_config(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Result<Vec<RegistryConfiguration>, AnyError> {
    let fetch_result = self.file_fetcher
      .fetch_with_options(
        specifier,
        FetchPermissionsOptionRef::AllowAll,
        FetchOptions {
          local: Default::default(),
          maybe_auth: None,
          maybe_accept: Some("application/vnd.deno.reg.v2+json, application/vnd.deno.reg.v1+json;q=0.9, application/json;q=0.8"),
          maybe_cache_setting: None,
        }
      )
      .await;
    let file = match fetch_result {
      Ok(file) => file,
      Err(err) => {
        // The remote fetch failed (e.g. we're offline). If we previously
        // fetched the configuration, fall back to the cached copy regardless
        // of its freshness, so that completions keep working without a network
        // connection.
        if let Ok(Some(file)) =
          self.file_fetcher.fetch_cached_remote(specifier, 10)
          && !file.source.is_empty()
        {
          file
        } else {
          // Otherwise we cache an empty file, so that subsequent requests are
          // just an empty doc which will error without needing to connect to
          // the remote URL. We will cache it for 1 week.
          let mut headers_map = HashMap::new();
          headers_map.insert(
            "cache-control".to_string(),
            "max-age=604800, immutable".to_string(),
          );
          self.http_cache.set(specifier, headers_map, &[])?;
          return Err(err.into());
        }
      }
    };
    let file = TextDecodedFile::decode(file)?;
    let config: RegistryConfigurationJson = serde_json::from_str(&file.source)?;
    validate_config(&config)?;
    Ok(config.registries)
  }

  /// Enable a registry by attempting to retrieve its configuration and
  /// validating it.
  pub async fn enable(&mut self, origin: &str) {
    let Ok(origin_url) = Url::parse(origin) else {
      return;
    };
    let origin = base_url(&origin_url);
    #[allow(clippy::map_entry, reason = "less allocations")]
    // we can't use entry().or_insert_with() because we can't use async closures
    if !self.origins.contains_key(&origin) {
      let Ok(specifier) = origin_url.join(CONFIG_PATH) else {
        return;
      };
      match self.fetch_config(&specifier).await {
        Ok(configs) => {
          self.origins.insert(origin, configs);
        }
        Err(err) => {
          lsp_log!(
            "  Error fetching registry config for \"{}\": {}",
            origin,
            err.to_string()
          );
          self.origins.remove(&origin);
        }
      }
    }
  }

  #[cfg(test)]
  /// This is only used during testing, as it directly provides the full URL
  /// for obtaining the registry configuration, versus "guessing" at it.
  async fn enable_custom(&mut self, specifier: &str) -> Result<(), AnyError> {
    let specifier = Url::parse(specifier)?;
    let origin = base_url(&specifier);
    #[allow(clippy::map_entry, reason = "reduces a clone")]
    if !self.origins.contains_key(&origin) {
      let configs = self.fetch_config(&specifier).await?;
      self.origins.insert(origin, configs);
    }

    Ok(())
  }

  pub async fn get_hover(&self, dependency: &Dependency) -> Option<String> {
    let maybe_code = dependency.get_code();
    let maybe_type = dependency.get_type();
    let specifier = match (maybe_code, maybe_type) {
      (Some(specifier), _) => Some(specifier),
      (_, Some(specifier)) => Some(specifier),
      _ => None,
    }?;
    let origin = base_url(specifier);
    let registries = self.origins.get(&origin)?;
    let path = &specifier[Position::BeforePath..];
    for registry in registries {
      let tokens = parse(&registry.schema, None).ok()?;
      let matcher = Matcher::new(&tokens, None).ok()?;
      if let Some(match_result) = matcher.matches(path) {
        let key = if let Some(Token::Key(key)) = tokens.iter().last() {
          Some(key)
        } else {
          None
        }?;
        let url = registry.get_documentation_url_for_key(key)?;
        let endpoint = get_endpoint_with_match(
          key,
          url,
          specifier,
          &tokens,
          &match_result,
          None,
        )
        .ok()?;
        let file_fetcher = self.file_fetcher.clone();
        let file = {
          let file = file_fetcher
            .fetch_bypass_permissions(&endpoint)
            .await
            .ok()?;
          TextDecodedFile::decode(file).ok()?
        };
        let documentation: lsp::Documentation =
          serde_json::from_str(&file.source).ok()?;
        return match documentation {
          lsp::Documentation::String(doc) => Some(doc),
          lsp::Documentation::MarkupContent(lsp::MarkupContent {
            value,
            ..
          }) => Some(value),
        };
      }
    }

    None
  }

  /// For a string specifier from the client, provide a set of completions, if
  /// any, for the specifier.
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_completions(
    &self,
    text: &str,
    range: &lsp::Range,
    resolved: Option<&ModuleSpecifier>,
    specifier_exists: impl Fn(&ModuleSpecifier) -> bool,
  ) -> Option<lsp::CompletionList> {
    let resolved = resolved
      .map(Cow::Borrowed)
      .or_else(|| ModuleSpecifier::parse(text).ok().map(Cow::Owned))?;
    let resolved_str = resolved.as_str();
    let origin = base_url(&resolved);
    let origin_char_count = origin.chars().count();
    let registries = self.origins.get(&origin)?;
    let path = &resolved[Position::BeforePath..];
    let path_char_offset = resolved_str.chars().count() - origin_char_count;
    let mut completions = HashMap::<String, lsp::CompletionItem>::new();
    let mut is_incomplete = false;
    let mut did_match = false;
    for registry in registries {
      let tokens = parse(&registry.schema, None)
        .map_err(|e| {
          error!(
            "Error parsing registry schema for origin \"{}\". {}",
            origin, e
          );
        })
        .ok()?;
      let mut i = tokens.len();
      let last_key_name = StringOrNumber::String(
        tokens
          .iter()
          .last()
          .map(|t| {
            if let Token::Key(key) = t
              && let StringOrNumber::String(s) = &key.name
            {
              return s.clone();
            }
            "".to_string()
          })
          .unwrap_or_default(),
      );
      loop {
        let matcher = Matcher::new(&tokens[..i], None)
          .map_err(|e| {
            error!(
              "Error creating matcher for schema for origin \"{}\". {}",
              origin, e
            );
          })
          .ok()?;
        if let Some(match_result) = matcher.matches(path) {
          did_match = true;
          let completion_type =
            get_completion_type(path_char_offset, &tokens, &match_result);
          match completion_type {
            Some(CompletionType::Literal(s)) => {
              let label = s;
              let full_text = format!("{text}{label}");
              let text_edit =
                Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                  range: *range,
                  new_text: full_text.clone(),
                }));
              let filter_text = Some(full_text);
              completions.insert(
                label.clone(),
                lsp::CompletionItem {
                  label,
                  kind: Some(lsp::CompletionItemKind::FOLDER),
                  filter_text,
                  sort_text: Some("1".to_string()),
                  text_edit,
                  commit_characters: Some(
                    REGISTRY_IMPORT_COMMIT_CHARS
                      .iter()
                      .map(|&c| c.into())
                      .collect(),
                  ),
                  ..Default::default()
                },
              );
            }
            Some(CompletionType::Key { key, prefix, index }) => {
              let maybe_url = registry.get_url_for_key(&key);
              if let Some(url) = maybe_url
                && let Some(items) = self
                  .get_variable_items(
                    &key,
                    url,
                    &resolved,
                    &tokens,
                    &match_result,
                  )
                  .await
              {
                let compiler = Compiler::new(&tokens[..=index], None);
                let base = Url::parse(&origin).ok()?;
                let (items, preselect, incomplete) = match items {
                  VariableItems::List(list) => {
                    (list.items, list.preselect, list.is_incomplete)
                  }
                  VariableItems::Simple(items) => (items, None, false),
                };
                if incomplete {
                  is_incomplete = true;
                }
                for (idx, item) in items.into_iter().enumerate() {
                  let mut label = if let Some(p) = &prefix {
                    format!("{p}{item}")
                  } else {
                    item.clone()
                  };
                  if label.ends_with('/') {
                    label.pop();
                  }
                  let kind =
                    if key.name == last_key_name && !item.ends_with('/') {
                      Some(lsp::CompletionItemKind::FILE)
                    } else {
                      Some(lsp::CompletionItemKind::FOLDER)
                    };
                  let mut params = match_result.params.clone();
                  params.insert(
                    key.name.clone(),
                    StringOrVec::from_str(&item, &key),
                  );
                  let mut path = compiler.to_path(&params).unwrap_or_default();
                  if path.ends_with('/') {
                    path.pop();
                  }
                  let item_specifier = base.join(&path).ok()?;
                  let full_text = if let Some(suffix) =
                    item_specifier.as_str().strip_prefix(resolved_str)
                  {
                    format!("{text}{suffix}")
                  } else {
                    item_specifier.to_string()
                  };
                  let text_edit =
                    Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                      range: *range,
                      new_text: full_text.to_string(),
                    }));
                  let command = if key.name == last_key_name
                    && !item.ends_with('/')
                    && !specifier_exists(&item_specifier)
                  {
                    Some(lsp::Command {
                      title: "".to_string(),
                      command: "deno.cache".to_string(),
                      arguments: Some(vec![
                        json!([item_specifier]),
                        json!(&resolved),
                      ]),
                    })
                  } else {
                    None
                  };
                  let detail = Some(format!("({})", key.name));
                  let filter_text = Some(full_text.to_string());
                  let sort_text = Some(format!("{:0>10}", idx + 1));
                  let preselect =
                    get_preselect(item.clone(), preselect.clone());
                  let data = get_data_with_match(
                    registry,
                    &resolved,
                    &tokens,
                    &match_result,
                    &key,
                    &item,
                  );
                  let commit_characters = if is_incomplete {
                    Some(
                      REGISTRY_IMPORT_COMMIT_CHARS
                        .iter()
                        .map(|&c| c.into())
                        .collect(),
                    )
                  } else {
                    Some(
                      IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
                    )
                  };
                  completions.insert(
                    item,
                    lsp::CompletionItem {
                      label,
                      kind,
                      detail,
                      sort_text,
                      filter_text,
                      text_edit,
                      command,
                      preselect,
                      data: data.map(|data| {
                        json!(CompletionItemData::Documentation(data))
                      }),
                      commit_characters,
                      ..Default::default()
                    },
                  );
                }
              }
            }
            None => (),
          }
          break;
        }
        i -= 1;
        // If we have fallen though to the first token, and we still
        // didn't get a match
        if i == 0 {
          match &tokens[i] {
            // so if the first token is a string literal, we will return
            // that as a suggestion
            Token::String(s) => {
              if s.starts_with(path) {
                let label = s.to_string();
                let kind = Some(lsp::CompletionItemKind::FOLDER);
                let mut url = resolved.as_ref().clone();
                url.set_path(s);
                let full_text = if let Some(suffix) =
                  url.as_str().strip_prefix(resolved_str)
                {
                  format!("{text}{suffix}")
                } else {
                  url.into()
                };
                let text_edit =
                  Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                    range: *range,
                    new_text: full_text.to_string(),
                  }));
                let filter_text = Some(full_text.to_string());
                completions.insert(
                  s.to_string(),
                  lsp::CompletionItem {
                    label,
                    kind,
                    filter_text,
                    sort_text: Some("1".to_string()),
                    text_edit,
                    preselect: Some(true),
                    commit_characters: Some(
                      REGISTRY_IMPORT_COMMIT_CHARS
                        .iter()
                        .map(|&c| c.into())
                        .collect(),
                    ),
                    ..Default::default()
                  },
                );
              }
            }
            // if the token though is a key, and the key has a prefix, and
            // the path matches the prefix, we will go and get the items
            // for that first key and return them.
            Token::Key(k) => {
              if let Some(prefix) = &k.prefix {
                let maybe_url = registry.get_url_for_key(k);
                if let Some(url) = maybe_url
                  && let Some(items) = self.get_items(url).await
                {
                  let base = Url::parse(&origin).ok()?;
                  let (items, preselect, incomplete) = match items {
                    VariableItems::List(list) => {
                      (list.items, list.preselect, list.is_incomplete)
                    }
                    VariableItems::Simple(items) => (items, None, false),
                  };
                  if incomplete {
                    is_incomplete = true;
                  }
                  for (idx, item) in items.into_iter().enumerate() {
                    let path = format!("{prefix}{item}");
                    let kind = Some(lsp::CompletionItemKind::FOLDER);
                    let item_specifier = base.join(&path).ok()?;
                    let full_text = if let Some(suffix) =
                      item_specifier.as_str().strip_prefix(resolved_str)
                    {
                      format!("{text}{suffix}")
                    } else {
                      item_specifier.to_string()
                    };
                    let text_edit =
                      Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                        range: *range,
                        new_text: full_text.clone(),
                      }));
                    let command = if k.name == last_key_name
                      && !specifier_exists(&item_specifier)
                    {
                      Some(lsp::Command {
                        title: "".to_string(),
                        command: "deno.cache".to_string(),
                        arguments: Some(vec![
                          json!([item_specifier]),
                          json!(&resolved),
                        ]),
                      })
                    } else {
                      None
                    };
                    let detail = Some(format!("({})", k.name));
                    let filter_text = Some(full_text.to_string());
                    let sort_text = Some(format!("{:0>10}", idx + 1));
                    let preselect =
                      get_preselect(item.clone(), preselect.clone());
                    let data = get_data(registry, &resolved, k, &path);
                    let commit_characters = if is_incomplete {
                      Some(
                        REGISTRY_IMPORT_COMMIT_CHARS
                          .iter()
                          .map(|&c| c.into())
                          .collect(),
                      )
                    } else {
                      Some(
                        IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
                      )
                    };
                    completions.insert(
                      item.clone(),
                      lsp::CompletionItem {
                        label: item,
                        kind,
                        detail,
                        sort_text,
                        filter_text,
                        text_edit,
                        command,
                        preselect,
                        data: data.map(|data| {
                          json!(CompletionItemData::Documentation(data))
                        }),
                        commit_characters,
                        ..Default::default()
                      },
                    );
                  }
                }
              }
            }
          }
          break;
        }
      }
    }
    // If we return None, other sources of completions will be looked for
    // but if we did at least match part of a registry, we should send an
    // empty vector so that no-completions will be sent back to the client
    if completions.is_empty() && !did_match {
      None
    } else {
      Some(lsp::CompletionList {
        items: completions.into_values().collect(),
        is_incomplete,
      })
    }
  }

  pub async fn get_documentation(
    &self,
    url: &Url,
  ) -> Option<lsp::Documentation> {
    let file_fetcher = self.file_fetcher.clone();
    let file = {
      let file = file_fetcher.fetch_bypass_permissions(url).await.ok()?;
      TextDecodedFile::decode(file).ok()?
    };
    serde_json::from_str(&file.source).ok()
  }

  pub fn get_origin_completions(
    &self,
    current_specifier: &str,
    range: &lsp::Range,
  ) -> Option<lsp::CompletionList> {
    let items = self
      .origins
      .keys()
      .filter_map(|k| {
        let mut origin = k.to_string();
        if origin.ends_with('/') {
          origin.pop();
        }
        if origin.starts_with(current_specifier) {
          let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
            range: *range,
            new_text: origin.clone(),
          }));
          Some(lsp::CompletionItem {
            label: origin,
            kind: Some(lsp::CompletionItemKind::FOLDER),
            detail: Some("(registry)".to_string()),
            sort_text: Some("2".to_string()),
            text_edit,
            commit_characters: Some(
              REGISTRY_IMPORT_COMMIT_CHARS
                .iter()
                .map(|&c| c.into())
                .collect(),
            ),
            ..Default::default()
          })
        } else {
          None
        }
      })
      .collect::<Vec<lsp::CompletionItem>>();
    if !items.is_empty() {
      Some(lsp::CompletionList {
        items,
        is_incomplete: false,
      })
    } else {
      None
    }
  }

  async fn get_items(&self, url: &str) -> Option<VariableItems> {
    let specifier = ModuleSpecifier::parse(url).ok()?;
    let file = {
      let file = self
        .file_fetcher
        .fetch_bypass_permissions(&specifier)
        .await
        .map_err(|err| {
          error!(
            "Internal error fetching endpoint \"{}\". {}",
            specifier, err
          );
        })
        .ok()?;
      TextDecodedFile::decode(file).ok()?
    };
    let items: VariableItems = serde_json::from_str(&file.source)
      .map_err(|err| {
        error!(
          "Error parsing response from endpoint \"{}\". {}",
          specifier, err
        );
      })
      .ok()?;
    Some(items)
  }

  async fn get_variable_items(
    &self,
    variable: &Key,
    url: &str,
    base: &Url,
    tokens: &[Token],
    match_result: &MatchResult,
  ) -> Option<VariableItems> {
    let specifier =
      get_endpoint_with_match(variable, url, base, tokens, match_result, None)
        .map_err(|err| {
          error!("Internal error mapping endpoint \"{}\". {}", url, err);
        })
        .ok()?;
    let file = {
      let file = self
        .file_fetcher
        .fetch_bypass_permissions(&specifier)
        .await
        .map_err(|err| {
          error!(
            "Internal error fetching endpoint \"{}\". {}",
            specifier, err
          );
        })
        .ok()?;
      TextDecodedFile::decode(file).ok()?
    };
    let items: VariableItems = serde_json::from_str(&file.source)
      .map_err(|err| {
        error!(
          "Error parsing response from endpoint \"{}\". {}",
          specifier, err
        );
      })
      .ok()?;
    Some(items)
  }

  pub fn clear_cache(&self) {
    self.file_fetcher.clear_memory_files();
  }
}

#[cfg(test)]
mod tests {
  use test_util::TempDir;

  use super::*;

  #[test]
  fn test_validate_registry_configuration() {
    assert!(
      validate_config(&RegistryConfigurationJson {
        version: 3,
        registries: vec![],
      })
      .is_err()
    );

    let cfg = RegistryConfigurationJson {
      version: 1,
      registries: vec![RegistryConfiguration {
        schema: "/:module@:version/:path*".to_string(),
        variables: vec![
          RegistryConfigurationVariable {
            key: "module".to_string(),
            documentation: None,
            url: "https://api.deno.land/modules?short".to_string(),
          },
          RegistryConfigurationVariable {
            key: "version".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}".to_string(),
          },
        ],
      }],
    };
    assert!(validate_config(&cfg).is_err());

    let cfg = RegistryConfigurationJson {
      version: 1,
      registries: vec![RegistryConfiguration {
        schema: "/:module@:version/:path*".to_string(),
        variables: vec![
          RegistryConfigurationVariable {
            key: "module".to_string(),
            documentation: None,
            url: "https://api.deno.land/modules?short".to_string(),
          },
          RegistryConfigurationVariable {
            key: "version".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}/${path}".to_string(),
          },
          RegistryConfigurationVariable {
            key: "path".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
              .to_string(),
          },
        ],
      }],
    };
    assert!(validate_config(&cfg).is_err());

    let cfg = RegistryConfigurationJson {
      version: 1,
      registries: vec![RegistryConfiguration {
        schema: "/:module@:version/:path*".to_string(),
        variables: vec![
          RegistryConfigurationVariable {
            key: "module".to_string(),
            documentation: None,
            url: "https://api.deno.land/modules?short".to_string(),
          },
          RegistryConfigurationVariable {
            key: "version".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
              .to_string(),
          },
          RegistryConfigurationVariable {
            key: "path".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
              .to_string(),
          },
        ],
      }],
    };
    assert!(validate_config(&cfg).is_err());

    let cfg = RegistryConfigurationJson {
      version: 1,
      registries: vec![RegistryConfiguration {
        schema: "/:module@:version/:path*".to_string(),
        variables: vec![
          RegistryConfigurationVariable {
            key: "module".to_string(),
            documentation: None,
            url: "https://api.deno.land/modules?short".to_string(),
          },
          RegistryConfigurationVariable {
            key: "version".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}".to_string(),
          },
          RegistryConfigurationVariable {
            key: "path".to_string(),
            documentation: None,
            url: "https://deno.land/_vsc1/module/${module}/v/${{version}}"
              .to_string(),
          },
        ],
      }],
    };
    assert!(validate_config(&cfg).is_ok());

    let cfg: RegistryConfigurationJson = serde_json::from_value(json!({
      "version": 2,
      "registries": [
        {
          "schema": "/x/:module([a-z0-9_]+)@:version?/:path",
          "variables": [
            {
              "key": "module",
              "documentation": "/api/details/mods/${module}",
              "url": "/api/mods/${module}"
            },
            {
              "key": "version",
              "documentation": "/api/details/mods/${module}/v/${{version}}",
              "url": "/api/mods/${module}/v/${{version}}"
            },
            {
              "key": "path",
              "documentation": "/api/details/mods/${module}/v/${{version}}/p/${path}",
              "url": "/api/mods/${module}/v/${{version}}/p/${path}"
            }
          ]
        },
        {
          "schema": "/x/:module([a-z0-9_]+)/:path",
          "variables": [
            {
              "key": "module",
              "documentation": "/api/details/mods/${module}",
              "url": "/api/mods/${module}"
            },
            {
              "key": "path",
              "documentation": "/api/details/mods/${module}/v/latest/p/${path}",
              "url": "/api/mods/${module}/v/latest/p/${path}"
            }
          ]
        }
      ]
    })).unwrap();
    assert!(validate_config(&cfg).is_ok());
  }

  #[tokio::test]
  async fn test_registry_completions_origin_match() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    module_registry.enable("http://localhost:4545/").await;
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 21,
      },
    };
    let completions = module_registry.get_origin_completions("h", &range);
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 1);
    assert_eq!(completions[0].label, "http://localhost:4545");
    assert_eq!(
      completions[0].text_edit,
      Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
        range,
        new_text: "http://localhost:4545".to_string()
      }))
    );
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 36,
      },
    };
    let completions =
      module_registry.get_origin_completions("http://localhost", &range);
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 1);
    assert_eq!(completions[0].label, "http://localhost:4545");
    assert_eq!(
      completions[0].text_edit,
      Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
        range,
        new_text: "http://localhost:4545".to_string()
      }))
    );
  }

  #[tokio::test]
  async fn test_registry_completions() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    module_registry.enable("http://localhost:4545/").await;
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 41,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 42,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 44,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/x/", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap();
    assert_eq!(completions.items.len(), 2);
    assert!(completions.is_incomplete);
    assert!(
      completions.items[0].label == *"a" || completions.items[0].label == *"b"
    );
    assert!(
      completions.items[1].label == *"a" || completions.items[1].label == *"b"
    );

    // testing for incremental searching for a module
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 45,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/x/a", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap();
    assert_eq!(completions.items.len(), 4);
    assert!(!completions.is_incomplete);
    assert_eq!(
      completions.items[0].data,
      Some(json!({
        "documentation": {
          "url": format!("http://localhost:4545/lsp/registries/doc_{}.json", completions.items[0].label),
        },
      }))
    );

    // testing getting the documentation
    let documentation = module_registry
      .get_documentation(
        &Url::parse("http://localhost:4545/lsp/registries/doc_a.json").unwrap(),
      )
      .await;
    assert_eq!(
      documentation,
      Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
        kind: lsp::MarkupKind::Markdown,
        value: "**a**".to_string(),
      }))
    );

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 46,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/x/a@", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    assert_eq!(
      completions[0].data,
      Some(json!({
        "documentation": {
          "url": format!("http://localhost:4545/lsp/registries/doc_a_{}.json", completions[0].label),
        },
      }))
    );

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 49,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/x/a@v1.", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 2);
    assert_eq!(
      completions[0].data,
      Some(json!({
        "documentation": {
          "url": format!("http://localhost:4545/lsp/registries/doc_a_{}.json", completions[0].label),
        },
      }))
    );

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 53,
      },
    };
    let completions = module_registry
      .get_completions(
        "http://localhost:4545/x/a@v1.0.0/",
        &range,
        None,
        |_| false,
      )
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 2);
    assert_eq!(completions[0].detail, Some("(path)".to_string()));
    assert_eq!(completions[0].kind, Some(lsp::CompletionItemKind::FILE));
    assert!(completions[0].command.is_some());
    assert_eq!(completions[1].detail, Some("(path)".to_string()));
    assert_eq!(completions[0].kind, Some(lsp::CompletionItemKind::FILE));
    assert!(completions[1].command.is_some());

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 54,
      },
    };
    let completions = module_registry
      .get_completions(
        "http://localhost:4545/x/a@v1.0.0/b",
        &range,
        None,
        |_| false,
      )
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 1);
    assert_eq!(completions[0].detail, Some("(path)".to_string()));
    assert_eq!(completions[0].kind, Some(lsp::CompletionItemKind::FILE));
    assert!(completions[0].command.is_some());

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 55,
      },
    };
    let completions = module_registry
      .get_completions(
        "http://localhost:4545/x/a@v1.0.0/b/",
        &range,
        None,
        |_| false,
      )
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 1);
    assert_eq!(completions[0].detail, Some("(path)".to_string()));
    assert_eq!(completions[0].kind, Some(lsp::CompletionItemKind::FILE));
    assert!(completions[0].command.is_some());
  }

  #[tokio::test]
  async fn test_registry_completions_key_first() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    module_registry
      .enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-key-first.json")
      .await
      .expect("could not enable");
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 42,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    for completion in completions {
      assert!(completion.text_edit.is_some());
      if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
      {
        assert_eq!(
          edit.new_text,
          format!("http://localhost:4545/{}", completion.label)
        );
      } else {
        unreachable!("unexpected text edit");
      }
    }

    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 46,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/cde@", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    for completion in completions {
      if let Some(filter_text) = completion.filter_text
        && !"http://localhost:4545/cde@".contains(&filter_text)
      {
        continue;
      }
      assert!(completion.text_edit.is_some());
      if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
      {
        assert_eq!(
          edit.new_text,
          format!("http://localhost:4545/cde@{}", completion.label)
        );
      } else {
        unreachable!("unexpected text edit");
      }
    }
  }

  #[tokio::test]
  async fn test_registry_completions_complex() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    module_registry
      .enable_custom("http://localhost:4545/lsp/registries/deno-import-intellisense-complex.json")
      .await
      .expect("could not enable");
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 42,
      },
    };
    let completions = module_registry
      .get_completions("http://localhost:4545/", &range, None, |_| false)
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    for completion in completions {
      assert!(completion.text_edit.is_some());
      if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
      {
        assert_eq!(
          edit.new_text,
          format!("http://localhost:4545/{}", completion.label)
        );
      } else {
        unreachable!("unexpected text edit");
      }
    }
  }

  #[tokio::test]
  async fn test_registry_completions_import_map() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    module_registry.enable("http://localhost:4545/").await;
    let range = lsp::Range {
      start: lsp::Position {
        line: 0,
        character: 20,
      },
      end: lsp::Position {
        line: 0,
        character: 33,
      },
    };
    let completions = module_registry
      .get_completions(
        "localhost4545/",
        &range,
        Some(&ModuleSpecifier::parse("http://localhost:4545/").unwrap()),
        |_| false,
      )
      .await;
    assert!(completions.is_some());
    let completions = completions.unwrap().items;
    assert_eq!(completions.len(), 3);
    for completion in completions {
      assert!(completion.text_edit.is_some());
      if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
      {
        assert_eq!(edit.new_text, format!("localhost4545{}", completion.label));
      } else {
        unreachable!("unexpected text edit");
      }
    }
  }

  #[test]
  fn test_parse_replacement_variables() {
    let actual = parse_replacement_variables(
      "https://deno.land/_vsc1/modules/${module}/v/${{version}}",
    );
    assert_eq!(actual.len(), 2);
    assert!(actual.contains(&"module".to_owned()));
    assert!(actual.contains(&"version".to_owned()));
  }

  #[tokio::test]
  async fn test_check_origin_supported() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    let result = module_registry.check_origin("http://localhost:4545").await;
    assert!(result.is_ok());
  }

  #[tokio::test]
  async fn test_check_origin_not_supported() {
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let _g = test_util::http_server();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    let module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );
    let result = module_registry.check_origin("https://example.com").await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains(
      "https://example.com/.well-known/deno-import-intellisense.json"
    ));

    // because we are caching an empty file when we hit an error with import
    // detection when fetching the config file, we should have an error now that
    // indicates trying to parse an empty file.
    let result = module_registry.check_origin("https://example.com").await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("EOF while parsing a value at line 1 column 0"));
  }

  #[tokio::test]
  async fn test_registry_config_offline_fallback() {
    // Regression test for https://github.com/denoland/deno/issues/23898: when
    // the registry config can't be fetched (e.g. because we're offline) but it
    // was previously cached, the cached copy should be used so that
    // completions keep working without a network connection.
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    let temp_dir = TempDir::new();
    let location = temp_dir.path().join("registries").to_path_buf();
    // Nothing is listening on this port, so any remote fetch fails, simulating
    // being offline.
    let origin = "http://localhost:13899";
    let mut module_registry = ModuleRegistry::new(
      location,
      Arc::new(HttpClientProvider::new(None, None)),
    );

    // A valid registry configuration we pretend was cached during a previous
    // online session.
    let config = r#"{
      "version": 2,
      "registries": [
        {
          "schema": "/x/:module([a-z0-9_]*)@:version?/:path*",
          "variables": [
            { "key": "module", "url": "/_vsc/modules/${module}" },
            { "key": "version", "url": "/_vsc/modules/${module}/v/${{version}}" },
            { "key": "path", "url": "/_vsc/modules/${module}/v/${{version}}/p/${path}" }
          ]
        }
      ]
    }"#;
    let config_specifier =
      Url::parse(&format!("{origin}{CONFIG_PATH}")).unwrap();
    // Cache the config with a `cache-control` header that makes it immediately
    // stale, forcing `RespectHeaders` to revalidate (which fails because the
    // origin is unreachable).
    let mut headers = HashMap::new();
    headers.insert("content-type".to_string(), "application/json".to_string());
    headers.insert("cache-control".to_string(), "max-age=0".to_string());
    module_registry
      .http_cache
      .set(&config_specifier, headers, config.as_bytes())
      .unwrap();

    module_registry.enable(origin).await;

    // The cached configuration should have been used despite the failed fetch.
    assert!(module_registry.origins.contains_key(origin));
  }
}
