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

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::fmt::Write as _;
use std::future::poll_fn;
use std::io::Write;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::task::Poll;
use std::time::Duration;
use std::time::Instant;

use deno_ast::MediaType;
use deno_cache_dir::file_fetcher::File;
use deno_config::glob::FilePatterns;
use deno_config::glob::WalkEntry;
use deno_core::ModuleSpecifier;
use deno_core::OpState;
use deno_core::PollEventLoopOptions;
use deno_core::anyhow;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::error::CoreError;
use deno_core::error::CoreErrorKind;
use deno_core::error::JsError;
use deno_core::futures::StreamExt;
use deno_core::futures::future;
use deno_core::futures::stream;
use deno_core::located_script_name;
use deno_core::unsync::spawn;
use deno_core::unsync::spawn_blocking;
use deno_core::url::Url;
use deno_core::v8;
use deno_error::JsErrorBox;
use deno_npm_installer::graph::NpmCachingStrategy;
use deno_runtime::WorkerExecutionMode;
use deno_runtime::coverage::CoverageCollector;
use deno_runtime::deno_io::Stdio;
use deno_runtime::deno_io::StdioPipe;
use deno_runtime::deno_permissions::Permissions;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::permissions::RuntimePermissionDescriptorParser;
use deno_runtime::tokio_util::create_and_run_current_thread;
use deno_runtime::worker::MainWorker;
use indexmap::IndexMap;
use indexmap::IndexSet;
use log::Level;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use rand::seq::SliceRandom;
use regex::Regex;
use serde::Deserialize;
use tokio::sync::mpsc::UnboundedSender;

use crate::args::CliOptions;
use crate::args::Flags;
use crate::args::TestFlags;
use crate::args::TestReporterConfig;
use crate::args::TypeCheckModeExt;
use crate::colors;
use crate::display;
use crate::factory::CliFactory;
use crate::file_fetcher::CliFileFetcher;
use crate::graph_container::CheckSpecifiersOptions;
use crate::graph_util::has_graph_root_local_dependent_changed;
use crate::ops;
use crate::sys::CliSys;
use crate::util::extract::extract_doc_tests;
use crate::util::file_watcher;
use crate::util::fs::CollectSpecifiersOptions;
use crate::util::fs::canonicalize_path;
use crate::util::fs::collect_specifiers;
use crate::util::git::run_git;
use crate::util::path::get_extension;
use crate::util::path::is_script_ext;
use crate::util::path::matches_pattern_or_exact_path;
use crate::worker::CliMainWorkerFactory;
use crate::worker::CreateCustomWorkerError;

mod channel;
pub mod fmt;
pub mod reporters;
mod sanitizers;

pub use channel::TestEventReceiver;
pub use channel::TestEventSender;
pub use channel::TestEventWorkerSender;
pub use channel::create_single_test_event_channel;
pub use channel::create_test_event_channel;
use fmt::format_sanitizer_diff;
pub use fmt::format_test_error;
use reporters::CompoundTestReporter;
use reporters::DotTestReporter;
use reporters::JunitTestReporter;
use reporters::PrettyTestReporter;
use reporters::TapTestReporter;
use reporters::TestReporter;

use crate::tools::coverage::CoverageThresholdError;
use crate::tools::coverage::cover_files;
use crate::tools::coverage::reporter;
use crate::tools::test::channel::ChannelClosedError;

static SLOW_TEST_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
  let base_timeout = env::var("DENO_SLOW_TEST_TIMEOUT").unwrap_or_default();
  base_timeout.parse().unwrap_or(60).max(1)
});

/// The test mode is used to determine how a specifier is to be tested.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TestMode {
  /// Test as documentation, type-checking fenced code blocks.
  Documentation,
  /// Test as an executable module, loading the module into the isolate and running each test it
  /// defines.
  Executable,
  /// Test as both documentation and an executable module.
  Both,
}

impl TestMode {
  fn union(self, other: Self) -> Self {
    if self == other { self } else { Self::Both }
  }

  /// Returns `true` if the test mode indicates that code snippet extraction is
  /// needed.
  fn needs_test_extraction(&self) -> bool {
    matches!(self, Self::Documentation | Self::Both)
  }

  /// Returns `true` if the test mode indicates that the test should be
  /// type-checked and run.
  fn needs_test_run(&self) -> bool {
    matches!(self, Self::Executable | Self::Both)
  }
}

#[derive(Clone, Debug, Default)]
pub struct TestFilter {
  pub substring: Option<String>,
  pub regex: Option<Regex>,
  pub include: Option<Vec<String>>,
  pub exclude: Vec<String>,
}

impl TestFilter {
  pub fn includes(&self, name: &String) -> bool {
    if let Some(substring) = &self.substring
      && !name.contains(substring)
    {
      return false;
    }
    if let Some(regex) = &self.regex
      && !regex.is_match(name)
    {
      return false;
    }
    if let Some(include) = &self.include
      && !include.contains(name)
    {
      return false;
    }
    if self.exclude.contains(name) {
      return false;
    }
    true
  }

  pub fn from_flag(flag: &Option<String>) -> Self {
    let mut substring = None;
    let mut regex = None;
    if let Some(flag) = flag {
      if flag.starts_with('/') && flag.ends_with('/') {
        let rs = flag.trim_start_matches('/').trim_end_matches('/');
        regex =
          Some(Regex::new(rs).unwrap_or_else(|_| Regex::new("$^").unwrap()));
      } else {
        substring = Some(flag.clone());
      }
    }
    Self {
      substring,
      regex,
      ..Default::default()
    }
  }

  /// Returns `true` when this filter may exclude some tests from running.
  pub fn is_active(&self) -> bool {
    self.substring.is_some()
      || self.regex.is_some()
      || self.include.is_some()
      || !self.exclude.is_empty()
  }
}

#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TestLocation {
  pub file_name: String,
  pub line_number: u32,
  pub column_number: u32,
}

#[derive(Default)]
pub(crate) struct TestContainer {
  descriptions: TestDescriptions,
  test_functions: Vec<v8::Global<v8::Function>>,
  test_hooks: TestHooks,
  registration_phase_ended: bool,
}

#[derive(Default)]
pub(crate) struct TestHooks {
  pub before_all: Vec<v8::Global<v8::Function>>,
  pub before_each: Vec<v8::Global<v8::Function>>,
  pub after_each: Vec<v8::Global<v8::Function>>,
  pub after_all: Vec<v8::Global<v8::Function>>,
}

impl TestContainer {
  pub fn with_registration_phase_ended() -> Self {
    Self {
      registration_phase_ended: true,
      ..Default::default()
    }
  }

  pub fn register(
    &mut self,
    description: TestDescription,
    function: v8::Global<v8::Function>,
  ) -> Result<(), JsErrorBox> {
    if self.registration_phase_ended {
      return Err(JsErrorBox::type_error(
        "Nested Deno.test() calls are not supported. Use t.step() for nested tests.",
      ));
    }
    self.descriptions.tests.insert(description.id, description);
    self.test_functions.push(function);
    Ok(())
  }

  pub fn register_hook(
    &mut self,
    hook_type: String,
    function: v8::Global<v8::Function>,
  ) {
    match hook_type.as_str() {
      "beforeAll" => self.test_hooks.before_all.push(function),
      "beforeEach" => self.test_hooks.before_each.push(function),
      "afterEach" => self.test_hooks.after_each.push(function),
      "afterAll" => self.test_hooks.after_all.push(function),
      _ => {}
    }
  }

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

#[derive(Default, Debug)]
pub struct TestDescriptions {
  tests: IndexMap<usize, TestDescription>,
}

impl TestDescriptions {
  pub fn len(&self) -> usize {
    self.tests.len()
  }

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

impl<'a> IntoIterator for &'a TestDescriptions {
  type Item = <&'a IndexMap<usize, TestDescription> as IntoIterator>::Item;
  type IntoIter =
    <&'a IndexMap<usize, TestDescription> as IntoIterator>::IntoIter;
  fn into_iter(self) -> Self::IntoIter {
    (&self.tests).into_iter()
  }
}

#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TestDescription {
  pub id: usize,
  pub name: String,
  pub ignore: bool,
  pub only: bool,
  pub sanitize_only: bool,
  pub origin: String,
  pub location: TestLocation,
  pub sanitize_ops: bool,
  pub sanitize_resources: bool,
  pub timeout_ms: Option<u32>,
  /// Per-test `retry` option: number of times to re-run the test if it fails
  /// (the test passes if any attempt passes). `None` means the test did not set
  /// the option and inherits the `--retry` flag default; `Some(0)` is an
  /// explicit opt-out that takes precedence over the flag.
  pub retry: Option<u32>,
  /// Per-test `repeats` option: number of additional times to repeat the test
  /// (every repetition must pass). `None` means the test did not set the option
  /// and inherits the `--repeats` flag default; `Some(0)` is an explicit
  /// opt-out that takes precedence over the flag.
  pub repeats: Option<u32>,
}

/// May represent a failure of a test or test step.
#[derive(Debug, Clone, PartialEq, Deserialize, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TestFailureDescription {
  pub id: usize,
  pub name: String,
  pub origin: String,
  pub location: TestLocation,
}

impl From<&TestDescription> for TestFailureDescription {
  fn from(value: &TestDescription) -> Self {
    Self {
      id: value.id,
      name: value.name.clone(),
      origin: value.origin.clone(),
      location: value.location.clone(),
    }
  }
}

#[derive(Debug, Default, Clone, PartialEq)]
pub struct TestFailureFormatOptions {
  pub hide_stacktraces: bool,
  pub strip_ascii_color: bool,
  pub initial_cwd: Option<Url>,
}

#[allow(clippy::derive_partial_eq_without_eq, reason = "not important")]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TestFailure {
  JsError(Box<JsError>),
  FailedSteps(usize),
  IncompleteSteps,
  PendingPromiseResolution,
  Leaked(Vec<String>, Vec<String>), // Details, trailer notes
  TimedOut(u32),
  // The rest are for steps only.
  Incomplete,
  OverlapsWithSanitizers(IndexSet<String>), // Long names of overlapped tests
  HasSanitizersAndOverlaps(IndexSet<String>), // Long names of overlapped tests
}

impl TestFailure {
  pub fn format(
    &self,
    options: &TestFailureFormatOptions,
  ) -> Cow<'static, str> {
    match self {
      TestFailure::JsError(js_error) => {
        Cow::Owned(format_test_error(js_error, options))
      }
      TestFailure::FailedSteps(1) => Cow::Borrowed("1 test step failed."),
      TestFailure::FailedSteps(n) => {
        Cow::Owned(format!("{} test steps failed.", n))
      }
      TestFailure::IncompleteSteps => Cow::Borrowed(
        "Completed while steps were still running. Ensure all steps are awaited with `await t.step(...)`.",
      ),
      TestFailure::PendingPromiseResolution => Cow::Borrowed(
        "Promise resolution is still pending but the event loop has already resolved.",
      ),
      TestFailure::Incomplete => Cow::Borrowed(
        "Didn't complete before parent. Await step with `await t.step(...)`.",
      ),
      TestFailure::Leaked(details, trailer_notes) => {
        let mut f = String::new();
        write!(f, "Leaks detected:").ok();
        for detail in details {
          write!(f, "\n  - {}", detail).ok();
        }
        for trailer in trailer_notes {
          write!(f, "\n{}", trailer).ok();
        }
        Cow::Owned(f)
      }
      TestFailure::TimedOut(ms) => {
        Cow::Owned(format!("Test timed out after {}ms.", ms))
      }
      TestFailure::OverlapsWithSanitizers(long_names) => {
        let mut f = String::new();
        write!(f, "Started test step while another test step with sanitizers was running:").ok();
        for long_name in long_names {
          write!(f, "\n  * {}", long_name).ok();
        }
        Cow::Owned(f)
      }
      TestFailure::HasSanitizersAndOverlaps(long_names) => {
        let mut f = String::new();
        write!(f, "Started test step with sanitizers while another test step was running:").ok();
        for long_name in long_names {
          write!(f, "\n  * {}", long_name).ok();
        }
        Cow::Owned(f)
      }
    }
  }

  pub fn overview(&self) -> String {
    match self {
      TestFailure::JsError(js_error) => js_error.exception_message.clone(),
      TestFailure::FailedSteps(1) => "1 test step failed".to_string(),
      TestFailure::FailedSteps(n) => format!("{n} test steps failed"),
      TestFailure::IncompleteSteps => {
        "Completed while steps were still running".to_string()
      }
      TestFailure::PendingPromiseResolution => {
        "Promise resolution is still pending".to_string()
      }
      TestFailure::Incomplete => "Didn't complete before parent".to_string(),
      TestFailure::Leaked(_, _) => "Leaks detected".to_string(),
      TestFailure::TimedOut(ms) => format!("Timed out after {}ms", ms),
      TestFailure::OverlapsWithSanitizers(_) => {
        "Started test step while another test step with sanitizers was running"
          .to_string()
      }
      TestFailure::HasSanitizersAndOverlaps(_) => {
        "Started test step with sanitizers while another test step was running"
          .to_string()
      }
    }
  }

  pub fn error_location(&self) -> Option<TestLocation> {
    let TestFailure::JsError(js_error) = self else {
      return None;
    };
    // The first line of user code comes above the test file.
    // The call stack usually contains the top 10 frames, and cuts off after that.
    // We need to explicitly check for the test runner here.
    // - Checking for a `ext:` is not enough, since other Deno `ext:`s can appear in the call stack.
    // - This check guarantees that the next frame is inside of the Deno.test(),
    //   and not somewhere else.
    const TEST_RUNNER: &str = "ext:cli/40_test.js";
    let runner_frame_index = js_error
      .frames
      .iter()
      .position(|f| f.file_name.as_deref() == Some(TEST_RUNNER))?;
    let frame = js_error
      .frames
      .split_at(runner_frame_index)
      .0
      .iter()
      .rfind(|f| {
        f.file_name.as_ref().is_some_and(|f| {
          f.starts_with("file:") && !f.contains("node_modules")
        })
      })?;
    let file_name = frame.file_name.as_ref()?.clone();
    // Turn into zero based indices
    let line_number = frame.line_number.map(|v| v - 1)? as u32;
    let column_number = frame.column_number.map(|v| v - 1).unwrap_or(0) as u32;
    Some(TestLocation {
      file_name,
      line_number,
      column_number,
    })
  }

  fn format_label(&self) -> String {
    match self {
      TestFailure::Incomplete => colors::gray("INCOMPLETE").to_string(),
      _ => colors::red("FAILED").to_string(),
    }
  }

  fn format_inline_summary(&self) -> Option<String> {
    match self {
      TestFailure::FailedSteps(1) => Some("due to 1 failed step".to_string()),
      TestFailure::FailedSteps(n) => Some(format!("due to {} failed steps", n)),
      TestFailure::IncompleteSteps => {
        Some("due to incomplete steps".to_string())
      }
      _ => None,
    }
  }

  fn hide_in_summary(&self) -> bool {
    // These failure variants are hidden in summaries because they are caused
    // by child errors that will be summarized separately.
    matches!(
      self,
      TestFailure::FailedSteps(_) | TestFailure::IncompleteSteps
    )
  }
}

#[allow(clippy::derive_partial_eq_without_eq, reason = "not important")]
#[derive(Debug, Clone, PartialEq, deno_core::FromV8)]
pub enum TestResult {
  Ok,
  Ignored,
  #[from_v8(serde)]
  Failed(TestFailure),
  Cancelled,
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestStepDescription {
  pub id: usize,
  pub name: String,
  pub origin: String,
  pub location: TestLocation,
  pub level: usize,
  pub parent_id: usize,
  pub root_id: usize,
  pub root_name: String,
}

#[allow(clippy::derive_partial_eq_without_eq, reason = "not important")]
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TestStepResult {
  Ok,
  Ignored,
  Failed(TestFailure),
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TestPlan {
  pub origin: String,
  pub total: usize,
  pub filtered_out: usize,
  pub used_only: bool,
}

#[derive(Debug)]
pub enum TestEvent {
  Register(Arc<TestDescriptions>),
  Plan(TestPlan),
  Wait(usize),
  Output(Vec<u8>),
  Slow(usize, Duration),
  Result(usize, TestResult, Duration),
  /// A test attempt failed but will be retried (`retry` option). Carries the
  /// test id, the zero-based attempt index that failed, and the failure. This
  /// is informational only and does not count toward the test's final result.
  Retry(usize, u32, TestFailure),
  /// A test is starting a fresh repetition (`repeats` option). Carries the test
  /// id and the one-based repetition index. Lets the reporter drop the previous
  /// repetition's step results so they aren't tallied more than once.
  Repeat(usize, u32),
  UncaughtError(String, Box<JsError>),
  StepRegister(TestStepDescription),
  StepWait(usize),
  StepResult(usize, TestStepResult, Duration),
  /// Reports how many snapshots were updated and which were removed for a
  /// test module after it finished running with `--update-snapshots`.
  SnapshotSummary(TestSnapshotSummary),
  /// Indicates that this worker has completed running tests.
  Completed,
  /// Indicates that the user has cancelled the test run with Ctrl+C and
  /// the run should be aborted.
  Sigint,
  /// Indicates that a test called `Deno.exit()` while the exit sanitizer was
  /// disabled (`sanitizeExit: false`). The test run should be aborted, a
  /// message printed, and the process should exit with the given code. This
  /// ensures that buffered output is reliably flushed before exiting, instead
  /// of letting the test silently terminate the process.
  Exit(i32),
  /// Indicates that the test isolate called `Deno.exit()` outside of any test
  /// function (top-level code, or in the `unload` event). The isolate has
  /// been terminated; the test runner should report the exit code and
  /// continue with any remaining specifiers instead of killing the process.
  IsolateExit(String, i32),
  /// Used by the REPL to force a report to end without closing the worker
  /// or receiver.
  ForceEndReport,
}

impl TestEvent {
  // Certain messages require us to ensure that all output has been drained to ensure proper
  // interleaving of output messages.
  pub fn requires_stdio_sync(&self) -> bool {
    matches!(
      self,
      TestEvent::Plan(..)
        | TestEvent::Result(..)
        | TestEvent::Retry(..)
        | TestEvent::Repeat(..)
        | TestEvent::StepWait(..)
        | TestEvent::StepResult(..)
        | TestEvent::UncaughtError(..)
        | TestEvent::ForceEndReport
        | TestEvent::Completed
        | TestEvent::Exit(..)
        | TestEvent::IsolateExit(..)
    )
  }
}

#[derive(Debug, Clone, Deserialize)]
pub struct TestSummary {
  pub total: usize,
  pub passed: usize,
  pub failed: usize,
  /// Number of tests that passed but only after one or more retries.
  pub flaky: usize,
  pub ignored: usize,
  pub passed_steps: usize,
  pub failed_steps: usize,
  pub ignored_steps: usize,
  pub filtered_out: usize,
  pub measured: usize,
  pub snapshots_updated: usize,
  pub snapshots_removed: Vec<String>,
  pub failures: Vec<(TestFailureDescription, TestFailure)>,
  pub uncaught_errors: Vec<(String, Box<JsError>)>,
}

/// Snapshot statistics for a single test module run with
/// `--update-snapshots`, sent after all of its tests have finished.
#[derive(Debug, Clone)]
pub struct TestSnapshotSummary {
  pub updated: usize,
  pub removed: Vec<String>,
}

#[derive(Debug, Clone)]
struct TestSpecifiersOptions {
  cwd: Url,
  concurrent_jobs: NonZeroUsize,
  fail_fast: Option<NonZeroUsize>,
  log_level: Option<log::Level>,
  filter: bool,
  specifier: TestSpecifierOptions,
  reporter: TestReporterConfig,
  junit_path: Option<String>,
  hide_stacktraces: bool,
  /// `(index, count)` with a 1-based index, from `--shard`. Selects a subset
  /// of test files so a run can be split across machines.
  shard: Option<(usize, usize)>,
}

#[derive(Debug, Default, Clone)]
pub struct TestSpecifierOptions {
  pub shuffle: Option<u64>,
  pub filter: TestFilter,
  /// Default number of retries applied to tests that don't set their own
  /// `retry` option (from the `--retry` flag).
  pub retry: u32,
  /// Default number of repeats applied to tests that don't set their own
  /// `repeats` option (from the `--repeats` flag).
  pub repeats: u32,
  pub trace_leaks: bool,
  pub sanitize_ops: bool,
  pub sanitize_resources: bool,
  pub update_snapshots: bool,
}

impl TestSummary {
  pub fn new() -> TestSummary {
    TestSummary {
      total: 0,
      passed: 0,
      failed: 0,
      flaky: 0,
      ignored: 0,
      passed_steps: 0,
      failed_steps: 0,
      ignored_steps: 0,
      filtered_out: 0,
      measured: 0,
      snapshots_updated: 0,
      snapshots_removed: Vec::new(),
      failures: Vec::new(),
      uncaught_errors: Vec::new(),
    }
  }

  fn has_failed(&self) -> bool {
    self.failed > 0 || !self.failures.is_empty()
  }
}

fn get_test_reporter(options: &TestSpecifiersOptions) -> Box<dyn TestReporter> {
  let parallel = options.concurrent_jobs.get() > 1;
  let failure_format_options = TestFailureFormatOptions {
    hide_stacktraces: options.hide_stacktraces,
    strip_ascii_color: false,
    initial_cwd: Some(options.cwd.clone()),
  };
  let reporter: Box<dyn TestReporter> = match &options.reporter {
    TestReporterConfig::Dot => Box::new(DotTestReporter::new(
      options.cwd.clone(),
      failure_format_options,
    )),
    TestReporterConfig::Pretty => Box::new(PrettyTestReporter::new(
      parallel,
      options.log_level != Some(Level::Error),
      options.filter,
      false,
      options.cwd.clone(),
      failure_format_options,
    )),
    TestReporterConfig::Junit => Box::new(JunitTestReporter::new(
      options.cwd.clone(),
      "-".to_string(),
      TestFailureFormatOptions {
        strip_ascii_color: true,
        ..failure_format_options
      },
    )),
    TestReporterConfig::Tap => Box::new(TapTestReporter::new(
      options.cwd.clone(),
      options.concurrent_jobs > NonZeroUsize::new(1).unwrap(),
      failure_format_options,
    )),
  };

  if let Some(junit_path) = &options.junit_path {
    let junit = Box::new(JunitTestReporter::new(
      options.cwd.clone(),
      junit_path.to_string(),
      TestFailureFormatOptions {
        hide_stacktraces: options.hide_stacktraces,
        strip_ascii_color: true,
        initial_cwd: Some(options.cwd.clone()),
      },
    ));
    return Box::new(CompoundTestReporter::new(vec![reporter, junit]));
  }

  reporter
}

#[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
async fn configure_main_worker(
  worker_factory: Arc<CliMainWorkerFactory>,
  specifier: &Url,
  preload_modules: Vec<Url>,
  require_modules: Vec<Url>,
  permissions_container: PermissionsContainer,
  worker_sender: TestEventWorkerSender,
  options: &TestSpecifierOptions,
  sender: UnboundedSender<crate::ops::jupyter::IopubMessage>,
) -> Result<(Option<CoverageCollector>, MainWorker), CreateCustomWorkerError> {
  let mut worker = worker_factory
    .create_custom_worker(
      WorkerExecutionMode::Test,
      specifier.clone(),
      preload_modules,
      require_modules,
      permissions_container,
      vec![
        ops::testing::deno_test::init(worker_sender.sender),
        ops::lint::deno_lint_ext_for_test::init(),
        ops::jupyter::deno_jupyter_for_test::init(
          sender,
          // deno test does not service stdin requests; supplying a sender keeps
          // the extension uniform with the live kernel build but the rx end is
          // dropped so any `op_jupyter_input` call will resolve to `None`.
          tokio::sync::mpsc::unbounded_channel().0,
        ),
      ],
      Stdio {
        stdin: StdioPipe::inherit(),
        stdout: StdioPipe::file(worker_sender.stdout),
        stderr: StdioPipe::file(worker_sender.stderr),
      },
      None,
    )
    .await?;
  let coverage_collector = worker.maybe_setup_coverage_collector();
  // Store the isolate handle in OpState so that `op_test_isolate_exit` can
  // ask V8 to terminate the isolate when user code calls `Deno.exit()`
  // outside of any test function. Then install the default exit handler -
  // both must be in place before user code (including preload / side
  // modules) gets a chance to run.
  let isolate_handle = worker.v8_isolate_handle();
  worker
    .op_state()
    .borrow_mut()
    .put(ops::testing::TestIsolateHandle(isolate_handle));
  if options.update_snapshots {
    // Authorizes `op_test_snapshot_write` for this isolate. Only ever set
    // from the `--update-snapshots` CLI flag, never from JS.
    worker
      .op_state()
      .borrow_mut()
      .put(ops::testing::SnapshotUpdateMode);
  }
  worker
    .execute_script_static(
      located_script_name!(),
      "Deno[Deno.internal].installTestIsolateExitHandler();",
    )
    .map_err(|e| CoreErrorKind::Js(e).into_box())?;
  if options.trace_leaks {
    worker
      .execute_script_static(
        located_script_name!(),
        "Deno[Deno.internal].core.setLeakTracingEnabled(true);",
      )
      .map_err(|e| CoreErrorKind::Js(e).into_box())?;
  }
  if options.sanitize_ops {
    worker
      .execute_script_static(
        located_script_name!(),
        "Deno[Deno.internal].testSanitizeOps = true;",
      )
      .map_err(|e| CoreErrorKind::Js(e).into_box())?;
  }
  if options.sanitize_resources {
    worker
      .execute_script_static(
        located_script_name!(),
        "Deno[Deno.internal].testSanitizeResources = true;",
      )
      .map_err(|e| CoreErrorKind::Js(e).into_box())?;
  }

  let op_state = worker.op_state();

  let check_res =
    |res: Result<(), CoreError>| match res.map_err(|err| err.into_kind()) {
      Ok(()) => Ok(()),
      Err(CoreErrorKind::Js(err)) => {
        // If the failure was caused by user code calling `Deno.exit()` at
        // top level, the isolate-exit event has already been reported.
        // Swallow the resulting termination error instead of double-reporting
        // it as an uncaught error.
        if op_state.borrow().has::<ops::testing::IsolateExitInfo>() {
          return Ok(());
        }
        TestEventTracker::new(op_state.clone())
          .uncaught_error(specifier.to_string(), err)
          .map_err(|e| CoreErrorKind::JsBox(JsErrorBox::from_err(e)).into_box())
      }
      Err(err) => Err(err.into_box()),
    };

  check_res(worker.execute_preload_modules().await)?;
  if op_state.borrow().has::<ops::testing::IsolateExitInfo>() {
    worker.cancel_terminate_execution();
    return Ok((coverage_collector, worker.into_main_worker()));
  }
  check_res(worker.execute_side_module().await)?;
  if op_state.borrow().has::<ops::testing::IsolateExitInfo>() {
    worker.cancel_terminate_execution();
  }

  let worker = worker.into_main_worker();

  Ok((coverage_collector, worker))
}

/// Test a single specifier as documentation containing test programs, an executable test module or
/// both.
#[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
pub async fn test_specifier(
  worker_factory: Arc<CliMainWorkerFactory>,
  permissions_container: PermissionsContainer,
  specifier: ModuleSpecifier,
  preload_modules: Vec<ModuleSpecifier>,
  require_modules: Vec<ModuleSpecifier>,
  worker_sender: TestEventWorkerSender,
  fail_fast_tracker: FailFastTracker,
  options: TestSpecifierOptions,
) -> Result<(), AnyError> {
  if fail_fast_tracker.should_stop() {
    return Ok(());
  }
  let jupyter_channel = tokio::sync::mpsc::unbounded_channel();
  let (coverage_collector, mut worker) = configure_main_worker(
    worker_factory,
    &specifier,
    preload_modules,
    require_modules,
    permissions_container,
    worker_sender,
    &options,
    jupyter_channel.0,
  )
  .await?;
  let event_tracker = TestEventTracker::new(worker.js_runtime.op_state());

  // If user code already called `Deno.exit()` during top-level evaluation in
  // `configure_main_worker`, the isolate-exit event has already been sent.
  // Skip the rest of the lifecycle - the isolate is terminated and there are
  // no tests to run.
  if worker
    .js_runtime
    .op_state()
    .borrow()
    .has::<ops::testing::IsolateExitInfo>()
  {
    return Ok(());
  }

  let result = test_specifier_inner(
    &mut worker,
    coverage_collector,
    specifier.clone(),
    fail_fast_tracker,
    &event_tracker,
    options,
  )
  .await;

  // If user code called `Deno.exit()` from inside `dispatch_load_event`,
  // `run_tests_for_worker`, or `dispatch_unload_event`, the V8 isolate was
  // forcibly terminated and the call surfaces as a JS error. The
  // `op_test_isolate_exit` op already queued an `IsolateExit` event with the
  // requested exit code, so don't double-report the resulting termination as
  // an uncaught error.
  let isolate_exited = worker
    .js_runtime
    .op_state()
    .borrow()
    .has::<ops::testing::IsolateExitInfo>();
  if isolate_exited {
    // Clear V8's "terminating" flag so the worker can be cleanly dropped.
    worker.js_runtime.v8_isolate().cancel_terminate_execution();
  }

  match result {
    Ok(()) => Ok(()),
    Err(_) if isolate_exited => Ok(()),
    Err(TestSpecifierError::Core(err)) => match err.into_kind() {
      CoreErrorKind::Js(err) => {
        event_tracker.uncaught_error(specifier.to_string(), err)?;
        Ok(())
      }
      err => Err(err.into_box().into()),
    },
    Err(e) => Err(e.into()),
  }
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum TestSpecifierError {
  #[class(inherit)]
  #[error(transparent)]
  Core(#[from] CoreError),
  #[class(inherit)]
  #[error(transparent)]
  RunTestsForWorker(#[from] RunTestsForWorkerErr),
}

/// Test a single specifier as documentation containing test programs, an executable test module or
/// both.
async fn test_specifier_inner(
  worker: &mut MainWorker,
  mut coverage_collector: Option<CoverageCollector>,
  specifier: ModuleSpecifier,
  fail_fast_tracker: FailFastTracker,
  event_tracker: &TestEventTracker,
  options: TestSpecifierOptions,
) -> Result<(), TestSpecifierError> {
  // Ensure that there are no pending exceptions before we start running tests
  worker.run_up_to_duration(Duration::from_millis(0)).await?;

  worker
    .dispatch_load_event()
    .map_err(|e| CoreErrorKind::Js(e).into_box())?;

  run_tests_for_worker(
    worker,
    &specifier,
    &options,
    &fail_fast_tracker,
    event_tracker,
  )
  .await?;

  // Ignore `defaultPrevented` of the `beforeunload` event. We don't allow the
  // event loop to continue beyond what's needed to await results.
  worker
    .dispatch_beforeunload_event()
    .map_err(|e| CoreErrorKind::Js(e).into_box())?;
  worker
    .dispatch_unload_event()
    .map_err(|e| CoreErrorKind::Js(e).into_box())?;

  // Ensure all output has been flushed
  _ = worker
    .js_runtime
    .op_state()
    .borrow_mut()
    .borrow_mut::<TestEventSender>()
    .flush();

  // Ensure the worker has settled so we can catch any remaining unhandled rejections. We don't
  // want to wait forever here.
  worker.run_up_to_duration(Duration::from_millis(0)).await?;

  // Stop coverage before waiting for external debugger sessions. Coverage owns
  // a blocking local inspector session, so waiting first would hang forever.
  if let Some(mut coverage_collector) = coverage_collector.take() {
    coverage_collector.stop_collecting()?;
  }

  // If a debugger session is still attached (e.g. Chrome DevTools holding
  // the connection open for an in-progress Performance recording), wait for
  // it to disconnect before tearing down the worker. Mirrors `deno run`'s
  // behavior; otherwise the connection is dropped mid-flight, making it
  // impossible to profile `deno test` runs. See issue #19289.
  worker.wait_for_inspector_session_disconnect().await?;

  Ok(())
}

pub fn worker_has_tests(worker: &mut MainWorker) -> bool {
  let state_rc = worker.js_runtime.op_state();
  let state = state_rc.borrow();
  !state.borrow::<TestContainer>().is_empty()
}

// Each test needs a fresh reqwest connection pool to avoid inter-test weirdness with connections
// failing. If we don't do this, a connection to a test server we just tore down might be re-used in
// the next test.
// TODO(mmastrac): this should be some sort of callback that we can implement for any subsystem
pub fn worker_prepare_for_test(worker: &mut MainWorker) {
  worker
    .js_runtime
    .op_state()
    .borrow_mut()
    .try_take::<deno_runtime::deno_fetch::Client>();
}

/// Yields to tokio to allow async work to process, and then polls
/// the event loop once.
#[must_use = "The event loop result should be checked"]
pub async fn poll_event_loop(worker: &mut MainWorker) -> Result<(), CoreError> {
  // Allow any ops that to do work in the tokio event loop to do so
  tokio::task::yield_now().await;
  // Spin the event loop once
  poll_fn(|cx| {
    if let Poll::Ready(Err(err)) = worker
      .js_runtime
      .poll_event_loop(cx, PollEventLoopOptions::default())
    {
      return Poll::Ready(Err(err));
    }
    Poll::Ready(Ok(()))
  })
  .await
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum RunTestsForWorkerErr {
  #[class(inherit)]
  #[error(transparent)]
  ChannelClosed(#[from] ChannelClosedError),
  #[class(inherit)]
  #[error(transparent)]
  Core(#[from] CoreError),
  #[class(inherit)]
  #[error(transparent)]
  JsErrorBox(#[from] deno_error::JsErrorBox),
}

/// Single watchdog thread for an entire test specifier. Reused across all
/// tests in the file: each test calls `arm()` before running and `disarm()`
/// after, replacing the per-test thread spawn.
struct TestWatchdog {
  shared: std::sync::Arc<WatchdogShared>,
  thread: Option<std::thread::JoinHandle<()>>,
}

struct WatchdogShared {
  mutex: std::sync::Mutex<WatchdogState>,
  cvar: std::sync::Condvar,
}

#[derive(PartialEq, Eq)]
enum WatchdogState {
  Idle,
  Armed { deadline: Instant, fired: bool },
  Shutdown,
}

impl TestWatchdog {
  fn new(js_runtime: &mut deno_core::JsRuntime) -> Self {
    let isolate_handle = js_runtime.v8_isolate().thread_safe_handle();
    let shared = std::sync::Arc::new(WatchdogShared {
      mutex: std::sync::Mutex::new(WatchdogState::Idle),
      cvar: std::sync::Condvar::new(),
    });
    let shared_thread = shared.clone();
    let thread = std::thread::Builder::new()
      .name("deno-test-watchdog".to_string())
      .spawn(move || Self::run(shared_thread, isolate_handle))
      .expect("failed to spawn test watchdog thread");
    Self {
      shared,
      thread: Some(thread),
    }
  }

  fn run(shared: std::sync::Arc<WatchdogShared>, isolate: v8::IsolateHandle) {
    let mut state = shared.mutex.lock().unwrap();
    loop {
      match *state {
        WatchdogState::Shutdown => return,
        WatchdogState::Idle => {
          state = shared.cvar.wait(state).unwrap();
        }
        WatchdogState::Armed { fired: true, .. } => {
          state = shared.cvar.wait(state).unwrap();
        }
        WatchdogState::Armed {
          deadline,
          fired: false,
        } => {
          let now = Instant::now();
          if now >= deadline {
            // Hold the lock across `terminate_execution` so disarm()
            // can't call `cancel_terminate_execution` before the V8
            // flag is actually set.
            if let WatchdogState::Armed { ref mut fired, .. } = *state {
              *fired = true;
            }
            isolate.terminate_execution();
          } else {
            let (s, _) =
              shared.cvar.wait_timeout(state, deadline - now).unwrap();
            state = s;
          }
        }
      }
    }
  }

  fn arm(&self, timeout_ms: u32) {
    let mut state = self.shared.mutex.lock().unwrap();
    *state = WatchdogState::Armed {
      deadline: Instant::now() + Duration::from_millis(timeout_ms as u64),
      fired: false,
    };
    self.shared.cvar.notify_all();
  }

  /// Returns true if the watchdog fired — i.e. `terminate_execution` was
  /// called for this arming. The caller must then `cancel_terminate_execution`
  /// before running JS again on the isolate.
  fn disarm(&self) -> bool {
    let mut state = self.shared.mutex.lock().unwrap();
    let fired = matches!(*state, WatchdogState::Armed { fired: true, .. });
    *state = WatchdogState::Idle;
    self.shared.cvar.notify_all();
    fired
  }
}

impl Drop for TestWatchdog {
  fn drop(&mut self) {
    {
      let mut state = self.shared.mutex.lock().unwrap();
      *state = WatchdogState::Shutdown;
      self.shared.cvar.notify_all();
    }
    if let Some(t) = self.thread.take() {
      let _ = t.join();
    }
  }
}

async fn slow_test_watchdog(event_tracker: TestEventTracker, test_id: usize) {
  // The slow test warning should pop up every DENO_SLOW_TEST_TIMEOUT*(2**n) seconds,
  // with a duration that is doubling each time. So for a warning time of 60s,
  // we should get a warning at 60s, 120s, 240s, etc.
  let base_timeout = *SLOW_TEST_TIMEOUT;
  let mut multiplier = 1;
  let mut elapsed = 0;
  loop {
    tokio::time::sleep(Duration::from_secs(
      base_timeout * (multiplier - elapsed),
    ))
    .await;
    if event_tracker
      .slow(test_id, Duration::from_secs(base_timeout * multiplier))
      .is_err()
    {
      break;
    }
    multiplier *= 2;
    elapsed += 1;
  }
}

pub async fn run_tests_for_worker(
  worker: &mut MainWorker,
  specifier: &ModuleSpecifier,
  options: &TestSpecifierOptions,
  fail_fast_tracker: &FailFastTracker,
  event_tracker: &TestEventTracker,
) -> Result<(), RunTestsForWorkerErr> {
  let state_rc = worker.js_runtime.op_state();

  // Take whatever tests have been registered
  let container = std::mem::replace(
    &mut *state_rc.borrow_mut().borrow_mut::<TestContainer>(),
    TestContainer::with_registration_phase_ended(),
  );

  let descriptions = Arc::new(container.descriptions);
  let result = async {
    event_tracker.register(descriptions.clone())?;
    run_tests_for_worker_inner(
      worker,
      specifier,
      descriptions,
      container.test_functions,
      container.test_hooks,
      options,
      event_tracker,
      fail_fast_tracker,
    )
    .await
  }
  .await;

  // Let the snapshot machinery write pending `t.assertSnapshot()` updates and
  // report a summary (no-op unless running with `--update-snapshots`). Stale
  // snapshots may only be removed when every test in the module had a chance
  // to run its assertions: a name filter or an aborted (fail-fast) run must
  // not cause snapshots of tests that didn't run to be treated as stale.
  let result = if result.is_ok()
    && !state_rc.borrow().has::<ops::testing::IsolateExitInfo>()
  {
    let allow_stale_removal =
      !options.filter.is_active() && !fail_fast_tracker.should_stop();
    worker
      .js_runtime
      .execute_script(
        located_script_name!(),
        format!(
          "Deno[Deno.internal].flushTestSnapshots?.({})",
          allow_stale_removal
        ),
      )
      .map(|_| ())
      .map_err(|e| RunTestsForWorkerErr::Core(CoreErrorKind::Js(e).into_box()))
  } else {
    result
  };

  // This worker can execute another discovery/run cycle (for example in REPL),
  // so reset to a fresh container after the current run ends.
  *state_rc.borrow_mut().borrow_mut::<TestContainer>() =
    TestContainer::default();

  result
}

fn compute_tests_to_run(
  descs: &TestDescriptions,
  test_functions: Vec<v8::Global<v8::Function>>,
  filter: TestFilter,
) -> (Vec<(&TestDescription, v8::Global<v8::Function>)>, bool) {
  let mut tests_to_run = Vec::with_capacity(descs.len());
  let mut used_only = false;
  let mut has_only = false;
  for ((_, d), f) in descs.tests.iter().zip(test_functions) {
    if !filter.includes(&d.name) {
      continue;
    }

    // If we've seen an "only: true" test, the remaining tests must be "only: true" to be added
    if has_only && !d.only {
      continue;
    }

    // If this is the first "only: true" test we've seen, clear the other tests since they were
    // only: false.
    if d.only && !has_only {
      has_only = true;
      if d.sanitize_only {
        used_only = true;
      }
      tests_to_run.clear();
    }
    tests_to_run.push((d, f));
  }
  (tests_to_run, used_only)
}

async fn call_hooks<H>(
  worker: &mut MainWorker,
  hook_fns: impl Iterator<Item = &v8::Global<v8::Function>>,
  mut error_handler: H,
) -> Result<(), RunTestsForWorkerErr>
where
  H: FnMut(CoreErrorKind) -> Result<(), RunTestsForWorkerErr>,
{
  for hook_fn in hook_fns {
    let call = worker.js_runtime.call(hook_fn);
    let result = worker
      .js_runtime
      .with_event_loop_promise(call, PollEventLoopOptions::default())
      .await;
    let Err(err) = result else {
      continue;
    };
    error_handler(err.into_kind())?;
    break;
  }
  Ok(())
}

/// The outcome of a single invocation of a test function (one attempt).
enum AttemptInvocation {
  /// The test function ran to completion and produced this result.
  Completed(TestResult),
  /// The test function threw an uncaught error that escaped the test wrapper.
  /// The error has already been reported via the event tracker; the isolate is
  /// considered poisoned and the test run should be abandoned.
  Uncaught,
}

/// Runs the wrapped test function once, applying the timeout/watchdog handling.
/// This is the unit that `retry`/`repeats` re-invoke.
#[allow(clippy::too_many_arguments, reason = "internal helper")]
async fn invoke_test_function(
  worker: &mut MainWorker,
  function: &v8::Global<v8::Function>,
  timeout_ms: Option<u32>,
  watchdog: &TestWatchdog,
  event_tracker: &TestEventTracker,
  test_id: usize,
  specifier: &ModuleSpecifier,
) -> Result<AttemptInvocation, RunTestsForWorkerErr> {
  if let Some(ms) = timeout_ms {
    watchdog.arm(ms);
  }

  let call = worker.js_runtime.call(function);

  let slow_test_warning =
    spawn(slow_test_watchdog(event_tracker.clone(), test_id));

  let test_fut = worker
    .js_runtime
    .with_event_loop_promise(call, PollEventLoopOptions::default());

  let raced = match timeout_ms {
    Some(ms) => {
      match tokio::time::timeout(Duration::from_millis(ms as u64), test_fut)
        .await
      {
        Ok(r) => Ok(r),
        Err(_elapsed) => Err(()),
      }
    }
    None => Ok(test_fut.await),
  };
  slow_test_warning.abort();

  // `disarm()` must run before any further JS executes on the isolate —
  // a stale `Fired` flag would kill the next test. The
  // `cancel_terminate_execution` below clears it; do not refactor this
  // pair apart.
  let watchdog_fired = timeout_ms.is_some() && watchdog.disarm();
  if watchdog_fired {
    worker.js_runtime.v8_isolate().cancel_terminate_execution();
  }

  // Tokio fires for async hangs; the watchdog fires for sync hot loops.
  // If the test still produced a valid result, a watchdog fire must have
  // raced after V8 was already done — preserve the result.
  let timed_out = match &raced {
    Err(_) => true,
    Ok(Ok(_)) => false,
    Ok(Err(_)) => watchdog_fired,
  };

  if timed_out {
    return Ok(AttemptInvocation::Completed(TestResult::Failed(
      TestFailure::TimedOut(timeout_ms.unwrap_or(0)),
    )));
  }

  match raced.unwrap() {
    Ok(r) => {
      deno_core::scope!(scope, &mut worker.js_runtime);
      let result = v8::Local::new(scope, r);
      Ok(AttemptInvocation::Completed(
        <TestResult as deno_core::FromV8>::from_v8(scope, result)?,
      ))
    }
    Err(error) => match error.into_kind() {
      CoreErrorKind::Js(js_error) => {
        event_tracker.uncaught_error(specifier.to_string(), js_error)?;
        Ok(AttemptInvocation::Uncaught)
      }
      CoreErrorKind::PendingPromiseResolution => {
        Ok(AttemptInvocation::Completed(TestResult::Failed(
          TestFailure::PendingPromiseResolution,
        )))
      }
      err => Err(err.into_box().into()),
    },
  }
}

#[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
async fn run_tests_for_worker_inner(
  worker: &mut MainWorker,
  specifier: &ModuleSpecifier,
  descs: Arc<TestDescriptions>,
  test_functions: Vec<v8::Global<v8::Function>>,
  test_hooks: TestHooks,
  options: &TestSpecifierOptions,
  event_tracker: &TestEventTracker,
  fail_fast_tracker: &FailFastTracker,
) -> Result<(), RunTestsForWorkerErr> {
  let unfiltered = descs.len();

  let (mut tests_to_run, used_only) =
    compute_tests_to_run(&descs, test_functions, options.filter.clone());

  if let Some(seed) = options.shuffle {
    tests_to_run.shuffle(&mut SmallRng::seed_from_u64(seed));
  }

  event_tracker.plan(TestPlan {
    origin: specifier.to_string(),
    total: tests_to_run.len(),
    filtered_out: unfiltered - tests_to_run.len(),
    used_only,
  })?;

  let mut had_uncaught_error = false;
  let mut sanitizer_helper = sanitizers::create_test_sanitizer_helper(worker);
  let watchdog = TestWatchdog::new(&mut worker.js_runtime);

  // Execute beforeAll hooks (FIFO order)
  call_hooks(worker, test_hooks.before_all.iter(), |core_error| {
    tests_to_run = vec![];
    match core_error {
      CoreErrorKind::Js(err) => {
        event_tracker.uncaught_error(specifier.to_string(), err)?;
        Ok(())
      }
      err => Err(err.into_box().into()),
    }
  })
  .await?;

  for (desc, function) in tests_to_run.into_iter() {
    worker_prepare_for_test(worker);

    if fail_fast_tracker.should_stop() {
      break;
    }

    if desc.ignore {
      event_tracker.ignored(desc)?;
      continue;
    }
    if had_uncaught_error {
      event_tracker.cancelled(desc)?;
      continue;
    }
    event_tracker.wait(desc)?;

    let timeout_ms = desc.timeout_ms.filter(|&t| t > 0);
    // A test's own `retry`/`repeats` option takes precedence (including an
    // explicit `0` to opt out); otherwise fall back to the `--retry`/`--repeats`
    // flag defaults.
    let retry = desc.retry.unwrap_or(options.retry);
    let repeats = desc.repeats.unwrap_or(options.repeats);
    let earlier = Instant::now();

    // The terminal result of the test once all repetitions/attempts settle.
    // `None` means an early exit (uncaught error) already reported a result.
    let mut final_result: Option<TestResult> = None;

    // `repeats` requires every repetition to pass; `retry` allows each
    // repetition up to `1 + retry` attempts and passes on the first success.
    'repetitions: for repetition in 0..=repeats {
      // Signal the start of a fresh repetition so the reporter discards the
      // previous repetition's step results instead of tallying them again.
      if repetition > 0 {
        event_tracker.repeat(desc, repetition)?;
      }
      let mut repetition_result = TestResult::Ok;

      'attempts: for attempt in 0..=retry {
        // Poll event loop once, to allow all ops that are already resolved, but
        // haven't responded, to settle.
        // TODO(mmastrac): we should provide an API to poll the event loop until
        // no further progress is made.
        poll_event_loop(worker).await?;

        // We always capture stats, regardless of sanitization state. A fresh
        // baseline per attempt keeps each attempt's leak check independent of
        // resources leaked by a previous, retried attempt.
        let before_test_stats = sanitizer_helper.capture_stats();

        // Execute beforeEach hooks (FIFO order). A hook error is terminal and
        // is not retried.
        let mut before_each_failure: Option<Box<JsError>> = None;
        call_hooks(worker, test_hooks.before_each.iter(), |core_error| {
          match core_error {
            CoreErrorKind::Js(err) => {
              if before_each_failure.is_none() {
                before_each_failure = Some(err);
              }
              Ok(())
            }
            err => Err(err.into_box().into()),
          }
        })
        .await?;

        if let Some(err) = before_each_failure {
          // Run afterEach for best-effort cleanup, ignoring its errors, then
          // fail the test without retrying.
          call_hooks(
            worker,
            test_hooks.after_each.iter().rev(),
            |core_error| match core_error {
              CoreErrorKind::Js(_) => Ok(()),
              err => Err(err.into_box().into()),
            },
          )
          .await?;
          final_result = Some(TestResult::Failed(TestFailure::JsError(err)));
          break 'repetitions;
        }

        let mut attempt_result = match invoke_test_function(
          worker,
          &function,
          timeout_ms,
          &watchdog,
          event_tracker,
          desc.id,
          specifier,
        )
        .await?
        {
          AttemptInvocation::Completed(result) => result,
          AttemptInvocation::Uncaught => {
            fail_fast_tracker.add_failure();
            event_tracker.cancelled(desc)?;
            had_uncaught_error = true;
            final_result = None;
            break 'repetitions;
          }
        };

        // Execute afterEach hooks (LIFO order). A hook error is terminal and is
        // not retried.
        let mut after_each_failure: Option<Box<JsError>> = None;
        call_hooks(worker, test_hooks.after_each.iter().rev(), |core_error| {
          match core_error {
            CoreErrorKind::Js(err) => {
              if after_each_failure.is_none() {
                after_each_failure = Some(err);
              }
              Ok(())
            }
            err => Err(err.into_box().into()),
          }
        })
        .await?;

        // Close idle Node.js HTTP Agent connections to prevent cross-test
        // pollution and false positive resource leak detection from pooled
        // keepAlive connections. Defined in ext/node/polyfills/01_require.js.
        _ = worker.js_runtime.execute_script(
          located_script_name!(),
          "Deno[Deno.internal].closeIdleConnections?.()",
        );

        // Await activity stabilization. A leak counts as a failure of the
        // attempt, and is therefore retryable like any other failure. Only
        // check when the test function itself passed and afterEach didn't error
        // (a real failure takes precedence over a leak report).
        let should_check_sanitizers = matches!(attempt_result, TestResult::Ok)
          && after_each_failure.is_none();
        if should_check_sanitizers {
          if let Some(diff) = sanitizers::wait_for_activity_to_stabilize(
            worker,
            &mut sanitizer_helper,
            before_test_stats,
            desc.sanitize_ops,
            desc.sanitize_resources,
          )
          .await?
          {
            let (formatted, trailer_notes) = format_sanitizer_diff(diff);
            if !formatted.is_empty() {
              attempt_result = TestResult::Failed(TestFailure::Leaked(
                formatted,
                trailer_notes,
              ));
            }
          }
        } else {
          sanitizers::record_ignored_leaks(
            &mut sanitizer_helper,
            before_test_stats,
            desc.sanitize_ops,
            desc.sanitize_resources,
          );
        }

        // An afterEach error is terminal, but only wins when the test itself
        // passed (a test failure takes precedence, matching the old behavior).
        if matches!(attempt_result, TestResult::Ok)
          && let Some(err) = after_each_failure
        {
          final_result = Some(TestResult::Failed(TestFailure::JsError(err)));
          break 'repetitions;
        }

        match attempt_result {
          TestResult::Failed(failure) if attempt < retry => {
            // Attempts remain: surface the retry and try this repetition again.
            event_tracker.retry(desc, attempt, failure)?;
            continue 'attempts;
          }
          other => {
            repetition_result = other;
            break 'attempts;
          }
        }
      }

      if matches!(repetition_result, TestResult::Failed(_)) {
        // This repetition failed definitively; the whole test fails.
        final_result = Some(repetition_result);
        break 'repetitions;
      }
      // This repetition passed; carry on to the next one (if any).
      final_result = Some(repetition_result);
    }

    if let Some(result) = final_result {
      if matches!(result, TestResult::Failed(_)) {
        fail_fast_tracker.add_failure();
      }
      event_tracker.result(desc, result, earlier.elapsed())?;
    }
  }

  event_tracker.completed()?;

  // Execute afterAll hooks (LIFO order)
  call_hooks(worker, test_hooks.after_all.iter().rev(), |core_error| {
    match core_error {
      CoreErrorKind::Js(err) => {
        event_tracker.uncaught_error(specifier.to_string(), err)?;
        Ok(())
      }
      err => Err(err.into_box().into()),
    }
  })
  .await?;

  Ok(())
}

static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false);

/// Test a collection of specifiers with test modes concurrently.
/// Selects the test files belonging to shard `index` of `count` (1-based).
/// The files are sorted for a stable order, then split into `count`
/// consecutive, balanced groups. Assumes `1 <= index <= count` (validated at
/// flag-parse time).
fn shard_specifiers(
  mut specifiers: Vec<ModuleSpecifier>,
  index: usize,
  count: usize,
) -> Vec<ModuleSpecifier> {
  specifiers.sort();
  let total = specifiers.len();
  // Balanced split: shard i covers [total*(i-1)/count, total*i/count). This
  // partitions every file into exactly one shard with sizes differing by <= 1.
  let start = total * (index - 1) / count;
  let end = total * index / count;
  specifiers.into_iter().take(end).skip(start).collect()
}

async fn test_specifiers(
  worker_factory: Arc<CliMainWorkerFactory>,
  cli_options: &Arc<CliOptions>,
  permission_desc_parser: &Arc<RuntimePermissionDescriptorParser<CliSys>>,
  specifiers: Vec<ModuleSpecifier>,
  preload_modules: Vec<ModuleSpecifier>,
  require_modules: Vec<ModuleSpecifier>,
  options: TestSpecifiersOptions,
) -> Result<(), AnyError> {
  // Select this shard's files first (on a stable sorted order) so the split is
  // deterministic across machines regardless of any later shuffling.
  let specifiers = if let Some((index, count)) = options.shard {
    shard_specifiers(specifiers, index, count)
  } else {
    specifiers
  };

  let specifiers = if let Some(seed) = options.specifier.shuffle {
    let mut rng = SmallRng::seed_from_u64(seed);
    let mut specifiers = specifiers;
    specifiers.sort();
    specifiers.shuffle(&mut rng);
    specifiers
  } else {
    specifiers
  };

  let (test_event_sender_factory, receiver) = create_test_event_channel();
  let concurrent_jobs = options.concurrent_jobs;

  let mut cancel_sender = test_event_sender_factory.weak_sender();
  let sigint_handler_handle = spawn(async move {
    deno_signals::ctrl_c().await.unwrap();
    cancel_sender.send(TestEvent::Sigint).ok();
  });
  HAS_TEST_RUN_SIGINT_HANDLER.store(true, Ordering::Relaxed);
  let reporter = get_test_reporter(&options);
  let fail_fast_tracker = FailFastTracker::new(options.fail_fast);

  let join_handles = specifiers.into_iter().map(move |specifier| {
    let worker_factory = worker_factory.clone();
    let specifier_dir = cli_options.workspace().resolve_member_dir(&specifier);
    let preload_modules = preload_modules.clone();
    let require_modules = require_modules.clone();
    let worker_sender = test_event_sender_factory.worker();
    let fail_fast_tracker = fail_fast_tracker.clone();
    let specifier_options = options.specifier.clone();
    let cli_options = cli_options.clone();
    let permission_desc_parser = permission_desc_parser.clone();
    spawn_blocking(move || {
      // Various test files should not share the same permissions in terms of
      // `PermissionsContainer` - otherwise granting/revoking permissions in one
      // file would have impact on other files, which is undesirable.
      let permissions =
        cli_options.permissions_options_for_dir(&specifier_dir)?;
      let permissions_container = PermissionsContainer::new(
        permission_desc_parser.clone(),
        Permissions::from_options(
          permission_desc_parser.as_ref(),
          &permissions,
        )?,
      );
      create_and_run_current_thread(test_specifier(
        worker_factory,
        permissions_container,
        specifier,
        preload_modules,
        require_modules,
        worker_sender,
        fail_fast_tracker,
        specifier_options,
      ))
    })
  });

  let join_stream = stream::iter(join_handles)
    .buffer_unordered(concurrent_jobs.get())
    .collect::<Vec<Result<Result<(), AnyError>, tokio::task::JoinError>>>();

  let handler = spawn(async move { report_tests(receiver, reporter).await.0 });

  let (join_results, result) = future::join(join_stream, handler).await;
  sigint_handler_handle.abort();
  HAS_TEST_RUN_SIGINT_HANDLER.store(false, Ordering::Relaxed);
  for join_result in join_results {
    join_result??;
  }
  result??;

  Ok(())
}

/// Gives receiver back in case it was ended with `TestEvent::ForceEndReport`.
pub async fn report_tests(
  mut receiver: TestEventReceiver,
  mut reporter: Box<dyn TestReporter>,
) -> (Result<(), AnyError>, TestEventReceiver) {
  let mut tests = IndexMap::new();
  let mut test_steps = IndexMap::new();
  let mut tests_started = HashSet::new();
  let mut tests_with_result = HashSet::new();
  let mut start_time = None;
  let mut had_plan = false;
  let mut used_only = false;
  let mut failed = false;

  while let Some((_, event)) = receiver.recv().await {
    match event {
      TestEvent::Register(description) => {
        for (_, description) in description.into_iter() {
          reporter.report_register(description);
          // TODO(mmastrac): We shouldn't need to clone here -- we can reuse the descriptions everywhere
          tests.insert(description.id, description.clone());
        }
      }
      TestEvent::Plan(plan) => {
        if !had_plan {
          start_time = Some(Instant::now());
          had_plan = true;
        }
        if plan.used_only {
          used_only = true;
        }
        reporter.report_plan(&plan);
      }
      TestEvent::Wait(id) => {
        if tests_started.insert(id) {
          reporter.report_wait(tests.get(&id).unwrap());
        }
      }
      TestEvent::Output(output) => {
        reporter.report_output(&output);
      }
      TestEvent::Slow(id, elapsed) => {
        reporter.report_slow(tests.get(&id).unwrap(), elapsed);
      }
      TestEvent::Result(id, result, elapsed) => {
        if tests_with_result.insert(id) {
          match result {
            TestResult::Failed(_) | TestResult::Cancelled => {
              failed = true;
            }
            _ => (),
          }
          reporter.report_result(tests.get(&id).unwrap(), &result, elapsed);
        }
      }
      TestEvent::Retry(id, attempt, failure) => {
        // Informational only: a retried attempt does not produce a terminal
        // result and is intentionally not gated by `tests_with_result`.
        reporter.report_retry(tests.get(&id).unwrap(), attempt, &failure);
      }
      TestEvent::Repeat(id, repetition) => {
        // Informational only: a repetition boundary lets the reporter drop the
        // previous repetition's step state before the next one runs.
        reporter.report_repeat(tests.get(&id).unwrap(), repetition);
      }
      TestEvent::UncaughtError(origin, error) => {
        failed = true;
        reporter.report_uncaught_error(&origin, error);
      }
      TestEvent::StepRegister(description) => {
        reporter.report_step_register(&description);
        test_steps.insert(description.id, description);
      }
      TestEvent::StepWait(id) => {
        if tests_started.insert(id) {
          reporter.report_step_wait(test_steps.get(&id).unwrap());
        }
      }
      TestEvent::StepResult(id, result, duration) => {
        if tests_with_result.insert(id) {
          reporter.report_step_result(
            test_steps.get(&id).unwrap(),
            &result,
            duration,
            &tests,
            &test_steps,
          );
        }
      }
      TestEvent::SnapshotSummary(summary) => {
        reporter.report_snapshot_summary(&summary);
      }
      TestEvent::ForceEndReport => {
        break;
      }
      TestEvent::Completed => {
        reporter.report_completed();
      }
      TestEvent::Sigint => {
        let elapsed = start_time
          .map(|t| Instant::now().duration_since(t))
          .unwrap_or_default();
        reporter.report_sigint(
          &tests_started
            .difference(&tests_with_result)
            .copied()
            .collect(),
          &tests,
          &test_steps,
        );

        #[allow(clippy::print_stderr, reason = "force outputting on failure")]
        if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) {
          eprint!("Test reporter failed to flush: {}", err)
        }
        #[allow(
          clippy::disallowed_methods,
          reason = "TODO: why is this not using deno_runtime::exit?"
        )]
        std::process::exit(130);
      }
      TestEvent::Exit(exit_code) => {
        let elapsed = start_time
          .map(|t| Instant::now().duration_since(t))
          .unwrap_or_default();
        reporter.report_exit(
          exit_code,
          &tests_started
            .difference(&tests_with_result)
            .copied()
            .collect(),
          &tests,
          &test_steps,
        );

        #[allow(clippy::print_stderr, reason = "force outputting on failure")]
        if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) {
          eprint!("Test reporter failed to flush: {}", err)
        }
        #[allow(
          clippy::disallowed_methods,
          reason = "a test called Deno.exit() with the exit sanitizer disabled"
        )]
        std::process::exit(exit_code);
      }
      TestEvent::IsolateExit(origin, exit_code) => {
        // A non-zero exit code from top-level `Deno.exit()` (or from an
        // `unload` handler) should fail the test run. A zero exit code is
        // treated as a clean isolate teardown.
        if exit_code != 0 {
          failed = true;
        }
        reporter.report_isolate_exit(&origin, exit_code);
      }
    }
  }

  let elapsed = start_time
    .map(|t| Instant::now().duration_since(t))
    .unwrap_or_default();
  reporter.report_summary(&elapsed, &tests, &test_steps);
  if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) {
    return (
      Err(anyhow!("Test reporter failed to flush: {}", err)),
      receiver,
    );
  }

  // A genuine test failure takes precedence over the `only` notice: if a test
  // failed on its own, reporting that it failed "because the \"only\" option
  // was used" is misleading.
  if failed {
    return (Err(anyhow!("Test failed")), receiver);
  }

  if used_only {
    return (
      Err(anyhow!("Test failed because the \"only\" option was used",)),
      receiver,
    );
  }

  (Ok(()), receiver)
}

fn is_supported_test_path_predicate(entry: WalkEntry) -> bool {
  if !is_script_ext(entry.path) {
    false
  } else if has_supported_test_path_name(entry.path) {
    true
  } else if let Some(include) = &entry.patterns.include {
    // allow someone to explicitly specify a path
    matches_pattern_or_exact_path(include, entry.path)
  } else {
    false
  }
}

/// Checks if the path has a basename and extension Deno supports for tests.
pub(crate) fn is_supported_test_path(path: &Path) -> bool {
  has_supported_test_path_name(path) && is_script_ext(path)
}

fn has_supported_test_path_name(path: &Path) -> bool {
  if let Some(name) = path.file_stem() {
    let basename = name.to_string_lossy();
    if basename.ends_with("_test")
      || basename.ends_with(".test")
      || basename == "test"
    {
      return true;
    }

    path
      .components()
      .any(|seg| seg.as_os_str().to_str() == Some("__tests__"))
  } else {
    false
  }
}

/// Checks if the path has an extension Deno supports for tests.
fn is_supported_test_ext(path: &Path) -> bool {
  if let Some(ext) = get_extension(path) {
    matches!(
      ext.as_str(),
      "ts"
        | "tsx"
        | "js"
        | "jsx"
        | "mjs"
        | "mts"
        | "cjs"
        | "cts"
        | "md"
        | "mkd"
        | "mkdn"
        | "mdwn"
        | "mdown"
        | "markdown"
    )
  } else {
    false
  }
}

/// Collects specifiers marking them with the appropriate test mode while maintaining the natural
/// input order.
///
/// - Specifiers matching the `is_supported_test_ext` predicate are marked as
///   `TestMode::Documentation`.
/// - Specifiers matching the `is_supported_test_path` are marked as `TestMode::Executable`.
/// - Specifiers matching both predicates are marked as `TestMode::Both`
fn collect_specifiers_with_test_mode(
  cli_options: &CliOptions,
  files: FilePatterns,
  include_inline: &bool,
) -> Result<Vec<(ModuleSpecifier, TestMode)>, AnyError> {
  // todo(dsherret): there's no need to collect twice as it's slow
  let vendor_folder = cli_options.vendor_dir_path();
  let module_specifiers = collect_specifiers(
    CollectSpecifiersOptions {
      file_patterns: files.clone(),
      vendor_folder: vendor_folder.map(ToOwned::to_owned),
      include_ignored_specified: false,
    },
    is_supported_test_path_predicate,
  )?;

  if *include_inline {
    return collect_specifiers(
      CollectSpecifiersOptions {
        file_patterns: files,
        vendor_folder: vendor_folder.map(ToOwned::to_owned),
        include_ignored_specified: false,
      },
      |e| is_supported_test_ext(e.path),
    )
    .map(|specifiers| {
      specifiers
        .into_iter()
        .map(|specifier| {
          let mode = if module_specifiers.contains(&specifier) {
            TestMode::Both
          } else {
            TestMode::Documentation
          };

          (specifier, mode)
        })
        .collect()
    });
  }

  let specifiers_with_mode = module_specifiers
    .into_iter()
    .map(|specifier| (specifier, TestMode::Executable))
    .collect();

  Ok(specifiers_with_mode)
}

/// Collects module and document specifiers with test modes via
/// `collect_specifiers_with_test_mode` which are then pre-fetched and adjusted
/// based on the media type.
///
/// Specifiers that do not have a known media type that can be executed as a
/// module are marked as `TestMode::Documentation`. Type definition files
/// cannot be run, and therefore need to be marked as `TestMode::Documentation`
/// as well.
async fn fetch_specifiers_with_test_mode(
  cli_options: &CliOptions,
  file_fetcher: &CliFileFetcher,
  member_patterns: impl Iterator<Item = FilePatterns>,
  doc: &bool,
) -> Result<Vec<(ModuleSpecifier, TestMode)>, AnyError> {
  let mut deduped_specifiers_with_mode: IndexMap<ModuleSpecifier, TestMode> =
    IndexMap::new();
  for (specifier, mode) in member_patterns
    .map(|files| {
      collect_specifiers_with_test_mode(cli_options, files.clone(), doc)
    })
    .collect::<Result<Vec<_>, _>>()?
    .into_iter()
    .flatten()
  {
    match deduped_specifiers_with_mode.entry(specifier) {
      indexmap::map::Entry::Occupied(mut entry) => {
        let merged_mode = entry.get().clone().union(mode);
        entry.insert(merged_mode);
      }
      indexmap::map::Entry::Vacant(entry) => {
        entry.insert(mode);
      }
    }
  }

  let mut specifiers_with_mode =
    deduped_specifiers_with_mode.into_iter().collect::<Vec<_>>();

  for (specifier, mode) in &mut specifiers_with_mode {
    let file = file_fetcher.fetch_bypass_permissions(specifier).await?;

    let (media_type, _) = file.resolve_media_type_and_charset();
    if matches!(
      media_type,
      MediaType::Unknown | MediaType::Dts | MediaType::Markdown
    ) {
      *mode = TestMode::Documentation
    }
  }

  Ok(specifiers_with_mode)
}

/// Returns whether the specifier points at a file the module graph can parse
/// (i.e. a script, not a markdown doc-test file). Mirrors the partition used in
/// watch mode.
fn is_script_specifier(specifier: &ModuleSpecifier) -> bool {
  deno_path_util::url_to_file_path(specifier)
    .map(|p| is_script_ext(&p))
    .unwrap_or(true)
}

/// Collect the set of files changed in git, as absolute paths.
///
/// Without a `base` this is the working-tree diff (staged, unstaged and
/// untracked). With a `base` git ref it additionally includes committed changes
/// since the merge-base of `base` and `HEAD` (the `base...HEAD` three-dot form),
/// matching the behavior of Vitest and Jest.
fn changed_files_from_git(
  cwd: &Path,
  base: Option<&str>,
) -> Result<Vec<PathBuf>, AnyError> {
  // Resolve the repository root so git's repo-relative paths can be made
  // absolute.
  let repo_root = run_git(cwd, &["rev-parse", "--show-toplevel"])
    .map_err(|err| anyhow!("`--changed` requires a git repository: {err}"))?;
  let repo_root = PathBuf::from(repo_root.trim());

  let mut commands: Vec<Vec<&str>> = vec![
    // staged changes
    vec!["diff", "--cached", "--name-only"],
    // unstaged + untracked changes (respecting .gitignore)
    vec!["ls-files", "--other", "--modified", "--exclude-standard"],
  ];
  let range;
  if let Some(base) = base {
    // committed changes since the merge-base of `base` and HEAD
    range = format!("{base}...HEAD");
    commands.push(vec!["diff", "--name-only", &range]);
  }

  // Run the commands from the repository root so their output is uniformly
  // root-relative. `diff --name-only` is always root-relative, but `ls-files`
  // prints paths relative to the current directory, so running from a
  // subdirectory would otherwise emit paths that `repo_root.join` can't resolve.
  let mut files = Vec::new();
  for args in &commands {
    let stdout = run_git(&repo_root, args)?;
    for line in stdout.lines() {
      let line = line.trim();
      if !line.is_empty() {
        files.push(repo_root.join(line));
      }
    }
  }
  Ok(files)
}

/// Resolve the set of changed/related file paths requested via `--changed` and
/// `--related`, canonicalized so they line up with the module graph's paths.
///
/// Returns `None` when neither flag was passed (no filtering).
fn collect_changed_paths(
  cli_options: &CliOptions,
  changed: &Option<Option<String>>,
  related: &[String],
) -> Result<Option<HashSet<PathBuf>>, AnyError> {
  if changed.is_none() && related.is_empty() {
    return Ok(None);
  }

  let cwd = cli_options.initial_cwd();
  // Canonicalize so paths match the graph's canonicalized module paths. Deleted
  // files can't be canonicalized and are simply dropped.
  let mut result = HashSet::new();

  if let Some(base) = changed {
    for path in changed_files_from_git(cwd, base.as_deref())? {
      if let Ok(path) = canonicalize_path(&path) {
        result.insert(path);
      }
    }
  }
  // Unlike git-derived paths, a `--related` argument is typed by the user, so
  // warn when it doesn't resolve to an existing file rather than silently
  // dropping it (which would look like a legitimately empty result).
  for file in related {
    match canonicalize_path(&cwd.join(file)) {
      Ok(path) => {
        result.insert(path);
      }
      Err(_) => {
        log::warn!(
          "{} --related file not found: {}",
          colors::yellow("Warning"),
          file
        );
      }
    }
  }
  Ok(Some(result))
}

/// Filter `specifiers_with_mode` down to the test modules affected by
/// `changed_paths`, using the module graph to find dependents.
///
/// A module is kept when it is itself a changed/related file, or when any of its
/// local dependencies changed. As an escape hatch, a change to a file that can
/// affect any test but isn't visible in the import graph (an env file, a deno
/// config file or the lockfile) disables filtering and keeps every module.
async fn filter_specifiers_by_changed(
  factory: &CliFactory,
  cli_options: &CliOptions,
  specifiers_with_mode: Vec<(ModuleSpecifier, TestMode)>,
  changed_paths: HashSet<PathBuf>,
) -> Result<Vec<(ModuleSpecifier, TestMode)>, AnyError> {
  // A change to one of these files can affect resolution or runtime behavior of
  // any test in ways the import graph doesn't capture (env vars, import maps,
  // workspace config, locked versions), so don't filter when one of them
  // changed. This mirrors Vitest/Jest, which re-run everything on config changes.
  let mut run_everything_paths = cli_options
    .possible_env_file_paths_for_watch()
    .collect::<HashSet<_>>();
  for deno_json in cli_options.workspace().deno_jsons() {
    if let Ok(path) = deno_path_util::url_to_file_path(&deno_json.specifier)
      && let Ok(path) = canonicalize_path(&path)
    {
      run_everything_paths.insert(path);
    }
  }
  // A changed package.json can alter npm dependencies (and thus any test's
  // runtime behavior) in ways the import graph doesn't capture, so treat it
  // like a deno config change.
  for pkg_json in cli_options.workspace().package_jsons() {
    if let Ok(path) = canonicalize_path(&pkg_json.path) {
      run_everything_paths.insert(path);
    }
  }
  if let Some(lockfile) = factory.maybe_lockfile().await?
    && let Ok(path) = canonicalize_path(&lockfile.filename)
  {
    run_everything_paths.insert(path);
  }
  if changed_paths
    .iter()
    .any(|p| run_everything_paths.contains(p))
  {
    return Ok(specifiers_with_mode);
  }

  // Build a graph over the script test modules so we can walk each test's
  // dependencies. Doc-only (e.g. markdown) modules aren't part of the graph and
  // are matched by path instead. A markdown doc test is therefore only selected
  // when it is itself a changed/related file, never via the dependency walk.
  //
  // Note: this graph is built solely to resolve the affected set; the downstream
  // typecheck/run path builds its own graph again. That's the same shape watch
  // mode uses, where the cost is amortized across reruns; for a one-shot
  // `--changed`/`--related` run it's an extra graph build we accept for now to
  // keep the filtering self-contained.
  let graph_kind = cli_options.type_check_mode().as_graph_kind();
  let module_graph_creator = factory.module_graph_creator().await?;
  let graph_roots = specifiers_with_mode
    .iter()
    .filter(|(specifier, _)| is_script_specifier(specifier))
    .map(|(specifier, _)| specifier.clone())
    .collect::<Vec<_>>();
  let graph = module_graph_creator
    .create_graph(graph_kind, graph_roots, NpmCachingStrategy::Eager)
    .await?;
  module_graph_creator.graph_valid(&graph)?;

  let result = specifiers_with_mode
    .into_iter()
    .filter(|(specifier, _)| {
      // Always keep a module that is itself a changed/related file.
      if let Ok(path) = deno_path_util::url_to_file_path(specifier)
        && let Ok(path) = canonicalize_path(&path)
        && changed_paths.contains(&path)
      {
        return true;
      }
      // Keep a script module if any of its local dependencies changed.
      is_script_specifier(specifier)
        && has_graph_root_local_dependent_changed(
          &graph,
          specifier,
          &changed_paths,
        )
    })
    .collect();
  Ok(result)
}

pub async fn run_tests(
  flags: Arc<Flags>,
  test_flags: TestFlags,
) -> Result<(), AnyError> {
  let factory = CliFactory::from_flags(flags.clone());
  let cli_options = factory.cli_options()?;
  let workspace_test_options =
    cli_options.resolve_workspace_test_options(&test_flags);
  let file_fetcher = factory.file_fetcher()?;
  let log_level = cli_options.log_level();

  let members_with_test_options =
    cli_options.resolve_test_options_for_members(&test_flags)?;
  let specifiers_with_mode = fetch_specifiers_with_test_mode(
    cli_options,
    file_fetcher,
    members_with_test_options.into_iter().map(|(_, v)| v.files),
    &workspace_test_options.doc,
  )
  .await?;

  let is_changed_filter =
    test_flags.changed.is_some() || !test_flags.related.is_empty();

  if !workspace_test_options.permit_no_files
    && !is_changed_filter
    && specifiers_with_mode.is_empty()
  {
    return Err(anyhow!("No test modules found"));
  }

  // Filter down to test modules affected by `--changed` / `--related`.
  let specifiers_with_mode = match collect_changed_paths(
    cli_options,
    &test_flags.changed,
    &test_flags.related,
  )? {
    None => specifiers_with_mode,
    Some(changed_paths) => {
      filter_specifiers_by_changed(
        &factory,
        cli_options,
        specifiers_with_mode,
        changed_paths,
      )
      .await?
    }
  };

  if is_changed_filter && specifiers_with_mode.is_empty() {
    log::info!("No test modules were affected by the given changes");
    return Ok(());
  }

  let doc_tests = get_doc_tests(&specifiers_with_mode, file_fetcher).await?;
  let specifiers_for_typecheck_and_test =
    get_target_specifiers(specifiers_with_mode, &doc_tests);
  for doc_test in doc_tests {
    file_fetcher.insert_memory_files(doc_test);
  }

  let main_graph_container = factory.main_module_graph_container().await?;

  // Typecheck
  main_graph_container
    .check_specifiers(
      &specifiers_for_typecheck_and_test,
      CheckSpecifiersOptions {
        ext_overwrite: cli_options.ext_flag().as_ref(),
        ..Default::default()
      },
    )
    .await?;

  if workspace_test_options.no_run {
    return Ok(());
  }

  let worker_factory =
    Arc::new(factory.create_cli_main_worker_factory().await?);
  let preload_modules = cli_options.preload_modules()?;
  let require_modules = cli_options.require_modules()?;

  // Run tests
  test_specifiers(
    worker_factory,
    cli_options,
    factory.permission_desc_parser()?,
    specifiers_for_typecheck_and_test,
    preload_modules,
    require_modules,
    TestSpecifiersOptions {
      cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err(
        |_| {
          anyhow!(
            "Unable to construct URL from the path of cwd: {}",
            cli_options.initial_cwd().to_string_lossy(),
          )
        },
      )?,
      concurrent_jobs: workspace_test_options.concurrent_jobs,
      fail_fast: workspace_test_options.fail_fast,
      log_level,
      filter: workspace_test_options.filter.is_some(),
      reporter: workspace_test_options.reporter,
      junit_path: workspace_test_options.junit_path,
      hide_stacktraces: workspace_test_options.hide_stacktraces,
      shard: workspace_test_options.shard,
      specifier: TestSpecifierOptions {
        filter: TestFilter::from_flag(&workspace_test_options.filter),
        shuffle: workspace_test_options.shuffle,
        retry: workspace_test_options.retry,
        repeats: workspace_test_options.repeats,
        trace_leaks: workspace_test_options.trace_leaks,
        sanitize_ops: workspace_test_options.sanitize_ops,
        sanitize_resources: workspace_test_options.sanitize_resources,
        update_snapshots: workspace_test_options.update_snapshots,
      },
    },
  )
  .await?;

  if test_flags.coverage_raw_data_only {
    return Ok(());
  }

  if let Some(ref coverage) = test_flags.coverage_dir {
    // A malformed `coverage` config is a user error and must fail the command.
    // The best-effort error handling below only logs report-generation errors,
    // so surface an invalid config as a hard error before generating reports.
    cli_options.resolve_coverage_thresholds()?;
    let reporters: [&dyn reporter::CoverageReporter; 3] = [
      &reporter::SummaryCoverageReporter::new(),
      &reporter::LcovCoverageReporter::new(),
      &reporter::HtmlCoverageReporter::new(),
    ];
    if let Err(err) = cover_files(
      flags,
      vec![coverage.clone()],
      vec![],
      vec![],
      vec![],
      Some(
        PathBuf::from(coverage)
          .join("lcov.info")
          .to_string_lossy()
          .into_owned(),
      ),
      test_flags.coverage_threshold.map(|t| t as f64),
      &reporters,
    ) {
      // An unmet coverage threshold is a deliberate failure and must fail the
      // command; other (best-effort) report errors are only logged.
      if err.downcast_ref::<CoverageThresholdError>().is_some() {
        return Err(err);
      }
      log::info!("Error generating coverage report: {}", err);
    }
  }

  Ok(())
}

pub async fn run_tests_with_watch(
  flags: Arc<Flags>,
  test_flags: TestFlags,
) -> Result<(), AnyError> {
  // On top of the sigint handlers which are added and unbound for each test
  // run, a process-scoped basic exit handler is required due to a tokio
  // limitation where it doesn't unbind its own handler for the entire process
  // once a user adds one.
  spawn(async move {
    loop {
      deno_signals::ctrl_c().await.unwrap();
      if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) {
        #[allow(
          clippy::disallowed_methods,
          reason = "TODO: why is this not using deno_runtime::exit?"
        )]
        std::process::exit(130);
      }
    }
  });

  file_watcher::watch_func(
    flags,
    file_watcher::PrintConfig::new(
      "Test",
      test_flags
        .watch
        .as_ref()
        .map(|w| !w.no_clear_screen)
        .unwrap_or(true),
    ),
    move |flags, watcher_communicator, changed_paths| {
      let test_flags = test_flags.clone();
      watcher_communicator.show_path_changed(changed_paths.clone());
      Ok(async move {
        let factory = CliFactory::from_flags_for_watcher(
          flags,
          watcher_communicator.clone(),
        );
        let cli_options = factory.cli_options()?;
        let workspace_test_options =
          cli_options.resolve_workspace_test_options(&test_flags);

        let _ = watcher_communicator.watch_paths(cli_options.watch_paths());
        let graph_kind = cli_options.type_check_mode().as_graph_kind();
        let log_level = cli_options.log_level();
        let cli_options = cli_options.clone();
        let module_graph_creator = factory.module_graph_creator().await?;
        let file_fetcher = factory.file_fetcher()?;
        let members_with_test_options =
          cli_options.resolve_test_options_for_members(&test_flags)?;
        let watch_paths = members_with_test_options
          .iter()
          .flat_map(|(_, test_options)| {
            file_watcher::watch_paths_for_file_patterns(&test_options.files)
          })
          .collect::<Vec<_>>();
        let _ = watcher_communicator.watch_paths(watch_paths);
        let test_modules = members_with_test_options
          .iter()
          .map(|(_, test_options)| {
            collect_specifiers(
              CollectSpecifiersOptions {
                file_patterns: test_options.files.clone(),
                vendor_folder: cli_options
                  .vendor_dir_path()
                  .map(ToOwned::to_owned),
                include_ignored_specified: false,
              },
              if workspace_test_options.doc {
                Box::new(|e: WalkEntry| is_supported_test_ext(e.path))
                  as Box<dyn Fn(WalkEntry) -> bool>
              } else {
                Box::new(is_supported_test_path_predicate)
              },
            )
          })
          .collect::<Result<Vec<_>, _>>()?
          .into_iter()
          .flatten()
          .collect::<Vec<_>>();

        // Markdown (and other non-script) doc-test files cannot be parsed
        // by the module graph, so split them out and track reloads for
        // them by path instead of by graph dependency. With `--doc`, the
        // collector accepts extensions like `.md` that `is_script_ext`
        // rejects.
        let (graph_modules, doc_only_modules): (Vec<_>, Vec<_>) =
          test_modules.into_iter().partition(|specifier| {
            deno_path_util::url_to_file_path(specifier)
              .map(|p| is_script_ext(&p))
              .unwrap_or(true)
          });

        let graph = module_graph_creator
          .create_graph(graph_kind, graph_modules, NpmCachingStrategy::Eager)
          .await?;
        module_graph_creator.graph_valid(&graph)?;
        let graph_modules = graph.roots.clone();

        let test_modules_to_reload = if let Some(changed_paths) = changed_paths
        {
          let changed_paths = changed_paths.into_iter().collect::<HashSet<_>>();
          // If an env file changed, reload all test modules since any
          // test could depend on environment variables.
          let env_file_changed = cli_options
            .possible_env_file_paths_for_watch()
            .any(|path| changed_paths.contains(&path));
          if env_file_changed {
            graph_modules
              .into_iter()
              .chain(doc_only_modules)
              .collect::<IndexSet<_>>()
          } else {
            let mut result = IndexSet::with_capacity(
              graph_modules.len() + doc_only_modules.len(),
            );
            for test_module_specifier in &graph_modules {
              if has_graph_root_local_dependent_changed(
                &graph,
                test_module_specifier,
                &changed_paths,
              ) {
                result.insert(test_module_specifier.clone());
              }
            }
            for specifier in &doc_only_modules {
              if let Ok(path) = deno_path_util::url_to_file_path(specifier)
                && let Ok(path) = canonicalize_path(&path)
                && changed_paths.contains(&path)
              {
                result.insert(specifier.clone());
              }
            }
            result
          }
        } else {
          graph_modules
            .into_iter()
            .chain(doc_only_modules)
            .collect::<IndexSet<_>>()
        };

        let specifiers_with_mode = fetch_specifiers_with_test_mode(
          &cli_options,
          file_fetcher,
          members_with_test_options.into_iter().map(|(_, v)| v.files),
          &workspace_test_options.doc,
        )
        .await?
        .into_iter()
        .filter(|(specifier, _)| test_modules_to_reload.contains(specifier))
        .collect::<Vec<(ModuleSpecifier, TestMode)>>();

        let doc_tests =
          get_doc_tests(&specifiers_with_mode, file_fetcher).await?;
        let specifiers_for_typecheck_and_test =
          get_target_specifiers(specifiers_with_mode, &doc_tests);
        for doc_test in doc_tests {
          file_fetcher.insert_memory_files(doc_test);
        }

        let main_graph_container =
          factory.main_module_graph_container().await?;

        // Typecheck
        main_graph_container
          .check_specifiers(
            &specifiers_for_typecheck_and_test,
            crate::graph_container::CheckSpecifiersOptions {
              ext_overwrite: cli_options.ext_flag().as_ref(),
              ..Default::default()
            },
          )
          .await?;

        if workspace_test_options.no_run {
          return Ok(());
        }

        let worker_factory =
          Arc::new(factory.create_cli_main_worker_factory().await?);
        let preload_modules = cli_options.preload_modules()?;
        let require_modules = cli_options.require_modules()?;

        test_specifiers(
          worker_factory,
          &cli_options,
          factory.permission_desc_parser()?,
          specifiers_for_typecheck_and_test,
          preload_modules,
          require_modules,
          TestSpecifiersOptions {
            cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err(
              |_| {
                anyhow!(
                  "Unable to construct URL from the path of cwd: {}",
                  cli_options.initial_cwd().to_string_lossy(),
                )
              },
            )?,
            concurrent_jobs: workspace_test_options.concurrent_jobs,
            fail_fast: workspace_test_options.fail_fast,
            log_level,
            filter: workspace_test_options.filter.is_some(),
            reporter: workspace_test_options.reporter,
            junit_path: workspace_test_options.junit_path,
            hide_stacktraces: workspace_test_options.hide_stacktraces,
            shard: workspace_test_options.shard,
            specifier: TestSpecifierOptions {
              filter: TestFilter::from_flag(&workspace_test_options.filter),
              shuffle: workspace_test_options.shuffle,
              retry: workspace_test_options.retry,
              repeats: workspace_test_options.repeats,
              trace_leaks: workspace_test_options.trace_leaks,
              sanitize_ops: workspace_test_options.sanitize_ops,
              sanitize_resources: workspace_test_options.sanitize_resources,
              update_snapshots: workspace_test_options.update_snapshots,
            },
          },
        )
        .await?;

        Ok(())
      })
    },
  )
  .await?;

  Ok(())
}

/// Extracts doc tests from files specified by the given specifiers.
async fn get_doc_tests(
  specifiers_with_mode: &[(Url, TestMode)],
  file_fetcher: &CliFileFetcher,
) -> Result<Vec<File>, AnyError> {
  let specifiers_needing_extraction = specifiers_with_mode
    .iter()
    .filter(|(_, mode)| mode.needs_test_extraction())
    .map(|(s, _)| s);

  let mut doc_tests = Vec::new();
  for s in specifiers_needing_extraction {
    let file = file_fetcher.fetch_bypass_permissions(s).await?;
    doc_tests.extend(extract_doc_tests(file)?);
  }

  Ok(doc_tests)
}

/// Get a list of specifiers that we need to perform typecheck and run tests on.
/// The result includes "pseudo specifiers" for doc tests.
fn get_target_specifiers(
  specifiers_with_mode: Vec<(Url, TestMode)>,
  doc_tests: &[File],
) -> Vec<Url> {
  specifiers_with_mode
    .into_iter()
    .filter_map(|(s, mode)| mode.needs_test_run().then_some(s))
    .chain(doc_tests.iter().map(|d| d.url.clone()))
    .collect()
}

#[derive(Clone)]
pub struct TestEventTracker {
  op_state: Rc<RefCell<OpState>>,
}

impl TestEventTracker {
  pub fn new(op_state: Rc<RefCell<OpState>>) -> Self {
    Self { op_state }
  }

  fn send_event(&self, event: TestEvent) -> Result<(), ChannelClosedError> {
    self
      .op_state
      .borrow_mut()
      .borrow_mut::<TestEventSender>()
      .send(event)
  }

  fn slow(
    &self,
    test_id: usize,
    duration: Duration,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Slow(test_id, duration))
  }

  fn wait(&self, desc: &TestDescription) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Wait(desc.id))
  }

  fn ignored(&self, desc: &TestDescription) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Result(
      desc.id,
      TestResult::Ignored,
      Duration::default(),
    ))
  }

  fn cancelled(
    &self,
    desc: &TestDescription,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Result(
      desc.id,
      TestResult::Cancelled,
      Duration::default(),
    ))
  }

  fn register(
    &self,
    descriptions: Arc<TestDescriptions>,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Register(descriptions))
  }

  fn completed(&self) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Completed)
  }

  fn uncaught_error(
    &self,
    specifier: String,
    error: Box<JsError>,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::UncaughtError(specifier, error))
  }

  fn plan(&self, plan: TestPlan) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Plan(plan))
  }

  fn result(
    &self,
    desc: &TestDescription,
    test_result: TestResult,
    duration: Duration,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Result(desc.id, test_result, duration))
  }

  fn retry(
    &self,
    desc: &TestDescription,
    attempt: u32,
    failure: TestFailure,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Retry(desc.id, attempt, failure))
  }

  fn repeat(
    &self,
    desc: &TestDescription,
    repetition: u32,
  ) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::Repeat(desc.id, repetition))
  }

  pub(crate) fn force_end_report(&self) -> Result<(), ChannelClosedError> {
    self.send_event(TestEvent::ForceEndReport)
  }
}

/// Tracks failures for the `--fail-fast` argument in
/// order to tell when to stop running tests.
#[derive(Clone, Default)]
pub struct FailFastTracker {
  max_count: Option<usize>,
  failure_count: Arc<AtomicUsize>,
}

impl FailFastTracker {
  pub fn new(fail_fast: Option<NonZeroUsize>) -> Self {
    Self {
      max_count: fail_fast.map(|v| v.into()),
      failure_count: Default::default(),
    }
  }

  pub fn add_failure(&self) {
    self
      .failure_count
      .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
  }

  pub fn should_stop(&self) -> bool {
    if let Some(max_count) = &self.max_count {
      self.failure_count.load(std::sync::atomic::Ordering::SeqCst) >= *max_count
    } else {
      false
    }
  }
}

#[cfg(test)]
mod inner_test {
  use std::path::Path;

  use super::*;

  #[test]
  fn test_is_supported_test_ext() {
    assert!(!is_supported_test_ext(Path::new("tests/subdir/redirects")));
    assert!(is_supported_test_ext(Path::new("README.md")));
    assert!(is_supported_test_ext(Path::new("readme.MD")));
    assert!(is_supported_test_ext(Path::new("lib/typescript.d.ts")));
    assert!(is_supported_test_ext(Path::new(
      "testdata/run/001_hello.js"
    )));
    assert!(is_supported_test_ext(Path::new(
      "testdata/run/002_hello.ts"
    )));
    assert!(is_supported_test_ext(Path::new("foo.jsx")));
    assert!(is_supported_test_ext(Path::new("foo.tsx")));
    assert!(is_supported_test_ext(Path::new("foo.TS")));
    assert!(is_supported_test_ext(Path::new("foo.TSX")));
    assert!(is_supported_test_ext(Path::new("foo.JS")));
    assert!(is_supported_test_ext(Path::new("foo.JSX")));
    assert!(is_supported_test_ext(Path::new("foo.mjs")));
    assert!(is_supported_test_ext(Path::new("foo.mts")));
    assert!(is_supported_test_ext(Path::new("foo.cjs")));
    assert!(is_supported_test_ext(Path::new("foo.cts")));
    assert!(!is_supported_test_ext(Path::new("foo.mjsx")));
    assert!(!is_supported_test_ext(Path::new("foo.jsonc")));
    assert!(!is_supported_test_ext(Path::new("foo.JSONC")));
    assert!(!is_supported_test_ext(Path::new("foo.json")));
    assert!(!is_supported_test_ext(Path::new("foo.JsON")));
  }

  #[test]
  fn test_is_supported_test_path() {
    assert!(is_supported_test_path(Path::new(
      "tests/subdir/foo_test.ts"
    )));
    assert!(is_supported_test_path(Path::new(
      "tests/subdir/foo_test.tsx"
    )));
    assert!(is_supported_test_path(Path::new(
      "tests/subdir/foo_test.js"
    )));
    assert!(is_supported_test_path(Path::new(
      "tests/subdir/foo_test.jsx"
    )));
    assert!(is_supported_test_path(Path::new("bar/foo.test.ts")));
    assert!(is_supported_test_path(Path::new("bar/foo.test.tsx")));
    assert!(is_supported_test_path(Path::new("bar/foo.test.js")));
    assert!(is_supported_test_path(Path::new("bar/foo.test.jsx")));
    assert!(is_supported_test_path(Path::new("foo/bar/test.js")));
    assert!(is_supported_test_path(Path::new("foo/bar/test.jsx")));
    assert!(is_supported_test_path(Path::new("foo/bar/test.ts")));
    assert!(is_supported_test_path(Path::new("foo/bar/test.tsx")));
    assert!(is_supported_test_path(Path::new(
      "foo/bar/__tests__/foo.js"
    )));
    assert!(is_supported_test_path(Path::new(
      "foo/bar/__tests__/foo.jsx"
    )));
    assert!(is_supported_test_path(Path::new(
      "foo/bar/__tests__/foo.ts"
    )));
    assert!(is_supported_test_path(Path::new(
      "foo/bar/__tests__/foo.tsx"
    )));
    assert!(!is_supported_test_path(Path::new("README.md")));
    assert!(!is_supported_test_path(Path::new("lib/typescript.d.ts")));
    assert!(!is_supported_test_path(Path::new("notatest.js")));
    assert!(!is_supported_test_path(Path::new("NotAtest.ts")));
  }
}
