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

mod interactive;

use std::collections::HashSet;
use std::sync::Arc;

use deno_cache_dir::GlobalOrLocalHttpCache;
use deno_cache_dir::file_fetcher::CacheSetting;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_semver::StackString;
use deno_semver::VersionReq;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq;
use deno_terminal::colors;

use super::CacheTopLevelDepsOptions;
use super::deps::Dep;
use super::deps::DepId;
use super::deps::DepKind;
use super::deps::DepManager;
use super::deps::DepManagerArgs;
use super::deps::PackageLatestVersion;
use crate::args::Flags;
use crate::args::OutdatedFlags;
use crate::factory::CliFactory;
use crate::file_fetcher::CreateCliFileFetcherOptions;
use crate::file_fetcher::create_cli_file_fetcher;
use crate::jsr::JsrFetchResolver;
use crate::npm::NpmFetchResolver;
use crate::npm::PackageInfoLoadError;

/// Packages whose metadata could not be fetched (e.g. unreachable or
/// unauthorized private registries) are dropped from the update check. They are
/// collected here, keyed and deduplicated by `(kind, name)`, so we can warn the
/// user instead of skipping them silently.
type SkippedPackages =
  std::collections::BTreeMap<(DepKind, StackString), Arc<PackageInfoLoadError>>;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct OutdatedPackage {
  kind: DepKind,
  latest: String,
  semver_compatible: String,
  current: String,
  name: StackString,
}

#[allow(clippy::print_stdout, reason = "print method")]
fn print_outdated_table(packages: &[OutdatedPackage]) {
  const HEADINGS: &[&str] = &["Package", "Current", "Update", "Latest"];

  let mut longest_package = 0;
  let mut longest_current = 0;
  let mut longest_update = 0;
  let mut longest_latest = 0;

  for package in packages {
    let name_len = package.kind.scheme().len() + 1 + package.name.len();
    longest_package = longest_package.max(name_len);
    longest_current = longest_current.max(package.current.len());
    longest_update = longest_update.max(package.semver_compatible.len());
    longest_latest = longest_latest.max(package.latest.len());
  }

  let package_column_width = longest_package.max(HEADINGS[0].len()) + 2;
  let current_column_width = longest_current.max(HEADINGS[1].len()) + 2;
  let update_column_width = longest_update.max(HEADINGS[2].len()) + 2;
  let latest_column_width = longest_latest.max(HEADINGS[3].len()) + 2;

  let package_fill = "─".repeat(package_column_width);
  let current_fill = "─".repeat(current_column_width);
  let update_fill = "─".repeat(update_column_width);
  let latest_fill = "─".repeat(latest_column_width);

  println!("┌{package_fill}┬{current_fill}┬{update_fill}┬{latest_fill}┐");
  println!(
    "│ {}{} │ {}{} │ {}{} │ {}{} │",
    colors::intense_blue(HEADINGS[0]),
    " ".repeat(package_column_width - 2 - HEADINGS[0].len()),
    colors::intense_blue(HEADINGS[1]),
    " ".repeat(current_column_width - 2 - HEADINGS[1].len()),
    colors::intense_blue(HEADINGS[2]),
    " ".repeat(update_column_width - 2 - HEADINGS[2].len()),
    colors::intense_blue(HEADINGS[3]),
    " ".repeat(latest_column_width - 2 - HEADINGS[3].len())
  );
  for package in packages {
    println!("├{package_fill}┼{current_fill}┼{update_fill}┼{latest_fill}┤",);

    print!(
      "│ {:<package_column_width$} ",
      format!("{}:{}", package.kind.scheme(), package.name),
      package_column_width = package_column_width - 2
    );
    print!(
      "│ {:<current_column_width$} ",
      package.current,
      current_column_width = current_column_width - 2
    );
    print!(
      "│ {:<update_column_width$} ",
      package.semver_compatible,
      update_column_width = update_column_width - 2
    );
    println!(
      "│ {:<latest_column_width$} │",
      package.latest,
      latest_column_width = latest_column_width - 2
    );
  }

  println!("└{package_fill}┴{current_fill}┴{update_fill}┴{latest_fill}┘",);
}

fn print_suggestion(compatible: bool) {
  log::info!("");
  let (cmd, txt) = if compatible {
    ("", "compatible")
  } else {
    (" --latest", "available")
  };
  log::info!(
    "{}",
    color_print::cformat!(
      "<p(245)>Run</> <u>deno update{}</> <p(245)>to update to the latest {} versions,</>\n<p(245)>or</> <u>deno outdated --help</> <p(245)>for more information.</>",
      cmd,
      txt,
    )
  );
}

fn print_outdated(
  deps: &mut DepManager,
  compatible: bool,
) -> Result<(), AnyError> {
  let mut outdated = Vec::new();
  let mut seen = std::collections::BTreeSet::new();
  let mut skipped = SkippedPackages::new();
  for (dep_id, resolved, latest_versions) in
    deps.deps_with_resolved_latest_versions()
  {
    let dep = deps.get_dep(dep_id);

    collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions);

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

    let latest = {
      let preferred = if compatible {
        &latest_versions.semver_compatible
      } else {
        &latest_versions.latest
      };
      if let Some(v) = preferred {
        v
      } else {
        continue;
      }
    };

    if latest > &resolved
      && seen.insert((dep.kind, dep.req.name.clone(), resolved.version.clone()))
    {
      outdated.push(OutdatedPackage {
        kind: dep.kind,
        name: dep.req.name.clone(),
        current: resolved.version.to_string(),
        latest: latest_versions
          .latest
          .map(|l| l.version.to_string())
          .unwrap_or_default(),
        semver_compatible: latest_versions
          .semver_compatible
          .map(|l| l.version.to_string())
          .unwrap_or_default(),
      })
    }
  }

  if !outdated.is_empty() {
    outdated.sort();
    print_outdated_table(&outdated);
  }

  print_skipped_warning(&skipped);

  if !outdated.is_empty() {
    print_suggestion(compatible);
  }

  Ok(())
}

/// Records a package whose metadata fetch failed, deduplicating by
/// `(kind, name)` and keeping the first reason seen.
fn collect_skipped(
  skipped: &mut SkippedPackages,
  kind: DepKind,
  name: &StackString,
  latest_versions: &PackageLatestVersion,
) {
  if let Some(error) = &latest_versions.fetch_error {
    skipped
      .entry((kind, name.clone()))
      .or_insert_with(|| error.clone());
  }
}

fn print_skipped_warning(skipped: &SkippedPackages) {
  if skipped.is_empty() {
    return;
  }

  log::warn!("");
  log::warn!(
    "{}",
    color_print::cformat!(
      "<yellow>Warning</>: Unable to check updates for {} package(s), their metadata could not be fetched (e.g. private registry auth or network issues).",
      skipped.len(),
    )
  );

  if log::log_enabled!(log::Level::Debug) {
    for ((kind, name), error) in skipped {
      log::warn!(
        "{}",
        color_print::cformat!(
          "  <bold>{}:{}</> (registry: {})",
          kind.scheme(),
          name,
          error.registry_url,
        )
      );
      log::warn!("    Reason: {}", error.reason);
    }
    log::warn!(
      "{}",
      color_print::cformat!(
        "<p(245)>Verify your registry credentials (.npmrc) and network connectivity.</>"
      )
    );
  } else {
    log::warn!(
      "{}",
      color_print::cformat!(
        "<p(245)>Run with </><u>--log-level=debug</><p(245)> for details on which packages could not be checked.</>"
      )
    );
  }
}

pub async fn outdated(
  flags: Arc<Flags>,
  update_flags: OutdatedFlags,
) -> Result<(), AnyError> {
  let factory = CliFactory::from_flags(flags.clone());
  let cli_options = factory.cli_options()?;
  let workspace = cli_options.workspace();
  let http_client = factory.http_client_provider();
  let deps_http_cache = factory.global_http_cache()?;
  let file_fetcher = create_cli_file_fetcher(
    Arc::new(deno_runtime::deno_web::BlobStore::default())
      as Arc<dyn deno_runtime::deno_web::BlobStoreTrait>,
    GlobalOrLocalHttpCache::Global(deps_http_cache.clone()),
    http_client.clone(),
    factory.memory_files().clone(),
    factory.sys(),
    CreateCliFileFetcherOptions {
      allow_remote: true,
      cache_setting: CacheSetting::RespectHeaders,
      download_log_level: log::Level::Trace,
      progress_bar: None,
    },
  );
  let file_fetcher = Arc::new(file_fetcher);
  let npm_fetch_resolver = Arc::new(NpmFetchResolver::new(
    file_fetcher.clone(),
    factory.npmrc()?.clone(),
    factory.npm_version_resolver()?.clone(),
  ));
  let jsr_fetch_resolver = Arc::new(JsrFetchResolver::new(
    file_fetcher.clone(),
    factory.jsr_version_resolver()?.clone(),
  ));

  if !cli_options.start_dir.has_deno_or_pkg_json() {
    bail!(
      "No deno.json or package.json in \"{}\".",
      cli_options.initial_cwd().display(),
    );
  }

  let args = dep_manager_args(
    &factory,
    npm_fetch_resolver.clone(),
    jsr_fetch_resolver.clone(),
  )
  .await?;

  let filter_set = filter::FilterSet::from_filter_strings(
    update_flags.filters.iter().map(|s| s.as_str()),
  )?;

  let filter_fn = |alias: Option<&str>, req: &PackageReq, _: DepKind| {
    if filter_set.is_empty() {
      return true;
    }
    let name = alias.unwrap_or(&req.name);
    filter_set.matches(name)
  };
  let mut deps = if update_flags.recursive {
    super::deps::DepManager::from_workspace(workspace, filter_fn, args)?
  } else {
    super::deps::DepManager::from_workspace_dir(
      &cli_options.start_dir,
      filter_fn,
      args,
    )?
  };

  deps.resolve_versions().await?;

  match update_flags.kind {
    crate::args::OutdatedKind::Update {
      latest,
      interactive,
      lockfile_only,
    } => {
      update(
        deps,
        latest,
        &filter_set,
        interactive,
        flags,
        CacheTopLevelDepsOptions {
          lockfile_only,
          additional_roots: vec![],
        },
      )
      .await?;
    }
    crate::args::OutdatedKind::PrintOutdated { compatible } => {
      print_outdated(&mut deps, compatible)?;
    }
  }

  Ok(())
}

enum ChosenVersionReq {
  Some(VersionReq),
  None { latest_available: bool },
}

fn choose_new_version_req(
  dep: &Dep,
  resolved: Option<&PackageNv>,
  latest_versions: &PackageLatestVersion,
  update_to_latest: bool,
  filter_set: &filter::FilterSet,
) -> ChosenVersionReq {
  let explicit_version_req = filter_set
    .matching_filter(dep.alias.as_deref().unwrap_or(&dep.req.name))
    .version_spec()
    .cloned();

  if let Some(version_req) = explicit_version_req {
    if let Some(resolved) = resolved {
      // todo(nathanwhit): handle tag
      if version_req.tag().is_none() && version_req.matches(&resolved.version) {
        return ChosenVersionReq::None {
          latest_available: false,
        };
      }
    }
    ChosenVersionReq::Some(version_req)
  } else {
    let Some(resolved) = resolved else {
      return ChosenVersionReq::None {
        latest_available: false,
      };
    };
    let Some(preferred) = (if update_to_latest {
      latest_versions.latest.as_ref()
    } else {
      latest_versions.semver_compatible.as_ref()
    }) else {
      return ChosenVersionReq::None {
        latest_available: false,
      };
    };
    // Detect the original operator to preserve it
    let version_req_str = dep.req.version_req.to_string();
    let operator = if version_req_str.starts_with('~') {
      "~"
    } else if version_req_str.starts_with('^') {
      "^"
    } else {
      // Check if it's an exact version (no operator)
      let exact = if let Some(range) = dep.req.version_req.range() {
        range.0[0].start == range.0[0].end
      } else {
        false
      };
      if exact { "" } else { "^" }
    };
    let candidate_version_req = VersionReq::parse_from_specifier(
      format!("{}{}", operator, preferred.version).as_str(),
    )
    .unwrap();
    if preferred.version <= resolved.version
      && (candidate_version_req == dep.req.version_req
        || preferred.version < resolved.version)
    {
      return ChosenVersionReq::None {
        latest_available: !update_to_latest
          && latest_versions
            .latest
            .as_ref()
            .is_some_and(|nv| nv.version > resolved.version),
      };
    }
    ChosenVersionReq::Some(candidate_version_req)
  }
}

struct ToUpdate {
  dep_id: DepId,
  package_name: String,
  current_version: Option<PackageNv>,
  current_version_req: VersionReq,
  new_version_req: VersionReq,
  // Whether the npm registry metadata for this package must be refetched
  // before the post-modification install re-resolves it. See the comment at
  // the blocklist construction below for why.
  force_reload_npm: bool,
}

async fn update(
  mut deps: DepManager,
  update_to_latest: bool,
  filter_set: &filter::FilterSet,
  interactive: bool,
  flags: Arc<Flags>,
  cache_options: CacheTopLevelDepsOptions,
) -> Result<(), AnyError> {
  let mut to_update = Vec::new();

  let mut can_update_to_latest = false;
  let mut skipped = SkippedPackages::new();

  for (dep_id, resolved, latest_versions) in deps
    .deps_with_resolved_latest_versions()
    .into_iter()
    .collect::<Vec<_>>()
  {
    let dep = deps.get_dep(dep_id);
    collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions);
    let new_version_req = choose_new_version_req(
      dep,
      resolved.as_ref(),
      &latest_versions,
      update_to_latest,
      filter_set,
    );
    let new_version_req = match new_version_req {
      ChosenVersionReq::Some(version_req) => version_req,
      ChosenVersionReq::None { latest_available } => {
        can_update_to_latest = can_update_to_latest || latest_available;
        continue;
      }
    };

    // The post-modification install re-resolves each package through the npm
    // installer, which reads the npm registry cache with the default
    // `CacheSetting::Use`. That cache is *separate* from the one behind
    // `latest_versions` (the `NpmFetchResolver` fetched those fresh with
    // `RespectHeaders`), so it can be stale in ways the displayed "latest"
    // versions don't reveal. A stale installer cache re-resolves to whatever
    // version it happens to know about, which can move the lockfile up (hiding
    // a newer in-range version, #35348) or, worse, *down* -- silently
    // downgrading a lockfile entry that was pinned ahead of the stale cache
    // (#35822). Comparing `latest_versions` against the resolved version can't
    // detect this: both can read a fresh newest version while the installer
    // cache lags behind. So refetch the installer's metadata for every npm
    // package we might re-resolve.
    let force_reload_npm = dep.kind == DepKind::Npm;

    to_update.push(ToUpdate {
      dep_id,
      package_name: format!("{}:{}", dep.kind.scheme(), dep.req.name),
      current_version: resolved.clone(),
      current_version_req: dep.req.version_req.clone(),
      new_version_req: new_version_req.clone(),
      force_reload_npm,
    });
  }

  if interactive && !to_update.is_empty() {
    let selected = interactive::select_interactive(
      to_update
        .iter()
        .map(|to_update: &ToUpdate| {
          let dep = deps.get_dep(to_update.dep_id);
          interactive::PackageInfo {
            id: to_update.dep_id,
            current_version_req: to_update.current_version_req.clone(),
            name: dep.alias_or_name().into(),
            kind: dep.kind,
            new_version: to_update.new_version_req.clone(),
          }
        })
        .collect(),
    )?;
    if let Some(selected) = selected {
      to_update.retain(|to_update| selected.contains(&to_update.dep_id));
    } else {
      log::info!("Cancelled, not updating");
      return Ok(());
    }
  }

  if !to_update.is_empty() {
    let lockfile_only = cache_options.lockfile_only;
    if lockfile_only {
      // `--lockfile-only`: don't touch deno.json/package.json. Instead,
      // remove the existing lockfile entries for the deps so the
      // following install re-resolves them to the latest version within
      // their existing ranges.
      deps.invalidate_lockfile_for_update(
        to_update.iter().map(|pkg| pkg.dep_id),
      )?;
    } else {
      for pkg in &to_update {
        deps.update_dep(pkg.dep_id, pkg.new_version_req.clone());
      }

      deps.commit_changes()?;
    }

    // Force fresh npm registry metadata for exactly the packages being
    // updated. The installer's re-resolution reads the npm registry cache with
    // the default `CacheSetting::Use`, so a stale cached packument can hide a
    // newer in-range version and leave the lockfile pinned to the old one --
    // even though `deno outdated` (which fetches packuments fresh) reports the
    // update. Adding each such npm package to the cache blocklist makes
    // `cache_setting()` return `ReloadSome`, refetching metadata for only those
    // packages. See #35348.
    let install_flags = {
      let mut install_flags = (*flags).clone();
      install_flags.cache_blocklist.extend(
        to_update
          .iter()
          .filter(|pkg| pkg.force_reload_npm)
          .map(|pkg| pkg.package_name.clone()),
      );
      Arc::new(install_flags)
    };

    let factory = super::npm_install_after_modification(
      install_flags,
      Some(deps.jsr_fetch_resolver.clone()),
      cache_options,
    )
    .await?;

    let mut updated_to_versions = HashSet::new();
    let args = dep_manager_args(
      &factory,
      deps.npm_fetch_resolver.clone(),
      deps.jsr_fetch_resolver.clone(),
    )
    .await?;

    let mut deps = deps.reloaded_after_modification(args);
    deps.resolve_current_versions().await?;
    for pkg in &to_update {
      if let Some(new_resolved) = deps.resolved_version(pkg.dep_id) {
        if lockfile_only {
          // For lockfile-only, only report deps whose resolved version
          // actually changed in the lockfile.
          if pkg.current_version.as_ref() == Some(new_resolved) {
            continue;
          }
          updated_to_versions.insert((
            pkg.package_name.clone(),
            pkg
              .current_version
              .as_ref()
              .map(|nv| nv.version.to_string())
              .unwrap_or_else(|| "(unresolved)".to_string()),
            new_resolved.version.to_string(),
          ));
        } else {
          updated_to_versions.insert((
            pkg.package_name.clone(),
            pkg.current_version_req.to_string(),
            pkg.new_version_req.to_string(),
          ));
        }
      } else {
        log::warn!(
          "Failed to resolve version for new version requirement: {} -> {}",
          pkg.package_name,
          pkg.new_version_req
        );
      }
    }

    log::info!(
      "Updated {} dependenc{}:",
      updated_to_versions.len(),
      if updated_to_versions.len() == 1 {
        "y"
      } else {
        "ies"
      }
    );
    let mut updated_to_versions =
      updated_to_versions.into_iter().collect::<Vec<_>>();
    updated_to_versions.sort_by(|(k, _, _), (k2, _, _)| k.cmp(k2));
    let max_name = updated_to_versions
      .iter()
      .map(|(name, _, _)| name.len())
      .max()
      .unwrap_or(0);
    let max_old = updated_to_versions
      .iter()
      .map(|(_, maybe_current, _)| maybe_current.len())
      .max()
      .unwrap_or(0);
    let max_new = updated_to_versions
      .iter()
      .map(|(_, _, new_version)| new_version.len())
      .max()
      .unwrap_or(0);

    for (package_name, current_version, new_version) in updated_to_versions {
      log::info!(
        " - {}{}{} {}{} -> {}{}",
        colors::gray(package_name[0..4].to_string()),
        &package_name[4..],
        " ".repeat(max_name - package_name.len()),
        " ".repeat(max_old - current_version.len()),
        colors::gray(&current_version),
        " ".repeat(max_new - new_version.len()),
        colors::green(&new_version),
      );
    }
    if lockfile_only && update_to_latest {
      let note = deno_terminal::colors::intense_blue("note");
      log::info!(
        "{note}: --lockfile-only updates only within existing version requirements.\n      Drop --lockfile-only to update deno.json/package.json requirements.",
      );
    }
  } else {
    let maybe_matching = if filter_set.is_empty() {
      ""
    } else {
      "matching "
    };
    if !update_to_latest && can_update_to_latest {
      let note = deno_terminal::colors::intense_blue("note");
      log::info!(
        "All {maybe_matching}dependencies are at newest compatible versions.\n{note}: newer, incompatible versions are available.\n      Run with `--latest` to update",
      );
    } else {
      log::info!("All {maybe_matching}dependencies are up to date.");
    }
  }

  print_skipped_warning(&skipped);

  Ok(())
}

async fn dep_manager_args(
  factory: &CliFactory,
  npm_fetch_resolver: Arc<NpmFetchResolver>,
  jsr_fetch_resolver: Arc<JsrFetchResolver>,
) -> Result<DepManagerArgs, AnyError> {
  Ok(DepManagerArgs {
    module_load_preparer: factory.module_load_preparer().await?.clone(),
    jsr_fetch_resolver,
    npm_fetch_resolver,
    npm_resolver: factory.npm_resolver().await?.clone(),
    npm_installer: factory.npm_installer().await?.clone(),
    npm_version_resolver: factory.npm_version_resolver()?.clone(),
    progress_bar: factory.text_only_progress_bar().clone(),
    permissions_container: factory.root_permissions_container()?.clone(),
    main_module_graph_container: factory
      .main_module_graph_container()
      .await?
      .clone(),
    lockfile: factory.maybe_lockfile().await?.cloned(),
  })
}

pub(crate) mod filter {
  use deno_core::anyhow::Context;
  use deno_core::anyhow::anyhow;
  use deno_core::error::AnyError;
  use deno_semver::VersionReq;

  enum FilterKind {
    Exclude,
    Include,
  }
  pub struct Filter {
    kind: FilterKind,
    regex: regex::Regex,
    version_spec: Option<VersionReq>,
  }

  fn pattern_to_regex(pattern: &str) -> Result<regex::Regex, AnyError> {
    let escaped = regex::escape(pattern);
    let unescaped_star = escaped.replace(r"\*", ".*");
    Ok(regex::Regex::new(&format!("^{}$", unescaped_star))?)
  }

  impl Filter {
    pub fn version_spec(&self) -> Option<&VersionReq> {
      self.version_spec.as_ref()
    }
    pub fn from_str(input: &str) -> Result<Self, AnyError> {
      let (kind, first_idx) = if input.starts_with('!') {
        (FilterKind::Exclude, 1)
      } else {
        (FilterKind::Include, 0)
      };
      let s = &input[first_idx..];
      let (pattern, version_spec) =
        if let Some(scope_name) = s.strip_prefix('@') {
          if let Some(idx) = scope_name.find('@') {
            let (pattern, version_spec) = s.split_at(idx + 1);
            (
              pattern,
              Some(
                VersionReq::parse_from_specifier(
                  version_spec.trim_start_matches('@'),
                )
                .with_context(|| format!("Invalid filter \"{input}\""))?,
              ),
            )
          } else {
            (s, None)
          }
        } else {
          let mut parts = s.split('@');
          let Some(pattern) = parts.next() else {
            return Err(anyhow!("Invalid filter \"{input}\""));
          };
          (
            pattern,
            parts
              .next()
              .map(VersionReq::parse_from_specifier)
              .transpose()
              .with_context(|| format!("Invalid filter \"{input}\""))?,
          )
        };

      Ok(Filter {
        kind,
        regex: pattern_to_regex(pattern)
          .with_context(|| format!("Invalid filter \"{input}\""))?,
        version_spec,
      })
    }

    pub fn matches(&self, name: &str) -> bool {
      self.regex.is_match(name)
    }
  }

  pub struct FilterSet {
    filters: Vec<Filter>,
    has_exclude: bool,
    has_include: bool,
  }
  impl FilterSet {
    pub fn from_filter_strings<'a>(
      filter_strings: impl IntoIterator<Item = &'a str>,
    ) -> Result<Self, AnyError> {
      let filters = filter_strings
        .into_iter()
        .map(Filter::from_str)
        .collect::<Result<Vec<_>, _>>()?;
      let has_exclude = filters
        .iter()
        .any(|f| matches!(f.kind, FilterKind::Exclude));
      let has_include = filters
        .iter()
        .any(|f| matches!(f.kind, FilterKind::Include));
      Ok(FilterSet {
        filters,
        has_exclude,
        has_include,
      })
    }

    pub fn is_empty(&self) -> bool {
      self.filters.is_empty()
    }

    pub fn matches(&self, name: &str) -> bool {
      self.matching_filter(name).is_included()
    }

    pub fn matching_filter(&self, name: &str) -> MatchResult<'_> {
      if self.filters.is_empty() {
        return MatchResult::Included;
      }
      let mut matched = None;
      for filter in &self.filters {
        match filter.kind {
          FilterKind::Include => {
            if matched.is_none() && filter.matches(name) {
              matched = Some(filter);
            }
          }
          FilterKind::Exclude => {
            if filter.matches(name) {
              return MatchResult::Excluded;
            }
          }
        }
      }
      if let Some(filter) = matched {
        MatchResult::Matches(filter)
      } else if self.has_exclude && !self.has_include {
        MatchResult::Included
      } else {
        MatchResult::Excluded
      }
    }
  }

  pub enum MatchResult<'a> {
    Matches(&'a Filter),
    Included,
    Excluded,
  }

  impl MatchResult<'_> {
    pub fn version_spec(&self) -> Option<&VersionReq> {
      match self {
        MatchResult::Matches(filter) => filter.version_spec(),
        _ => None,
      }
    }
    pub fn is_included(&self) -> bool {
      matches!(self, MatchResult::Included | MatchResult::Matches(_))
    }
  }

  #[cfg(test)]
  mod test {
    fn matches_filters<'a, 'b>(
      filters: impl IntoIterator<Item = &'a str>,
      name: &str,
    ) -> bool {
      let filters = super::FilterSet::from_filter_strings(filters).unwrap();
      filters.matches(name)
    }

    fn version_spec(s: &str) -> deno_semver::VersionReq {
      deno_semver::VersionReq::parse_from_specifier(s).unwrap()
    }

    #[test]
    fn basic_glob() {
      assert!(matches_filters(["foo*"], "foo"));
      assert!(matches_filters(["foo*"], "foobar"));
      assert!(!matches_filters(["foo*"], "barfoo"));

      assert!(matches_filters(["*foo"], "foo"));
      assert!(matches_filters(["*foo"], "barfoo"));
      assert!(!matches_filters(["*foo"], "foobar"));

      assert!(matches_filters(["@scope/foo*"], "@scope/foobar"));
    }

    #[test]
    fn basic_glob_with_version() {
      assert!(matches_filters(["foo*@1"], "foo",));
      assert!(matches_filters(["foo*@1"], "foobar",));
      assert!(matches_filters(["foo*@1"], "foo-bar",));
      assert!(!matches_filters(["foo*@1"], "barfoo",));
      assert!(matches_filters(["@scope/*@1"], "@scope/foo"));
    }

    #[test]
    fn glob_exclude() {
      assert!(!matches_filters(["!foo*"], "foo"));
      assert!(!matches_filters(["!foo*"], "foobar"));
      assert!(matches_filters(["!foo*"], "barfoo"));

      assert!(!matches_filters(["!*foo"], "foo"));
      assert!(!matches_filters(["!*foo"], "barfoo"));
      assert!(matches_filters(["!*foo"], "foobar"));

      assert!(!matches_filters(["!@scope/foo*"], "@scope/foobar"));
    }

    #[test]
    fn multiple_globs() {
      assert!(matches_filters(["foo*", "bar*"], "foo"));
      assert!(matches_filters(["foo*", "bar*"], "bar"));
      assert!(!matches_filters(["foo*", "bar*"], "baz"));

      assert!(matches_filters(["foo*", "!bar*"], "foo"));
      assert!(!matches_filters(["foo*", "!bar*"], "bar"));
      assert!(matches_filters(["foo*", "!bar*"], "foobar"));
      assert!(!matches_filters(["foo*", "!*bar"], "foobar"));
      assert!(!matches_filters(["foo*", "!*bar"], "baz"));

      let filters =
        super::FilterSet::from_filter_strings(["foo*@1", "bar*@2"]).unwrap();

      assert_eq!(
        filters.matching_filter("foo").version_spec().cloned(),
        Some(version_spec("1"))
      );

      assert_eq!(
        filters.matching_filter("bar").version_spec().cloned(),
        Some(version_spec("2"))
      );
    }
  }
}
