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

use test_util as util;
use test_util::TempDir;
use test_util::assert_contains;
use test_util::env_vars_for_npm_tests;
use test_util::eprintln;
use test_util::http_server;
use test_util::test;
use tokio::io::AsyncBufReadExt;
use util::DenoChild;
use util::assert_not_contains;

/// Logs to stderr every time next_line() is called
struct LoggingLines<R>
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  pub lines: tokio::io::Lines<R>,
  pub stream_name: String,
}

impl<R> LoggingLines<R>
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  pub async fn next_line(&mut self) -> tokio::io::Result<Option<String>> {
    let line = self.lines.next_line().await;
    eprintln!(
      "{}: {}",
      self.stream_name,
      line.as_ref().unwrap().clone().unwrap()
    );
    line
  }
}

// Helper function to skip watcher output that contains "Restarting"
// phrase.
async fn skip_restarting_line<R>(stderr_lines: &mut LoggingLines<R>) -> String
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  loop {
    let msg = next_line(stderr_lines).await.unwrap();
    if !msg.contains("Restarting") {
      return msg;
    }
  }
}

async fn read_all_lints<R>(stderr_lines: &mut LoggingLines<R>) -> String
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let mut str = String::new();
  while let Some(t) = next_line(stderr_lines).await {
    let t = util::strip_ansi_codes(&t);
    if t.starts_with("Watcher Restarting! File change detected") {
      continue;
    }
    if t.starts_with("Watcher") {
      break;
    }
    if t.starts_with("error[") {
      str.push_str(&t);
      str.push('\n');
    }
  }
  str
}

async fn next_line<R>(lines: &mut LoggingLines<R>) -> Option<String>
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let timeout = tokio::time::Duration::from_secs(60);

  tokio::time::timeout(timeout, lines.next_line())
    .await
    .unwrap_or_else(|_| {
      panic!(
        "Output did not contain a new line after {} seconds",
        timeout.as_secs()
      )
    })
    .unwrap()
}

/// Returns the matched line or None if there are no more lines in this stream
async fn wait_for<R>(
  condition: impl Fn(&str) -> bool,
  lines: &mut LoggingLines<R>,
) -> Option<String>
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  while let Some(line) = lines.next_line().await.unwrap() {
    if condition(line.as_str()) {
      return Some(line);
    }
  }

  None
}

async fn wait_contains<R>(s: &str, lines: &mut LoggingLines<R>) -> String
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let timeout = tokio::time::Duration::from_secs(60);

  tokio::time::timeout(timeout, wait_for(|line| line.contains(s), lines))
    .await
    .unwrap_or_else(|_| {
      panic!(
        "Output did not contain \"{}\" after {} seconds",
        s,
        timeout.as_secs()
      )
    })
    .unwrap_or_else(|| panic!("Output ended without containing \"{}\"", s))
}

/// Before test cases touch files, they need to wait for the watcher to be
/// ready. Waiting for subcommand output is insufficient.
/// The file watcher takes a moment to start watching files due to
/// asynchronicity. It is possible for the watched subcommand to finish before
/// any files are being watched.
/// deno must be running with --log-level=debug
/// file_name should be the file name and, optionally, extension. file_name
/// may not be a full path, as it is not portable.
async fn wait_for_watcher<R>(
  file_name: &str,
  stderr_lines: &mut LoggingLines<R>,
) -> String
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let timeout = tokio::time::Duration::from_secs(60);

  tokio::time::timeout(
    timeout,
    wait_for(
      |line| line.contains("Watching paths") && line.contains(file_name),
      stderr_lines,
    ),
  )
  .await
  .unwrap_or_else(|_| {
    panic!(
      "Watcher did not start for file \"{}\" after {} seconds",
      file_name,
      timeout.as_secs()
    )
  })
  .unwrap_or_else(|| {
    panic!(
      "Output ended without before the watcher started watching file \"{}\"",
      file_name
    )
  })
}

async fn wait_for_server_and_watcher<R>(
  file_name: &str,
  stderr_lines: &mut LoggingLines<R>,
) -> String
where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let timeout = tokio::time::Duration::from_secs(60);

  tokio::time::timeout(timeout, async {
    let mut listening_line = None;
    let mut watcher_ready = false;
    while let Some(line) = stderr_lines.next_line().await.unwrap() {
      if line.contains("Listening on") {
        listening_line = Some(line.clone());
      }
      if line.contains("Watching paths") && line.contains(file_name) {
        watcher_ready = true;
      }
      if watcher_ready && let Some(line) = listening_line.take() {
        return line;
      }
    }
    panic!("Output ended before the server and watcher were ready")
  })
  .await
  .unwrap_or_else(|_| {
    panic!(
      "Server and watcher were not ready for file \"{}\" after {} seconds",
      file_name,
      timeout.as_secs()
    )
  })
}

fn check_alive_then_kill(mut child: DenoChild) {
  assert!(child.try_wait().unwrap().is_none());
  child.kill().unwrap();
}

fn child_lines(
  child: &mut std::process::Child,
) -> (
  LoggingLines<tokio::io::BufReader<tokio::process::ChildStdout>>,
  LoggingLines<tokio::io::BufReader<tokio::process::ChildStderr>>,
) {
  let stdout_lines = LoggingLines {
    lines: tokio::io::BufReader::new(
      tokio::process::ChildStdout::from_std(child.stdout.take().unwrap())
        .unwrap(),
    )
    .lines(),
    stream_name: "STDOUT".to_string(),
  };
  let stderr_lines = LoggingLines {
    lines: tokio::io::BufReader::new(
      tokio::process::ChildStderr::from_std(child.stderr.take().unwrap())
        .unwrap(),
    )
    .lines(),
    stream_name: "STDERR".to_string(),
  };
  (stdout_lines, stderr_lines)
}

#[test(flaky)]
async fn lint_watch_test() {
  let t = TempDir::new();
  let badly_linted_original =
    util::testdata_path().join("lint/watch/badly_linted.js");
  let badly_linted_output =
    util::testdata_path().join("lint/watch/badly_linted.js.out");
  let badly_linted_fixed1 =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js");
  let badly_linted_fixed1_output =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js.out");
  let badly_linted_fixed2 =
    util::testdata_path().join("lint/watch/badly_linted_fixed2.js");
  let badly_linted_fixed2_output =
    util::testdata_path().join("lint/watch/badly_linted_fixed2.js.out");
  let badly_linted = t.path().join("badly_linted.js");

  badly_linted_original.copy(&badly_linted);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("lint")
    .arg(&badly_linted)
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
  let next_line = next_line(&mut stderr_lines).await.unwrap();

  assert_contains!(&next_line, "Lint started");
  let mut output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_output.read_to_string();
  assert_eq!(output, expected);

  // Change content of the file again to be badly-linted
  badly_linted_fixed1.copy(&badly_linted);

  output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_fixed1_output.read_to_string();
  assert_eq!(output, expected);

  // Change content of the file again to be badly-linted
  badly_linted_fixed2.copy(&badly_linted);

  output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_fixed2_output.read_to_string();
  assert_eq!(output, expected);

  // the watcher process is still alive
  assert!(child.try_wait().unwrap().is_none());

  child.kill().unwrap();
}

#[test(flaky)]
async fn lint_watch_without_args_test() {
  let t = TempDir::new();
  let badly_linted_original =
    util::testdata_path().join("lint/watch/badly_linted.js");
  let badly_linted_output =
    util::testdata_path().join("lint/watch/badly_linted.js.out");
  let badly_linted_fixed1 =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js");
  let badly_linted_fixed1_output =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js.out");
  let badly_linted_fixed2 =
    util::testdata_path().join("lint/watch/badly_linted_fixed2.js");
  let badly_linted_fixed2_output =
    util::testdata_path().join("lint/watch/badly_linted_fixed2.js.out");
  let badly_linted = t.path().join("badly_linted.js");

  badly_linted_original.copy(&badly_linted);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("lint")
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  let next_line = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&next_line, "Lint started");
  let mut output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_output.read_to_string();
  assert_eq!(output, expected);

  // Change content of the file again to be badly-linted
  badly_linted_fixed1.copy(&badly_linted);

  output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_fixed1_output.read_to_string();
  assert_eq!(output, expected);

  // Change content of the file again to be badly-linted
  badly_linted_fixed2.copy(&badly_linted);

  output = read_all_lints(&mut stderr_lines).await;
  let expected = badly_linted_fixed2_output.read_to_string();
  assert_eq!(output, expected);

  // the watcher process is still alive
  assert!(child.try_wait().unwrap().is_none());

  child.kill().unwrap();
  drop(t);
}

// Regression test for https://github.com/denoland/deno/issues/28437
#[test(flaky)]
async fn lint_watch_picks_up_new_file_test() {
  let t = TempDir::new();
  let badly_linted_fixed1 =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js");

  // Start with a single file present. No path args are passed, so the watcher
  // must fall back to watching the base directory rather than the resolved
  // file list.
  let existing = t.path().join("existing.js");
  badly_linted_fixed1.copy(&existing);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("lint")
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Lint started");
  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 1 file"
  );

  // Create a new file *after* the watcher started. The watcher should notice it
  // and include it in the next run even though it wasn't initially resolved.
  let new_file = t.path().join("new_file.js");
  badly_linted_fixed1.copy(&new_file);

  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 2 files"
  );

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn lint_all_files_on_each_change_test() {
  let t = TempDir::new();
  let badly_linted_fixed0 =
    util::testdata_path().join("lint/watch/badly_linted.js");
  let badly_linted_fixed1 =
    util::testdata_path().join("lint/watch/badly_linted_fixed1.js");
  let badly_linted_fixed2 =
    util::testdata_path().join("lint/watch/badly_linted_fixed2.js");

  let badly_linted_1 = t.path().join("badly_linted_1.js");
  let badly_linted_2 = t.path().join("badly_linted_2.js");
  badly_linted_fixed0.copy(&badly_linted_1);
  badly_linted_fixed1.copy(&badly_linted_2);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("lint")
    .arg(t.path())
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 2 files"
  );

  badly_linted_fixed2.copy(&badly_linted_2);

  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 2 files"
  );

  assert!(child.try_wait().unwrap().is_none());

  child.kill().unwrap();
  drop(t);
}

#[test(flaky)]
async fn fmt_watch_test() {
  let fmt_testdata_path = util::testdata_path().join("fmt");
  let t = TempDir::new();
  let fixed = fmt_testdata_path.join("badly_formatted_fixed.js");
  let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs");
  let badly_formatted = t.path().join("badly_formatted.js");
  badly_formatted_original.copy(&badly_formatted);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("fmt")
    .arg(&badly_formatted)
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  let next_line = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&next_line, "Fmt started");
  assert_contains!(
    skip_restarting_line(&mut stderr_lines).await,
    "badly_formatted.js"
  );
  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 1 file"
  );
  wait_contains("Fmt finished", &mut stderr_lines).await;

  let expected = fixed.read_to_string();
  let actual = badly_formatted.read_to_string();
  assert_eq!(actual, expected);

  // Change content of the file again to be badly formatted
  badly_formatted_original.copy(&badly_formatted);

  assert_contains!(
    skip_restarting_line(&mut stderr_lines).await,
    "badly_formatted.js"
  );
  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 1 file"
  );
  wait_contains("Fmt finished", &mut stderr_lines).await;

  // Check if file has been automatically formatted by watcher
  let expected = fixed.read_to_string();
  let actual = badly_formatted.read_to_string();
  assert_eq!(actual, expected);
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn fmt_watch_without_args_test() {
  let fmt_testdata_path = util::testdata_path().join("fmt");
  let t = TempDir::new();
  let fixed = fmt_testdata_path.join("badly_formatted_fixed.js");
  let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs");
  let badly_formatted = t.path().join("badly_formatted.js");
  badly_formatted_original.copy(&badly_formatted);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("fmt")
    .arg("--watch")
    .arg(".")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  let next_line = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&next_line, "Fmt started");
  assert_contains!(
    skip_restarting_line(&mut stderr_lines).await,
    "badly_formatted.js"
  );
  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 1 file"
  );
  wait_contains("Fmt finished.", &mut stderr_lines).await;

  let expected = fixed.read_to_string();
  let actual = badly_formatted.read_to_string();
  assert_eq!(actual, expected);

  // Change content of the file again to be badly formatted
  badly_formatted_original.copy(&badly_formatted);
  assert_contains!(
    skip_restarting_line(&mut stderr_lines).await,
    "badly_formatted.js"
  );
  assert_contains!(
    wait_contains("Checked", &mut stderr_lines).await,
    "Checked 1 file"
  );

  // Check if file has been automatically formatted by watcher
  let expected = fixed.read_to_string();
  let actual = badly_formatted.read_to_string();
  assert_eq!(actual, expected);
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/28437
#[test(flaky)]
async fn fmt_watch_picks_up_new_file_test() {
  let fmt_testdata_path = util::testdata_path().join("fmt");
  let t = TempDir::new();
  let fixed = fmt_testdata_path.join("badly_formatted_fixed.js");
  let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs");

  // Start with a single, already-formatted file so the initial run is clean.
  let existing = t.path().join("existing.js");
  fixed.copy(&existing);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("fmt")
    .arg("--watch")
    .arg(".")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Fmt started");
  wait_contains("Fmt finished.", &mut stderr_lines).await;

  // Create a new badly-formatted file *after* the watcher started. The watcher
  // should notice it even though it wasn't part of the initially resolved set.
  let new_file = t.path().join("new_file.js");
  badly_formatted_original.copy(&new_file);

  assert_contains!(
    wait_contains("new_file.js", &mut stderr_lines).await,
    "new_file.js"
  );
  wait_contains("Fmt finished.", &mut stderr_lines).await;

  // The newly created file should have been formatted by the watcher.
  let expected = fixed.read_to_string();
  let actual = new_file.read_to_string();
  assert_eq!(actual, expected);

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn fmt_check_all_files_on_each_change_test() {
  let t = TempDir::new();
  let fmt_testdata_path = util::testdata_path().join("fmt");
  let badly_formatted_original = fmt_testdata_path.join("badly_formatted.mjs");
  let badly_formatted_1 = t.path().join("badly_formatted_1.js");
  let badly_formatted_2 = t.path().join("badly_formatted_2.js");
  badly_formatted_original.copy(&badly_formatted_1);
  badly_formatted_original.copy(&badly_formatted_2);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("fmt")
    .arg(t.path())
    .arg("--watch")
    .arg("--check")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_contains!(
    wait_contains("error", &mut stderr_lines).await,
    "Found 2 not formatted files in 2 files"
  );
  wait_contains("Fmt failed.", &mut stderr_lines).await;

  // Change content of the file again to be badly formatted
  badly_formatted_original.copy(&badly_formatted_1);

  assert_contains!(
    wait_contains("error", &mut stderr_lines).await,
    "Found 2 not formatted files in 2 files"
  );

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn check_watch_test() {
  let t = TempDir::new();
  let file_to_check = t.path().join("main.ts");
  file_to_check.write("const x: number = \"hello\";\n");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("check")
    .arg(&file_to_check)
    .arg("--watch")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  let next_line = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&next_line, "Check started");
  assert_contains!(wait_contains("TS2322", &mut stderr_lines).await, "TS2322");
  wait_contains("Check failed.", &mut stderr_lines).await;

  // Fix the type error.
  file_to_check.write("const x: number = 42;\nconsole.log(x);\n");

  wait_contains("Check finished.", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn compile_watch_test() {
  let t = TempDir::new();
  let main = t.path().join("main.ts");
  let message = t.path().join("message.ts");
  let data = t.path().join("data.txt");
  let exe = if cfg!(windows) {
    t.path().join("app.exe")
  } else {
    t.path().join("app")
  };

  main.write(
    r#"import { message } from "./message.ts";
console.log(message);
console.log(Deno.readTextFileSync(new URL("./data.txt", import.meta.url)));
"#,
  );
  message.write(r#"export const message = "before";"#);
  data.write("included before");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("compile")
    .arg("--watch")
    .arg("--no-clear-screen")
    .arg("-L")
    .arg("debug")
    .arg("--allow-read")
    .arg("--include")
    .arg(&data)
    .arg("--output")
    .arg(&exe)
    .arg(&main)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("data.txt", &mut stderr_lines).await;
  wait_for_watcher("message.ts", &mut stderr_lines).await;
  wait_contains("Compile finished", &mut stderr_lines).await;

  let output = std::process::Command::new(&exe).output().unwrap();
  assert!(output.status.success());
  assert_contains!(String::from_utf8_lossy(&output.stdout), "before");
  assert_contains!(String::from_utf8_lossy(&output.stdout), "included before");

  message.write(r#"export const message = "after";"#);
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Compile finished", &mut stderr_lines).await;

  let output = std::process::Command::new(&exe).output().unwrap();
  assert!(output.status.success());
  assert_contains!(String::from_utf8_lossy(&output.stdout), "after");
  assert_contains!(String::from_utf8_lossy(&output.stdout), "included before");

  data.write("included after");
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Compile finished", &mut stderr_lines).await;

  let output = std::process::Command::new(&exe).output().unwrap();
  assert!(output.status.success());
  assert_contains!(String::from_utf8_lossy(&output.stdout), "after");
  assert_contains!(String::from_utf8_lossy(&output.stdout), "included after");

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_no_dynamic() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("console.log('Hello world');");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Hello world", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Change content of the file
  file_to_watch.write("console.log('Hello world2');");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Hello world2", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Add dependency
  let another_file = t.path().join("another_file.js");
  another_file.write("export const foo = 0;");
  file_to_watch
    .write("import { foo } from './another_file.js'; console.log(foo);");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("0", &mut stdout_lines).await;
  wait_for_watcher("another_file.js", &mut stderr_lines).await;

  // Confirm that restarting occurs when a new file is updated
  another_file.write("export const foo = 42;");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("42", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
  file_to_watch.write("syntax error ^^");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("error:", &mut stderr_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Then restore the file
  file_to_watch
    .write("import { foo } from './another_file.js'; console.log(foo);");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("42", &mut stdout_lines).await;
  wait_for_watcher("another_file.js", &mut stderr_lines).await;

  // Update the content of the imported file with invalid syntax
  another_file.write("syntax error ^^");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("error:", &mut stderr_lines).await;
  wait_for_watcher("another_file.js", &mut stderr_lines).await;

  // Modify the imported file and make sure that restarting occurs
  another_file.write("export const foo = 'modified!';");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("modified!", &mut stdout_lines).await;
  wait_contains("Watching paths", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test: modules given via --preload (or --require / NODE_OPTIONS)
// must execute under --watch, both on the initial run and again after every
// watcher restart. They were silently skipped because the watch path never
// called execute_preload_modules().
#[test(flaky)]
async fn run_watch_preload_reruns() {
  let t = TempDir::new();
  let preload_file = t.path().join("preload.js");
  preload_file.write("console.log('preload ran');");
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("console.log('main ran');");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--preload")
    .arg(&preload_file)
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Lines are consumed in order, so this also asserts that the preload
  // module executes before the main module.
  wait_contains("preload ran", &mut stdout_lines).await;
  wait_contains("main ran", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Change content of the file and assert that the preload runs again
  // before the restarted main module.
  file_to_watch.write("console.log('main ran again');");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("preload ran", &mut stdout_lines).await;
  wait_contains("main ran again", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn serve_watch_all() {
  let t = TempDir::new();
  let main_file_to_watch = t.path().join("main_file_to_watch.js");
  main_file_to_watch.write(
    "export default {
      fetch(_request) {
        return new Response(\"aaaaaaqqq!\");
      },
    };",
  );

  let another_file = t.path().join("another_file.js");
  another_file.write("");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("serve")
    .arg(format!("--watch={another_file}"))
    .arg("-L")
    .arg("debug")
    .arg(&main_file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;

  // Change content of the file
  main_file_to_watch.write(
    "export default {
      fetch(_request) {
        return new Response(\"aaaaaaqqq123!\");
      },
    };",
  );
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;

  another_file.write("export const foo = 0;");
  // Confirm that the added file is watched as well
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;

  main_file_to_watch
    .write("import { foo } from './another_file.js'; console.log(foo);");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;
  wait_contains("0", &mut stdout_lines).await;

  another_file.write("export const foo = 42;");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;
  wait_contains("42", &mut stdout_lines).await;

  // Confirm that watch continues even with wrong syntax error
  another_file.write("syntax error ^^");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("error:", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;

  main_file_to_watch.write(
    "export default {
      fetch(_request) {
        return new Response(\"aaaaaaqqq!\");
      },
    };",
  );
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/30382.
// Ensures that `deno serve --watch` applies the import map from deno.json when
// resolving the main module, so a bare specifier entrypoint resolves the same
// way it does without `--watch`.
#[test(flaky)]
async fn serve_watch_with_import_map() {
  let t = TempDir::new();

  let server_file = t.path().join("server.js");
  server_file.write(
    "export default {
      fetch(_request) {
        return new Response(\"hello\");
      },
    };",
  );

  t.path().join("deno.json").write(
    "{
      \"imports\": {
        \"app\": \"./server.js\"
      }
    }",
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("serve")
    .arg("--watch=.")
    .arg("--port")
    .arg("0")
    .arg("app")
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // The bare specifier "app" only resolves if the import map is applied. Before
  // the fix this failed with `Module not found "file://.../app"` and the server
  // never started listening.
  wait_contains("Listening on", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/26052
// On a watcher restart, `deno serve --parallel` used to leave the previous
// generation of workers running (the worker threads were never signalled and
// the main worker task was detached), so requests kept being served by the old
// code and the process would eventually panic with an isolate mismatch.
#[test(flaky)]
async fn serve_watch_parallel_stops_old_workers() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("server_file_to_watch.js");
  file_to_watch.write(
    "export default {
      fetch(_request) {
        return new Response(\"v1\");
      },
    };",
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("serve")
    .arg("--parallel")
    .arg("--watch")
    .arg("--port")
    .arg("0")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .env("DENO_JOBS", "4")
    .piped_output()
    .spawn()
    .unwrap();

  // If anything in the test body panics (a timeout, a failed assertion), the
  // child must still be killed before unwinding: a leaked `deno serve --watch`
  // child never exits on its own, and on Windows tokio reads child pipes with
  // blocking threads, so dropping the test runtime during the unwind waits
  // forever on a stderr read that never completes. This wedged CI for the
  // full job timeout instead of reporting the failure.
  let result = futures::FutureExt::catch_unwind(std::panic::AssertUnwindSafe(
    serve_watch_parallel_stops_old_workers_inner(&t, &mut child),
  ))
  .await;
  if let Err(panic) = result {
    let _ = child.kill();
    std::panic::resume_unwind(panic);
  }

  check_alive_then_kill(child);
}

async fn serve_watch_parallel_stops_old_workers_inner(
  t: &TempDir,
  child: &mut DenoChild,
) {
  let file_to_watch = t.path().join("server_file_to_watch.js");
  let (_stdout_lines, mut stderr_lines) = child_lines(child);

  let port_regex =
    regex::Regex::new(r"Listening on https?:[^:]+:(\d+)/").unwrap();

  // "Listening on" only means that the HTTP listener is ready. Watch paths
  // are delivered to the notify task asynchronously, so wait until the entry
  // point is actually watched before doing the one-shot rewrite below. The
  // readiness lines are produced by separate tasks and may arrive either way
  // around.
  let line =
    wait_for_server_and_watcher("server_file_to_watch.js", &mut stderr_lines)
      .await;
  let old_port = port_regex.captures(&line).unwrap()[1].to_string();

  let client = reqwest::Client::builder()
    // disable connection pooling so we create a new connection per request
    // which allows us to distribute requests across workers
    .pool_max_idle_per_host(0)
    // bound every request; reqwest has no default timeout, so without this a
    // request to a socket that connects but never responds (e.g. a dying old
    // worker generation's listener backlog) hangs the test forever
    .connect_timeout(std::time::Duration::from_secs(5))
    .timeout(std::time::Duration::from_secs(10))
    .build()
    .unwrap();

  let body = client
    .get(format!("http://127.0.0.1:{old_port}/"))
    .send()
    .await
    .unwrap()
    .text()
    .await
    .unwrap();
  assert_eq!(body, "v1");

  // Rewrite the watched file until the watcher actually picks up the change.
  // "Watching paths" is logged before the OS-level watch is necessarily armed:
  // on Windows the notify backend (ReadDirectoryChangesWatcher) dispatches the
  // add-watch to a background thread, so a single write can land in the window
  // after the log line but before the watch arms, be missed entirely, and the
  // restart never happens (issue #36106). Re-writing on an interval closes that
  // window; the watcher debounces the repeated identical writes, and we abort
  // the moment the restart begins so only one restart is triggered.
  let v2 = "export default {
      fetch(_request) {
        return new Response(\"v2\");
      },
    };";
  file_to_watch.write(v2);
  let rewriter = {
    let file_to_watch = file_to_watch.clone();
    tokio::spawn(async move {
      loop {
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        file_to_watch.write(v2);
      }
    })
  };

  wait_contains("Restarting", &mut stderr_lines).await;
  rewriter.abort();
  let line = wait_contains("Listening on", &mut stderr_lines).await;
  let new_port = port_regex.captures(&line).unwrap()[1].to_string();

  // The old generation may keep serving the old code briefly while winding
  // down, and with `--parallel` both generations can even share the same port
  // (SO_REUSEPORT), so responses can interleave right after the restart.
  // Eventually only the new code must be served; before the fix the old
  // workers kept serving "v1" forever.
  let start = std::time::Instant::now();
  let mut consecutive_v2 = 0;
  while consecutive_v2 < 20 {
    match client
      .get(format!("http://127.0.0.1:{new_port}/"))
      .send()
      .await
    {
      Ok(response) => {
        match response.text().await {
          Ok(body) if body == "v2" => consecutive_v2 += 1,
          // Only the old or the new code may respond, nothing else.
          Ok(body) => {
            assert_eq!(body, "v1");
            consecutive_v2 = 0;
          }
          // The connection can die mid-response during the restart.
          Err(_) => consecutive_v2 = 0,
        }
      }
      // Transient connection errors can happen mid-restart.
      Err(_) => {
        consecutive_v2 = 0;
      }
    }
    if start.elapsed() > std::time::Duration::from_secs(30) {
      panic!("old workers are still serving 30s after the restart");
    }
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
  }

  // If the new generation got a different port, the old generation must shut
  // down and release the old port.
  if new_port != old_port {
    let start = std::time::Instant::now();
    loop {
      match client
        .get(format!("http://127.0.0.1:{old_port}/"))
        .send()
        .await
      {
        // Connection error or timeout: the old workers no longer serve.
        Err(_) => break,
        Ok(response) => {
          match response.text().await {
            // Old workers may still serve the old code while winding down.
            Ok(body) if body == "v1" => {}
            // Seeing the new code here is fine too: on platforms without
            // SO_REUSEPORT load balancing (e.g. Windows) every worker binds
            // its own ephemeral port, and the OS can hand the just-released
            // old port to a worker of the new generation. Either way the old
            // listener is gone.
            Ok(body) if body == "v2" => break,
            // Only the old or the new code may respond, nothing else.
            Ok(body) => panic!("unexpected response body: {body}"),
            // The connection can die mid-response while the old generation
            // winds down.
            Err(_) => {}
          }
        }
      }
      if start.elapsed() > std::time::Duration::from_secs(30) {
        panic!("old workers are still serving 30s after the restart");
      }
      tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    }
  }
}

// Regression test for https://github.com/denoland/deno/issues/30046
// `deno serve` used to ignore `--watch-exclude` entirely, so writing to an
// excluded file that's part of the module graph would trigger a restart.
#[test(flaky)]
async fn serve_watch_with_excluded_paths() {
  let t = TempDir::new();

  let excluded_file = t.path().join("excluded.js");
  excluded_file.write("export const foo = 0;");

  let main_file = t.path().join("main_file_to_watch.js");
  main_file.write(
    "import { foo } from './excluded.js';
    console.log('ran', foo);
    export default { fetch: () => new Response('ok') };",
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("serve")
    .arg("--watch")
    .arg("--watch-exclude=excluded.js")
    .arg("-L")
    .arg("debug")
    .arg("--allow-net")
    .arg(&main_file)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("ran", &mut stdout_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;

  // Writing the excluded file must not trigger a restart. Writing the watched
  // main file must, and the first restart we observe must be for the main file
  // rather than the excluded one.
  excluded_file.write("export const foo = 42;");
  main_file.write(
    "import { foo } from './excluded.js';
    console.log('ran2', foo);
    export default { fetch: () => new Response('ok') };",
  );

  let restart = wait_contains("File change detected", &mut stderr_lines).await;
  assert!(
    restart.contains("main_file_to_watch.js"),
    "unexpected file triggered restart: {restart}"
  );
  assert!(
    !restart.contains("excluded.js"),
    "excluded file triggered a restart: {restart}"
  );
  wait_contains("ran2", &mut stdout_lines).await;
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_npm_specifier() {
  let _g = util::http_server();
  let t = TempDir::new();

  let file_to_watch = t.path().join("file_to_watch.txt");
  file_to_watch.write("Hello world");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .envs(env_vars_for_npm_tests())
    .arg("run")
    .arg("--watch=file_to_watch.txt")
    .arg("-L")
    .arg("debug")
    .arg("npm:@denotest/bin/cli-cjs")
    .arg("Hello world")
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Hello world", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.txt", &mut stderr_lines).await;

  // Change content of the file
  file_to_watch.write("Hello world2");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Hello world", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.txt", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

// TODO(bartlomieju): this test became flaky on macOS runner; it is unclear
// if that's because of a bug in code or the runner itself. We should reenable
// it once we upgrade to XL runners for macOS.
#[cfg(not(target_os = "macos"))]
#[test(flaky)]
async fn run_watch_external_watch_files() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("console.log('Hello world');");

  let external_file_to_watch = t.path().join("external_file_to_watch.txt");
  external_file_to_watch.write("Hello world");

  let mut watch_arg = "--watch=".to_owned();
  let external_file_to_watch_str = external_file_to_watch.to_string();
  watch_arg.push_str(&external_file_to_watch_str);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg(watch_arg)
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Hello world", &mut stdout_lines).await;
  wait_for_watcher("external_file_to_watch.txt", &mut stderr_lines).await;

  // Change content of the external file
  external_file_to_watch.write("Hello world2");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;

  // Again (https://github.com/denoland/deno/issues/17584)
  external_file_to_watch.write("Hello world3");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_load_unload_events() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      setInterval(() => {}, 0);
      globalThis.addEventListener("load", () => {
        console.log("load");
      });

      globalThis.addEventListener("unload", () => {
        console.log("unload");
      });
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for the first load event to fire
  wait_contains("load", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Change content of the file, this time without an interval to keep it alive.
  file_to_watch.write(
    r#"
      globalThis.addEventListener("load", () => {
        console.log("load");
      });

      globalThis.addEventListener("unload", () => {
        console.log("unload");
      });
    "#,
  );

  // Wait for the restart
  wait_contains("Restarting", &mut stderr_lines).await;

  // Confirm that the unload event was dispatched from the first run
  wait_contains("unload", &mut stdout_lines).await;

  // Followed by the load event of the second run
  wait_contains("load", &mut stdout_lines).await;

  // Which is then unloaded as there is nothing keeping it alive.
  wait_contains("unload", &mut stdout_lines).await;
  check_alive_then_kill(child);
}

/// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt
#[test(flaky)]
async fn run_watch_not_exit() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("syntax error ^^");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("error:", &mut stderr_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Make sure the watcher actually restarts and works fine with the proper syntax
  file_to_watch.write("console.log(42);");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("42", &mut stdout_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_with_import_map_and_relative_paths() {
  fn create_relative_tmp_file(
    directory: &TempDir,
    filename: &'static str,
    filecontent: &'static str,
  ) -> std::path::PathBuf {
    let absolute_path = directory.path().join(filename);
    absolute_path.write(filecontent);
    let relative_path = absolute_path
      .as_path()
      .strip_prefix(directory.path())
      .unwrap()
      .to_owned();
    assert!(relative_path.is_relative());
    relative_path
  }

  let temp_directory = TempDir::new();
  let file_to_watch = create_relative_tmp_file(
    &temp_directory,
    "file_to_watch.js",
    "console.log('Hello world');",
  );
  let import_map_path = create_relative_tmp_file(
    &temp_directory,
    "import_map.json",
    "{\"imports\": {}}",
  );

  let mut child = util::deno_cmd()
    .current_dir(temp_directory.path())
    .arg("run")
    .arg("--watch")
    .arg("--import-map")
    .arg(&import_map_path)
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  let line = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&line, "Process started");
  assert_contains!(
    next_line(&mut stderr_lines).await.unwrap(),
    "Process finished"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "Hello world");

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_with_ext_flag() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch");
  file_to_watch.write("interface I{}; console.log(42);");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--log-level")
    .arg("debug")
    .arg("--ext")
    .arg("ts")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("42", &mut stdout_lines).await;

  // Make sure the watcher actually restarts and works fine with the proper language
  wait_for_watcher("file_to_watch", &mut stderr_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;

  file_to_watch.write("type Bear = 'polar' | 'grizzly'; console.log(123);");

  wait_contains("Restarting!", &mut stderr_lines).await;
  wait_contains("123", &mut stdout_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_error_messages() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch
    .write("throw SyntaxError(`outer`, {cause: TypeError(`inner`)})");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains(
    "error: Uncaught (in promise) SyntaxError: outer",
    &mut stderr_lines,
  )
  .await;
  wait_contains("Caused by: TypeError: inner", &mut stderr_lines).await;
  wait_contains("Process failed", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn test_watch_basic() {
  let t = TempDir::new();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("test")
    .arg("--watch")
    .arg("--no-check")
    .arg(t.path())
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_eq!(next_line(&mut stdout_lines).await.unwrap(), "");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "0 passed | 0 failed"
  );
  wait_contains("Test finished", &mut stderr_lines).await;

  let foo_file = t.path().join("foo.js");
  let bar_file = t.path().join("bar.js");
  let foo_test = t.path().join("foo_test.js");
  let bar_test = t.path().join("bar_test.js");
  foo_file.write("export default function foo() { 1 + 1 }");
  bar_file.write("export default function bar() { 2 + 2 }");
  foo_test.write("import foo from './foo.js'; Deno.test('foo', foo);");
  bar_test.write("import bar from './bar.js'; Deno.test('bar', bar);");

  assert_eq!(next_line(&mut stdout_lines).await.unwrap(), "");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo", "bar");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo", "bar");
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Change content of the file
  foo_test.write("import foo from './foo.js'; Deno.test('foobar', foo);");

  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foobar");
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Add test
  let another_test = t.path().join("new_test.js");
  another_test.write("Deno.test('another one', () => 3 + 3)");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one");
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Confirm that restarting occurs when a new file is updated
  another_test.write("Deno.test('another one', () => 3 + 3); Deno.test('another another one', () => 4 + 4)");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 2 tests"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "another another one"
  );
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
  another_test.write("syntax error ^^");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "error:");
  assert_eq!(next_line(&mut stderr_lines).await.unwrap(), "  |");
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "1 | syntax error ^^"
  );
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "  |        ~~~~~"
  );
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "at file:");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Test failed");

  // Then restore the file
  another_test.write("Deno.test('another one', () => 3 + 3)");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "another one");
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Confirm that the watcher keeps on working even if the file is updated and the test fails
  // This also confirms that it restarts when dependencies change
  foo_file
    .write("export default function foo() { throw new Error('Whoops!'); }");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "FAILED");
  wait_contains("FAILED", &mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test failed", &mut stderr_lines).await;

  // Then restore the file
  foo_file.write("export default function foo() { 1 + 1 }");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "running 1 test"
  );
  assert_contains!(next_line(&mut stdout_lines).await.unwrap(), "foo");
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  next_line(&mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Test that circular dependencies work fine
  foo_file.write("import './bar.js'; export default function foo() { 1 + 1 }");
  bar_file.write("import './foo.js'; export default function bar() { 2 + 2 }");
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn test_watch_external_watch_files() {
  let t = TempDir::new();

  let test_file = t.path().join("test_file.ts");
  test_file.write("Deno.test('hello', () => {});");

  let external_file = t.path().join("external_file.txt");
  external_file.write("Hello world");

  let mut watch_arg = "--watch=".to_owned();
  let external_file_str = external_file.to_string();
  watch_arg.push_str(&external_file_str);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("test")
    .arg(watch_arg)
    .arg("-L")
    .arg("debug")
    .arg(&test_file)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_for_watcher("external_file.txt", &mut stderr_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Change content of the external file — should trigger a re-run
  external_file.write("Hello world2");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn test_watch_doc() {
  let t = TempDir::new();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("test")
    .arg("--config")
    .arg(util::deno_config_path())
    .arg("--watch")
    .arg("--doc")
    .arg(t.path())
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_eq!(next_line(&mut stdout_lines).await.unwrap(), "");
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    "0 passed | 0 failed"
  );
  wait_contains("Test finished", &mut stderr_lines).await;

  let foo_file = t.path().join("foo.ts");
  let foo_file_url = foo_file.url_file();
  foo_file.write(
    r#"
    export function add(a: number, b: number) {
      return a + b;
    }
  "#,
  );

  wait_contains("ok | 0 passed | 0 failed", &mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Trigger a type error
  foo_file.write(
    r#"
    /**
     * ```ts
     * const sum: string = add(1, 2);
     * ```
     */
    export function add(a: number, b: number) {
      return a + b;
    }
  "#,
  );

  let file_regex = lazy_regex::lazy_regex!(r"Check [^\n]*foo\.ts#3-6\.ts");
  assert!(file_regex.is_match(&skip_restarting_line(&mut stderr_lines).await),);
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "TS2322 [ERROR]: Type 'number' is not assignable to type 'string'."
  );
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "    const sum: string = add(1, 2);"
  );
  assert_eq!(next_line(&mut stderr_lines).await.unwrap(), "          ~~~");
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    format!("    at {foo_file_url}#3-6.ts:3:11")
  );
  wait_contains("Test failed", &mut stderr_lines).await;

  // Trigger a runtime error
  foo_file.write(
    r#"
    /**
     * ```ts
     * import { assertEquals } from "@std/assert/equals";
     *
     * assertEquals(add(1, 2), 4);
     * ```
     */
    export function add(a: number, b: number) {
      return a + b;
    }
  "#,
  );

  wait_contains("running 1 test from", &mut stdout_lines).await;
  assert_contains!(
    next_line(&mut stdout_lines).await.unwrap(),
    &format!("{foo_file_url}#3-8.ts ... FAILED")
  );
  wait_contains("ERRORS", &mut stdout_lines).await;
  wait_contains(
    "error: AssertionError: Values are not equal.",
    &mut stdout_lines,
  )
  .await;
  wait_contains("-   3", &mut stdout_lines).await;
  wait_contains("+   4", &mut stdout_lines).await;
  wait_contains("FAILURES", &mut stdout_lines).await;
  wait_contains("FAILED | 0 passed | 1 failed", &mut stdout_lines).await;

  wait_contains("Test failed", &mut stderr_lines).await;

  // Fix the runtime error
  foo_file.write(
    r#"
    /**
     * ```ts
     * import { assertEquals } from "@std/assert/equals";
     *
     * assertEquals(add(1, 2), 3);
     * ```
     */
    export function add(a: number, b: number) {
      return a + b;
    }
  "#,
  );

  wait_contains("running 1 test from", &mut stdout_lines).await;

  let file_regex = lazy_regex::lazy_regex!(r"[^\n]*foo\.ts#3-8\.ts");
  assert!(file_regex.is_match(&next_line(&mut stdout_lines).await.unwrap()));
  wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await;

  wait_contains("Test finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/24558:
// `deno test --doc --watch` panicked with a parse error on Markdown files
// because the watcher passed them to the module graph builder as roots.
#[test(flaky)]
async fn test_watch_doc_markdown() {
  let t = TempDir::new();

  let readme = t.path().join("README.md");
  readme.write(
    r#"# demo

## Example

```ts
console.log("Hello world");
```
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("test")
    .arg("--config")
    .arg(util::deno_config_path())
    .arg("--watch")
    .arg("--doc")
    .arg(t.path())
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("running 1 test from", &mut stdout_lines).await;
  wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Edit the markdown file to introduce a failing assertion in its doc test.
  readme.write(
    r#"# demo

## Example

```ts
import { assertEquals } from "@std/assert/equals";

assertEquals(1, 2);
```
"#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("running 1 test from", &mut stdout_lines).await;
  wait_contains("FAILED | 0 passed | 1 failed", &mut stdout_lines).await;
  wait_contains("Test failed", &mut stderr_lines).await;

  // Fix it again.
  readme.write(
    r#"# demo

## Example

```ts
import { assertEquals } from "@std/assert/equals";

assertEquals(1, 1);
```
"#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("running 1 test from", &mut stdout_lines).await;
  wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn test_watch_module_graph_error_referrer() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("import './nonexistent.js';");
  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);
  let line1 = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&line1, "Process started");
  let line2 = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&line2, "error: Module not found");
  assert_contains!(&line2, "nonexistent.js");
  let line3 = next_line(&mut stderr_lines).await.unwrap();
  assert_contains!(&line3, "    at ");
  assert_contains!(&line3, "file_to_watch.js");
  wait_contains("Process failed", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/15428.
#[test(flaky)]
async fn test_watch_unload_handler_error_on_drop() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
    addEventListener("unload", () => {
      throw new Error("foo");
    });
    setTimeout(() => {
      throw new Error("bar");
    });
    "#,
  );
  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Uncaught Error: bar", &mut stderr_lines).await;
  wait_contains("Process failed", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_blob_urls_reset() {
  let _g = util::http_server();
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  let file_content = r#"
    const prevUrl = localStorage.getItem("url");
    if (prevUrl == null) {
      console.log("first run, storing blob url");
      const url = URL.createObjectURL(
        new Blob(["export {}"], { type: "application/javascript" }),
      );
      await import(url); // this shouldn't insert into the fs module cache
      localStorage.setItem("url", url);
    } else {
      await import(prevUrl)
        .then(() => console.log("importing old blob url incorrectly works"))
        .catch(() => console.log("importing old blob url correctly failed"));
    }
    "#;
  file_to_watch.write(file_content);
  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("first run, storing blob url", &mut stdout_lines).await;
  wait_contains("finished", &mut stderr_lines).await;
  file_to_watch.write(file_content);
  wait_contains("importing old blob url correctly failed", &mut stdout_lines)
    .await;
  wait_contains("finished", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/25559 —
// `Deno.chdir()` in the watched program must not break module
// resolution on restart. The watcher should restore the original cwd
// before re-running so the main module can still be loaded.
#[test(flaky)]
async fn run_watch_chdir() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
console.log("Files reloaded");
console.log("cwd:", Deno.cwd());
Deno.mkdirSync("temp", { recursive: true });
Deno.chdir("temp");
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-read")
    .arg("--allow-write")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Files reloaded", &mut stdout_lines).await;
  let initial_cwd_log = wait_contains("cwd:", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Trigger a restart. The previous run changed cwd via `Deno.chdir`;
  // the watcher must restore the original cwd or this restart will
  // fail with "Module not found".
  file_to_watch.write(
    r#"
console.log("Files reloaded");
console.log("cwd:", Deno.cwd());
Deno.mkdirSync("temp", { recursive: true });
Deno.chdir("temp");
console.log("done");
"#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Files reloaded", &mut stdout_lines).await;
  let cwd_line_after = wait_contains("cwd:", &mut stdout_lines).await;
  wait_contains("done", &mut stdout_lines).await;
  // The cwd printed on restart must match the cwd printed on the
  // first run — `Deno.chdir("temp")` from the previous iteration must
  // not leak across the watcher restart.
  assert_eq!(initial_cwd_log, cwd_line_after);
  check_alive_then_kill(child);
}

#[cfg(unix)]
#[test(flaky)]
async fn test_watch_sigint() {
  use nix::sys::signal;
  use nix::sys::signal::Signal;
  use nix::unistd::Pid;
  use util::TestContext;

  let context = TestContext::default();
  let t = context.temp_dir();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(r#"Deno.test("foo", () => {});"#);
  let mut child = context
    .new_command()
    .args_vec(["test", "--watch", &file_to_watch.to_string_lossy()])
    .env("NO_COLOR", "1")
    .spawn_with_piped_output();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Test started", &mut stderr_lines).await;
  wait_contains("ok | 1 passed | 0 failed", &mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;
  signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap();
  let exit_status = child.wait().unwrap();
  assert_eq!(exit_status.code(), Some(130));
}

/// Test that the "unload" event fires on watch restart when the
/// event loop is running (e.g. with setInterval keeping it alive).
#[test(flaky)]
async fn run_watch_unload_on_restart() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      addEventListener("unload", () => {
        console.log("unload event fired");
      });
      setInterval(() => {}, 1000);
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--allow-all")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Trigger a restart by modifying the file
  file_to_watch.write(
    r#"
      addEventListener("unload", () => {
        console.log("unload event fired");
      });
      setInterval(() => {}, 1000);
      // changed
    "#,
  );

  // The unload handler should fire before the process restarts
  wait_contains("unload event fired", &mut stdout_lines).await;
  wait_contains("Restarting", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

/// Test that Node.js process "exit" event fires on watch restart.
#[test(flaky)]
async fn run_watch_process_exit_on_restart() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      import process from "node:process";
      process.on("exit", () => {
        console.log("process exit fired");
      });
      setInterval(() => {}, 1000);
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--allow-all")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Trigger a restart by modifying the file
  file_to_watch.write(
    r#"
      import process from "node:process";
      process.on("exit", () => {
        console.log("process exit fired");
      });
      setInterval(() => {}, 1000);
      // changed
    "#,
  );

  // The process exit handler should fire before the process restarts
  wait_contains("process exit fired", &mut stdout_lines).await;
  wait_contains("Restarting", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

/// Wait, in either order, for the watcher to both report the requested exit
/// code and start watching `file_name`.
///
/// The "Process finished with exit code N" line (printed by the watcher when
/// the run ends) and the "Watching paths" line (emitted asynchronously by the
/// watcher thread) race on stderr, so a fixed-order wait is flaky. Waiting for
/// both before touching files ensures the exit code was surfaced and the
/// watcher is ready, so a subsequent edit is not missed.
async fn wait_for_exit_code_and_watcher<R>(
  exit_code: i32,
  file_name: &str,
  stderr_lines: &mut LoggingLines<R>,
) where
  R: tokio::io::AsyncBufRead + Unpin,
{
  let finished = format!("Process finished with exit code {exit_code}");
  let saw_finished = std::cell::Cell::new(false);
  let saw_watching = std::cell::Cell::new(false);
  let result = tokio::time::timeout(
    tokio::time::Duration::from_secs(60),
    wait_for(
      |line| {
        if line.contains(finished.as_str()) {
          saw_finished.set(true);
        }
        if line.contains("Watching paths") && line.contains(file_name) {
          saw_watching.set(true);
        }
        saw_finished.get() && saw_watching.get()
      },
      stderr_lines,
    ),
  )
  .await;
  match result {
    Ok(Some(_)) => {}
    Ok(None) => panic!(
      "output ended before the watcher reported exit code {exit_code} and started watching \"{file_name}\""
    ),
    Err(_) => panic!(
      "watcher did not report exit code {exit_code} and start watching \"{file_name}\" within 60 seconds"
    ),
  }
}

/// Shared body for the `Deno.exit()` / `process.exit()` under-watch tests: the
/// exit call ends the current run with the requested code, the `unload` event
/// still fires, code after the exit call does not run, and the watcher stays
/// alive and restarts on the next file change.
/// See https://github.com/denoland/deno/issues/7590.
async fn run_watch_exit_keeps_watcher_alive(watch_flag: &str, exit_call: &str) {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(format!(
    r#"
      import process from "node:process";
      globalThis.addEventListener("unload", () => {{
        console.log("unload event fired");
      }});
      console.log("started");
      {exit_call};
      console.log("not reached");
    "#
  ));

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg(watch_flag)
    .arg("-L")
    .arg("debug")
    .arg("--allow-all")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("started", &mut stdout_lines).await;
  // The exit call still dispatches the `unload` event, and code after it does
  // not run.
  wait_contains("unload event fired", &mut stdout_lines).await;
  // The watcher must survive the exit instead of terminating the process, and
  // the requested exit code is surfaced in the finished message.
  wait_for_exit_code_and_watcher(42, "file_to_watch.js", &mut stderr_lines)
    .await;

  // Editing the file restarts the run, proving the watcher is still alive.
  file_to_watch.write("console.log('restarted');");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("restarted", &mut stdout_lines).await;
  check_alive_then_kill(child);
}

/// Test that a script calling `Deno.exit()` under `--watch` ends the current
/// run without killing the watcher, which keeps watching and restarts the
/// script on the next file change. See https://github.com/denoland/deno/issues/7590.
#[test(flaky)]
async fn run_watch_deno_exit() {
  run_watch_exit_keeps_watcher_alive("--watch", "Deno.exit(42)").await;
}

/// Like [`run_watch_deno_exit`], but using `process.exit()` from `node:process`,
/// which routes through the same `op_exit` chokepoint.
#[test(flaky)]
async fn run_watch_process_exit() {
  run_watch_exit_keeps_watcher_alive("--watch", "process.exit(42)").await;
}

/// Like [`run_watch_deno_exit`], but for HMR mode (`--watch-hmr`), which runs
/// through `worker.run()` rather than `run_for_watcher`. A script calling
/// `Deno.exit()` must still keep the watcher alive. See
/// https://github.com/denoland/deno/issues/7590.
#[test(flaky)]
async fn run_watch_hmr_deno_exit() {
  run_watch_exit_keeps_watcher_alive("--watch-hmr", "Deno.exit(42)").await;
}

/// Test that a served module calling `Deno.exit()` under `deno serve --watch`
/// ends the current run without killing the watcher, which keeps watching and
/// restarts on the next file change. `deno serve --watch` shares the same exit
/// handling as `deno run --watch`. See https://github.com/denoland/deno/issues/7590.
#[test(flaky)]
async fn serve_watch_deno_exit() {
  let t = TempDir::new();
  let main_file_to_watch = t.path().join("main_file_to_watch.js");
  main_file_to_watch.write(
    r#"
      console.log("started");
      Deno.exit(42);
      export default {
        fetch(_request) {
          return new Response("not reached");
        },
      };
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("serve")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg(&main_file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("started", &mut stdout_lines).await;
  // The watcher must survive `Deno.exit()` instead of terminating the process,
  // and the requested exit code is surfaced in the finished message.
  wait_for_exit_code_and_watcher(
    42,
    "main_file_to_watch.js",
    &mut stderr_lines,
  )
  .await;

  // Editing the file restarts the run with a working server, proving the
  // watcher is still alive.
  main_file_to_watch.write(
    r#"
      export default {
        fetch(_request) {
          return new Response("ok");
        },
      };
    "#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_for_watcher("main_file_to_watch.js", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

/// Test that SIGTERM is dispatched to JS signal handlers on watch restart,
/// giving the process a chance to clean up before restarting.
#[test(flaky)]
async fn run_watch_sigterm_on_restart() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      Deno.addSignalListener("SIGTERM", () => {
        console.log("received SIGTERM");
      });
      setInterval(() => {}, 1000);
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--allow-all")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Trigger a restart by modifying the file (keep same behavior)
  file_to_watch.write(
    r#"
      Deno.addSignalListener("SIGTERM", () => {
        console.log("received SIGTERM");
      });
      setInterval(() => {}, 1000);
      // changed
    "#,
  );

  // The SIGTERM handler should fire before the process restarts
  wait_contains("received SIGTERM", &mut stdout_lines).await;
  wait_contains("Restarting", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

/// Test that both SIGINT and SIGTERM are dispatched on Ctrl+C in watch mode.
#[cfg(unix)]
#[test(flaky)]
async fn test_watch_sigint_and_sigterm_on_ctrlc() {
  use nix::sys::signal;
  use nix::sys::signal::Signal;
  use nix::unistd::Pid;

  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      Deno.addSignalListener("SIGINT", () => {
        console.log("received SIGINT");
      });
      Deno.addSignalListener("SIGTERM", () => {
        console.log("received SIGTERM");
      });
      setInterval(() => {}, 1000);
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--allow-all")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Send SIGINT (simulating Ctrl+C)
  signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap();

  // Both signal handlers should fire before exit
  wait_contains("received SIGINT", &mut stdout_lines).await;
  wait_contains("received SIGTERM", &mut stdout_lines).await;

  // The watcher handles Ctrl+C gracefully and exits with 0
  // (not 130) because it intercepts SIGINT via ctrl_c().
  let exit_status = child.wait().unwrap();
  assert_eq!(exit_status.code(), Some(0));
}

/// Test that Ctrl+C terminates the watcher right away while the
/// watched program is blocked in synchronous JS code and has no
/// signal listeners registered.
/// Regression test for https://github.com/denoland/deno/issues/35824.
#[cfg(unix)]
#[test(flaky)]
async fn test_watch_sigint_during_blocking_sync_code() {
  use std::os::unix::process::ExitStatusExt;

  use nix::sys::signal;
  use nix::sys::signal::Signal;
  use nix::unistd::Pid;

  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      console.log("looping");
      const start = Date.now();
      while (Date.now() - start < 60000) {}
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, _stderr_lines) = child_lines(&mut child);

  wait_contains("looping", &mut stdout_lines).await;

  // Send SIGINT (simulating Ctrl+C) while the program is inside the
  // synchronous busy loop and cannot service the event loop.
  signal::kill(Pid::from_raw(child.id() as i32), Signal::SIGINT).unwrap();

  // The process must be terminated by the default SIGINT behavior
  // without waiting for the synchronous code to finish.
  let exit_status = child.wait().unwrap();
  assert_eq!(exit_status.signal(), Some(Signal::SIGINT as i32));
}

#[test(flaky)]
async fn bench_watch_basic() {
  let t = TempDir::new();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("bench")
    .arg("--watch")
    .arg("--no-check")
    .arg(t.path())
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  assert_contains!(
    next_line(&mut stderr_lines).await.unwrap(),
    "Bench started"
  );
  assert_contains!(
    next_line(&mut stderr_lines).await.unwrap(),
    "Bench finished"
  );

  let foo_file = t.path().join("foo.js");
  let bar_file = t.path().join("bar.js");
  let foo_bench = t.path().join("foo_bench.js");
  let bar_bench = t.path().join("bar_bench.js");
  foo_file.write("export default function foo() { 1 + 1 }");
  bar_file.write("export default function bar() { 2 + 2 }");
  foo_bench.write("import foo from './foo.js'; Deno.bench('foo bench', foo);");
  bar_bench.write("import bar from './bar.js'; Deno.bench('bar bench', bar);");

  wait_contains("bar_bench.js", &mut stdout_lines).await;
  wait_contains("bar bench", &mut stdout_lines).await;
  wait_contains("foo_bench.js", &mut stdout_lines).await;
  wait_contains("foo bench", &mut stdout_lines).await;
  wait_contains("Bench finished", &mut stderr_lines).await;

  // Change content of the file
  foo_bench.write("import foo from './foo.js'; Deno.bench('foo asdf', foo);");

  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  loop {
    let line = next_line(&mut stdout_lines).await.unwrap();
    assert_not_contains!(line, "bar");
    if line.contains("foo asdf") {
      break; // last line
    }
  }
  wait_contains("Bench finished", &mut stderr_lines).await;

  // Add bench
  let another_test = t.path().join("new_bench.js");
  another_test.write("Deno.bench('another one', () => 3 + 3)");
  loop {
    let line = next_line(&mut stdout_lines).await.unwrap();
    assert_not_contains!(line, "bar");
    assert_not_contains!(line, "foo");
    if line.contains("another one") {
      break; // last line
    }
  }
  wait_contains("Bench finished", &mut stderr_lines).await;

  // Confirm that restarting occurs when a new file is updated
  another_test.write("Deno.bench('another one', () => 3 + 3); Deno.bench('another another one', () => 4 + 4)");
  loop {
    let line = next_line(&mut stdout_lines).await.unwrap();
    assert_not_contains!(line, "bar");
    assert_not_contains!(line, "foo");
    if line.contains("another another one") {
      break; // last line
    }
  }
  wait_contains("Bench finished", &mut stderr_lines).await;

  // Confirm that the watcher keeps on working even if the file is updated and has invalid syntax
  another_test.write("syntax error ^^");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "error:");
  assert_eq!(next_line(&mut stderr_lines).await.unwrap(), "  |");
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "1 | syntax error ^^"
  );
  assert_eq!(
    next_line(&mut stderr_lines).await.unwrap(),
    "  |        ~~~~~"
  );
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "at file:");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Bench failed");

  // Then restore the file
  another_test.write("Deno.bench('another one', () => 3 + 3)");
  assert_contains!(next_line(&mut stderr_lines).await.unwrap(), "Restarting");
  loop {
    let line = next_line(&mut stdout_lines).await.unwrap();
    assert_not_contains!(line, "bar");
    assert_not_contains!(line, "foo");
    if line.contains("another one") {
      break; // last line
    }
  }
  wait_contains("Bench finished", &mut stderr_lines).await;

  // Test that circular dependencies work fine
  foo_file.write("import './bar.js'; export default function foo() { 1 + 1 }");
  bar_file.write("import './foo.js'; export default function bar() { 2 + 2 }");
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/15465.
#[test(flaky)]
async fn run_watch_reload_once() {
  let _g = util::http_server();
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  let file_content = r#"
      import { time } from "http://localhost:4545/dynamic_module.ts";
      console.log(time);
    "#;
  file_to_watch.write(file_content);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--allow-import")
    .arg("--watch")
    .arg("--reload")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Process started", &mut stderr_lines).await;
  let first_output = next_line(&mut stdout_lines).await.unwrap();

  // Make sure the watcher is actually watching the file before we modify it,
  // otherwise the change may be written before the watch is registered and
  // never picked up (causing a hang/timeout).
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("finished", &mut stderr_lines).await;

  file_to_watch.write(file_content);
  // The remote dynamic module should not have been reloaded again.

  wait_contains("Restarting", &mut stderr_lines).await;
  let second_output = next_line(&mut stdout_lines).await.unwrap();
  assert_eq!(second_output, first_output);

  wait_contains("finished", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

/// Regression test for https://github.com/denoland/deno/issues/18960. Ensures that Deno.serve
/// operates properly after a watch restart.
#[test(flaky)]
async fn test_watch_serve() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  let file_content = r#"
      console.error("serving");
      await Deno.serve({port: 4600, handler: () => new Response("hello")});
    "#;
  file_to_watch.write(file_content);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-net")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut _stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Listening on", &mut stderr_lines).await;
  // Note that we start serving very quickly, so we specifically want to wait for this message
  wait_contains(r#"Watching paths: [""#, &mut stderr_lines).await;

  file_to_watch.write(file_content);

  wait_contains("serving", &mut stderr_lines).await;
  wait_contains("Listening on", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

/// Ensures that Deno.serve with unix socket operates properly after a watch restart.
#[cfg(unix)]
#[test(flaky)]
async fn test_watch_serve_unix_socket() {
  let t = TempDir::new();
  let socket_path = t.path().join("test.sock");
  let file_to_watch = t.path().join("file_to_watch.js");
  let file_content = format!(
    r#"
      console.error("serving");
      await Deno.serve({{path: "{}"}}, () => new Response("hello"));
    "#,
    socket_path.to_string().replace('\\', "\\\\")
  );
  file_to_watch.write(&file_content);

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-read")
    .arg("--allow-write")
    .arg("--allow-net")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut _stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Listening on", &mut stderr_lines).await;
  // Note that we start serving very quickly, so we specifically want to wait for this message
  wait_contains(r#"Watching paths: [""#, &mut stderr_lines).await;

  file_to_watch.write(&file_content);

  wait_contains("serving", &mut stderr_lines).await;
  wait_contains("Listening on", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_dynamic_imports() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
    console.log("Hopefully dynamic import will be watched...");
    await import("./imported.js");
    "#,
  );
  let file_to_watch2 = t.path().join("imported.js");
  file_to_watch2.write(
    r#"
    import "./imported2.js";
    console.log("I'm dynamically imported and I cause restarts!");
    "#,
  );
  let file_to_watch3 = t.path().join("imported2.js");
  file_to_watch3.write(
    r#"
    console.log("I'm statically imported from the dynamic import");
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-read")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_contains(
    "Hopefully dynamic import will be watched...",
    &mut stdout_lines,
  )
  .await;
  wait_contains(
    "I'm statically imported from the dynamic import",
    &mut stdout_lines,
  )
  .await;
  wait_contains(
    "I'm dynamically imported and I cause restarts!",
    &mut stdout_lines,
  )
  .await;

  wait_for_watcher("imported2.js", &mut stderr_lines).await;
  wait_contains("finished", &mut stderr_lines).await;

  file_to_watch3.write(
    r#"
    console.log("I'm statically imported from the dynamic import and I've changed");
    "#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains(
    "Hopefully dynamic import will be watched...",
    &mut stdout_lines,
  )
  .await;
  wait_contains(
    "I'm statically imported from the dynamic import and I've changed",
    &mut stdout_lines,
  )
  .await;
  wait_contains(
    "I'm dynamically imported and I cause restarts!",
    &mut stdout_lines,
  )
  .await;

  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/30642
#[test(flaky)]
async fn run_watch_dynamic_raw_imports() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
    console.log("main module loaded");
    const d = await import("./" + "d.md", { with: { type: "text" } });
    console.log("d:", d.default.trim());
    "#,
  );
  let raw_file = t.path().join("d.md");
  raw_file.write("hello v1\n");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--unstable-raw-imports")
    .arg("--allow-read")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_contains("main module loaded", &mut stdout_lines).await;
  wait_contains("d: hello v1", &mut stdout_lines).await;

  wait_for_watcher("d.md", &mut stderr_lines).await;
  wait_contains("finished", &mut stderr_lines).await;

  raw_file.write("hello v2\n");

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("main module loaded", &mut stdout_lines).await;
  wait_contains("d: hello v2", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_inspect() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
      console.log("hello world");
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--inspect")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Debugger listening", &mut stderr_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("hello world", &mut stdout_lines).await;

  file_to_watch.write(
    r#"
      console.log("updated file");
    "#,
  );

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("Debugger listening", &mut stderr_lines).await;
  wait_contains("updated file", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_with_excluded_paths() {
  let t = TempDir::new();

  let file_to_exclude = t.path().join("file_to_exclude.js");
  file_to_exclude.write("export const foo = 0;");

  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch
    .write("import { foo } from './file_to_exclude.js'; console.log(foo);");

  let mjs_file_to_exclude = t.path().join("file_to_exclude.mjs");
  mjs_file_to_exclude.write("export const foo = 0;");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--watch-exclude=file_to_exclude.js,*.mjs")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("0", &mut stdout_lines).await;
  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  // Confirm that restarting doesn't occurs when a excluded file is updated
  file_to_exclude.write("export const foo = 42;");
  mjs_file_to_exclude.write("export const foo = 42;");

  wait_contains("finished", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

// Regression test for https://github.com/denoland/deno/issues/26217
// When using `--watch=.` (directory), excluded files within that directory
// should not trigger restarts.
#[test(flaky)]
async fn run_watch_directory_with_excluded_file() {
  let t = TempDir::new();

  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write("console.log('hello');");

  let file_to_exclude = t.path().join("file_to_exclude.txt");
  file_to_exclude.write("original content");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch=.")
    .arg("--watch-exclude=file_to_exclude.txt")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("hello", &mut stdout_lines).await;
  wait_contains("finished", &mut stderr_lines).await;

  // Modify excluded file - should NOT trigger restart
  file_to_exclude.write("modified content");

  // Modify watched file - should trigger restart
  file_to_watch.write("console.log('restarted');");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("restarted", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

// When using `--watch=.`, excluded subdirectories should not trigger restarts
// for any file changes within them.
#[test(flaky)]
async fn run_watch_directory_with_excluded_subdirectory() {
  let t = TempDir::new();

  let file_to_watch = t.path().join("main.js");
  file_to_watch.write("console.log('start');");

  let sub_dir = t.path().join("ignored_dir");
  sub_dir.create_dir_all();
  let sub_file = sub_dir.join("data.txt");
  sub_file.write("data");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch=.")
    .arg("--watch-exclude=ignored_dir")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_contains("start", &mut stdout_lines).await;
  wait_contains("finished", &mut stderr_lines).await;

  // Modify file inside excluded subdirectory - should NOT trigger restart
  sub_file.write("data updated");

  // Modify watched file - should trigger restart
  file_to_watch.write("console.log('restarted');");
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("restarted", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_hmr_server() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
globalThis.state = { i: 0 };

function bar() {
  globalThis.state.i = 0;
  console.log("got request", globalThis.state.i);
}

function handler(_req) {
  bar();
  return new Response("Hello world!");
}

Deno.serve({ port: 11111 }, handler);
console.log("Listening...")
    "#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch-hmr")
    .arg("--allow-net")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("Listening...", &mut stdout_lines).await;

  file_to_watch.write(
    r#"
globalThis.state = { i: 0 };

function bar() {
  globalThis.state.i = 0;
  console.error("got request1", globalThis.state.i);
}

function handler(_req) {
  bar();
  return new Response("Hello world!");
}

Deno.serve({ port: 11111 }, handler);
console.log("Listening...")
    "#,
  );

  wait_contains("Replaced changed module", &mut stderr_lines).await;
  // Re-evaluating the module calls `Deno.serve` again, so there is a window
  // right after the module is replaced where the listener has been closed and
  // not yet rebound and a connection is refused. Retry until it is accepting.
  //
  // This used to spawn a fire-and-forget `deno eval` child that fetched once
  // and was never awaited: a refused connection there silently dropped the
  // request (leaving this test to hang until `wait_contains` timed out) and
  // printed `error sending request for url (http://localhost:11111/)` onto the
  // inherited stderr, where it got attributed to whichever test happened to be
  // reporting at the time.
  let client = reqwest::Client::new();
  let start = std::time::Instant::now();
  loop {
    match client.get("http://localhost:11111/").send().await {
      Ok(_) => break,
      Err(err) => {
        if start.elapsed() > std::time::Duration::from_secs(30) {
          panic!("could not reach the hmr server after 30 seconds: {err}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
      }
    }
  }
  wait_contains("got request1", &mut stderr_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_hmr_jsx() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
import { foo } from "./foo.jsx";

let i = 0;
setInterval(() => {
  console.log(i++, foo());
}, 100);
"#,
  );
  let file_to_watch2 = t.path().join("foo.jsx");
  file_to_watch2.write(
    r#"
export function foo() {
  return `<h1>Hello</h1>`;
}
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch-hmr")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("5 <h1>Hello</h1>", &mut stdout_lines).await;

  file_to_watch2.write(
    r#"
export function foo() {
  return `<h1>Hello world</h1>`;
}
    "#,
  );

  wait_contains("Replaced changed module", &mut stderr_lines).await;
  wait_contains("<h1>Hello world</h1>", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_hmr_uncaught_error() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
import { foo } from "./foo.jsx";

let i = 0;
setInterval(() => {
  console.log(i++, foo());
}, 100);
"#,
  );
  let file_to_watch2 = t.path().join("foo.jsx");
  file_to_watch2.write(
    r#"
export function foo() {
  setTimeout(() => {
    throw new Error("fail");
  });
  return `<h1>asd1</h1>`;
}
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch-hmr")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;
  wait_contains("fail", &mut stderr_lines).await;

  file_to_watch2.write(
    r#"
export function foo() {
  return `<h1>asd2</h1>`;
}
    "#,
  );

  wait_contains("Process failed", &mut stderr_lines).await;
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("<h1>asd2</h1>", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_hmr_unhandled_rejection() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
import { foo } from "./foo.jsx";

// deno-lint-ignore require-await
async function rejection() {
  throw new Error("boom!");
}

let i = 0;
setInterval(() => {
  if (i == 3) {
    rejection();
  }
  console.log(i++, foo());
}, 100);
"#,
  );
  let file_to_watch2 = t.path().join("foo.jsx");
  file_to_watch2.write(
    r#"
export function foo() {
  return `<h1>asd1</h1>`;
}
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch-hmr")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await;
  wait_contains("boom", &mut stderr_lines).await;

  file_to_watch.write(
    r#"
import { foo } from "./foo.jsx";

let i = 0;
setInterval(() => {
  console.log(i++, foo());
}, 100);
    "#,
  );

  wait_contains("Process failed", &mut stderr_lines).await;
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_hmr_compile_error() {
  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
function foo() {
  return `<h1>asd1</h1>`;
}

let i = 0;
setInterval(() => {
  console.log(i++, foo());
}, 100);
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch-hmr")
    .arg("-L")
    .arg("debug")
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Process started", &mut stderr_lines).await;
  wait_contains("Finished config loading.", &mut stderr_lines).await;

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
  wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await;

  // Misspelled `function` on purpose
  file_to_watch.write(
    r#"
function foo() {
  fnction bar();
}

let i = 0;
setInterval(() => {
  console.log(i++, foo());
}, 100);
"#,
  );

  wait_contains("compile error: Uncaught SyntaxError", &mut stderr_lines).await;
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn bundle_watch() {
  let _server = http_server();

  let t = TempDir::new();
  let file_to_watch = t.path().join("file_to_watch.js");
  file_to_watch.write(
    r#"
    console.log("hello");
"#,
  );

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("bundle")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("-o")
    .arg(t.path().join("output.js"))
    .arg(&file_to_watch)
    .env("NO_COLOR", "1")
    .envs(env_vars_for_npm_tests())
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);
  wait_contains("Bundled 1 module in", &mut stderr_lines).await;
  let contents = t.path().join("output.js").read_to_string();
  assert_contains!(contents, "console.log(\"hello\");");

  wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;

  file_to_watch.write(
    r#"
    console.log("hello world");
"#,
  );
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Bundled 1 module in", &mut stderr_lines).await;
  let contents = t.path().join("output.js").read_to_string();
  assert_contains!(contents, "console.log(\"hello world\");");

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn bundle_watch_raw_css_import() {
  let _server = http_server();

  let t = TempDir::new();
  let main_ts = t.path().join("main.ts");
  main_ts.write(
    r#"import styles from "./styles.css" with { type: "css" };
console.log(styles);
"#,
  );
  let styles_css = t.path().join("styles.css");
  styles_css.write("div { color: red; }\n");
  let output = t.path().join("output.js");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("bundle")
    .arg("--watch")
    .arg("--unstable-raw-imports")
    .arg("--platform=browser")
    .arg("-L")
    .arg("debug")
    .arg("--output")
    .arg(&output)
    .arg(&main_ts)
    .env("NO_COLOR", "1")
    .envs(env_vars_for_npm_tests())
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Bundled 2 modules in", &mut stderr_lines).await;
  assert_contains!(output.read_to_string(), "div { color: red; }");
  wait_for_watcher("styles.css", &mut stderr_lines).await;

  styles_css.write("div { color: blue; }\n");
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Bundled 2 modules in", &mut stderr_lines).await;
  let contents = output.read_to_string();
  assert_contains!(contents, "div { color: blue; }");
  assert_not_contains!(contents, "div { color: red; }");

  wait_for_watcher("main.ts", &mut stderr_lines).await;
  main_ts.write(
    r#"import styles from "./styles.css" with { type: "css" };
console.log("watch still works", styles);
"#,
  );
  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Bundled 2 modules in", &mut stderr_lines).await;
  assert_contains!(output.read_to_string(), "watch still works");

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn bundle_watch_html_entry() {
  let _server = http_server();

  let t = TempDir::new();
  let index_html = t.path().join("index.html");
  index_html.write(
    r#"<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Initial Page</title>
  </head>
  <body>
    <h1 id="message">Initial Content</h1>
    <script type="module" src="./main.ts"></script>
  </body>
</html>
"#,
  );
  let main_ts = t.path().join("main.ts");
  main_ts.write(
    r#"
  const h1 = document.createElement("h1");
  h1.textContent = "Hello, World!";
  document.body.appendChild(h1);
"#,
  );

  let out_dir = t.path().join("dist");

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("bundle")
    .arg("--watch")
    .arg("-L")
    .arg("debug")
    .arg("--outdir")
    .arg(&out_dir)
    .arg(&index_html)
    .env("NO_COLOR", "1")
    .envs(env_vars_for_npm_tests())
    .piped_output()
    .spawn()
    .unwrap();
  let (_, mut stderr_lines) = child_lines(&mut child);

  wait_contains("Bundled", &mut stderr_lines).await;

  let html_output = out_dir.join("index.html");
  let contents = html_output.read_to_string();
  assert_contains!(contents, "Initial Content");

  wait_for_watcher("index.html", &mut stderr_lines).await;

  index_html.write(
    r#"<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Updated Page</title>
  </head>
  <body>
    <h1 id="message">Updated Content</h1>
    <p>Watching HTML works</p>
    <script type="module" src="./main.ts"></script>
  </body>
</html>
"#,
  );

  wait_contains("File change detected", &mut stderr_lines).await;
  wait_contains("Bundled", &mut stderr_lines).await;

  let contents = html_output.read_to_string();
  assert_contains!(contents, "Updated Content");
  assert_not_contains!(contents, "Initial Content");

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_basic() {
  let t = TempDir::new();

  // Create initial .env file
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap();

  // Create main script that reads environment variables
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for initial run
  wait_contains("FOO: initial_value", &mut stdout_lines).await;
  wait_contains("BAR: test_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;
  // Ensure initial run is complete
  wait_contains("Process finished", &mut stderr_lines).await;

  // Change the .env file
  std::fs::write(
    &env_file,
    "FOO=updated_value\nBAR=test_value\nNEW_VAR=new_value",
  )
  .unwrap();

  // Wait for actual restart (not the initial "Restarting on file change..." message)
  wait_contains("Restarting!", &mut stderr_lines).await;
  // Check that new values are reflected
  wait_contains("FOO: updated_value", &mut stdout_lines).await;
  wait_contains("BAR: test_value", &mut stdout_lines).await;
  // This test should fail if NEW_VAR isn't being loaded - but the script doesn't read NEW_VAR
  // so we can't test for it without updating the script first
  wait_contains("---", &mut stdout_lines).await;
  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_process_env_precedence() {
  let t = TempDir::new();

  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=from_env_file\nBAR=from_env_file").unwrap();

  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg("-L")
    .arg("debug")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .env("FOO", "from_process")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  wait_for_watcher("main.js", &mut stderr_lines).await;

  wait_contains("FOO: from_process", &mut stdout_lines).await;
  wait_contains("BAR: from_env_file", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;
  wait_contains("Process finished", &mut stderr_lines).await;

  std::fs::write(
    &env_file,
    "FOO=updated_from_env_file\nBAR=updated_from_env_file",
  )
  .unwrap();

  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("FOO: from_process", &mut stdout_lines).await;
  wait_contains("BAR: updated_from_env_file", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_multiple_files() {
  let t = TempDir::new();

  // Create multiple .env files
  let env_file1 = t.path().join(".env");
  let env_file2 = t.path().join(".env.local");

  std::fs::write(&env_file1, "FOO=from_env\nBAR=from_env").unwrap();
  std::fs::write(&env_file2, "BAR=from_local\nBAZ=from_local").unwrap();

  // Create main script
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("BAZ:", Deno.env.get("BAZ"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg("--env-file=.env.local")
    .arg("-L")
    .arg("debug")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for watcher to be ready
  wait_for_watcher("main.js", &mut stderr_lines).await;

  // Wait for initial run - .env.local should override .env for BAR
  wait_contains("FOO: from_env", &mut stdout_lines).await;
  wait_contains("BAR: from_local", &mut stdout_lines).await;
  wait_contains("BAZ: from_local", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  // Change the first .env file and add small delay
  std::fs::write(&env_file1, "FOO=updated_from_env\nBAR=updated_from_env")
    .unwrap();
  std::fs::write(&env_file2, "BAR=updated_from_local\nBAZ=updated_from_local")
    .unwrap();
  // Wait for restart
  wait_contains("Restarting", &mut stderr_lines).await;

  wait_contains("FOO: updated_from_env", &mut stdout_lines).await;
  wait_contains("BAR: updated_from_local", &mut stdout_lines).await;
  wait_contains("BAZ: updated_from_local", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_removed() {
  let t = TempDir::new();

  // Create initial .env file
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap();

  // Create main script
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg("-L")
    .arg("debug")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for watcher to be ready
  wait_for_watcher("main.js", &mut stderr_lines).await;

  // Wait for initial run
  wait_contains("FOO: initial_value", &mut stdout_lines).await;
  wait_contains("BAR: test_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  // Remove the .env file
  std::fs::remove_file(&env_file).unwrap();

  // Wait for restart
  wait_contains("Restarting", &mut stderr_lines).await;

  // Check that environment variables are no longer available
  let foo_line = wait_contains("FOO:", &mut stdout_lines).await;
  assert!(
    foo_line.contains("undefined"),
    "FOO should be undefined after .env file is removed"
  );

  let bar_line = wait_contains("BAR:", &mut stdout_lines).await;
  assert!(
    bar_line.contains("undefined"),
    "BAR should be undefined after .env file is removed"
  );

  wait_contains("---", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_invalid_syntax() {
  let t = TempDir::new();

  // Create initial valid .env file
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=valid_value\nBAR=valid_value").unwrap();

  // Create main script
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for initial run
  wait_contains("FOO: valid_value", &mut stdout_lines).await;
  wait_contains("BAR: valid_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  // Ensure initial run is complete
  wait_contains("Process finished", &mut stderr_lines).await;

  // Change to invalid .env file
  std::fs::write(
    &env_file,
    "FOO=valid_value\nINVALID_LINE_WITHOUT_EQUALS\nBAR=valid_value",
  )
  .unwrap();

  // Wait for actual restart - should show warning but continue
  wait_contains("Restarting!", &mut stderr_lines).await;

  // Should still show valid values - test the error handling behavior
  let foo_line = wait_contains("FOO:", &mut stdout_lines).await;
  assert!(
    foo_line.contains("valid_value"),
    "FOO should still be valid despite invalid syntax in file"
  );

  let bar_line = wait_contains("BAR:", &mut stdout_lines).await;
  assert!(
    bar_line.contains("valid_value"),
    "BAR should still be valid despite invalid syntax in file"
  );

  wait_contains("---", &mut stdout_lines).await;

  // Also check that a warning was logged about the invalid syntax
  // This assertion might reveal if error handling is actually working
  // (depending on how Deno handles invalid .env syntax)

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_with_script_changes() {
  let t = TempDir::new();

  // Create initial .env file
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=env_value\nBAR=env_value").unwrap();

  // Create main script
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg("-L")
    .arg("debug")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for watcher to be ready
  wait_for_watcher("main.js", &mut stderr_lines).await;

  // Wait for initial run
  wait_contains("FOO: env_value", &mut stdout_lines).await;
  wait_contains("BAR: env_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  // Ensure initial run is complete
  wait_contains("Process finished", &mut stderr_lines).await;

  // Change the script to read different env vars
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("BAZ:", Deno.env.get("BAZ"));
console.log("---");
"#,
  )
  .unwrap();

  // Wait for actual restart
  wait_contains("Restarting!", &mut stderr_lines).await;

  // Should show FOO and BAR, but BAZ should be undefined
  wait_contains("FOO: env_value", &mut stdout_lines).await;
  wait_contains("BAR: env_value", &mut stdout_lines).await;
  let baz_line = wait_contains("BAZ:", &mut stdout_lines).await;
  assert!(
    baz_line.contains("undefined"),
    "BAZ should be undefined since it's not in the .env file yet"
  );
  wait_contains("---", &mut stdout_lines).await;

  // Ensure previous run is complete
  wait_contains("Process finished", &mut stderr_lines).await;

  // Now add BAZ to .env file
  std::fs::write(&env_file, "FOO=env_value\nBAR=env_value\nBAZ=new_env_value")
    .unwrap();

  // Wait for actual restart
  wait_contains("Restarting!", &mut stderr_lines).await;

  // Now BAZ should have a value
  wait_contains("FOO: env_value", &mut stdout_lines).await;
  wait_contains("BAR: env_value", &mut stdout_lines).await;
  let baz_line = wait_contains("BAZ:", &mut stdout_lines).await;
  assert!(
    baz_line.contains("new_env_value"),
    "BAZ should now have a value after being added to .env file"
  );
  wait_contains("---", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn run_watch_env_file_with_multiline_values() {
  let t = TempDir::new();

  // Create .env file with multiline values
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=simple_value\nMULTILINE=\"First Line\nSecond Line\"\nBAR=another_value").unwrap();

  // Create main script
  let main_script = t.path().join("main.js");
  std::fs::write(
    &main_script,
    r#"
console.log("FOO:", Deno.env.get("FOO"));
console.log("MULTILINE:", Deno.env.get("MULTILINE"));
console.log("BAR:", Deno.env.get("BAR"));
console.log("---");
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("run")
    .arg("--watch")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg("-L")
    .arg("debug")
    .arg(&main_script)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for watcher to be ready
  wait_for_watcher("main.js", &mut stderr_lines).await;

  // Wait for initial run
  wait_contains("FOO: simple_value", &mut stdout_lines).await;
  // Check for multiline content - this might be the issue if multiline parsing doesn't work
  let multiline_line = wait_contains("MULTILINE:", &mut stdout_lines).await;
  assert!(
    multiline_line.contains("First Line"),
    "Multiline values should be parsed correctly"
  );
  // Note: The newline in multiline might not appear as separate lines in console.log output
  // This assertion might need adjustment based on how Deno actually handles multiline env vars
  wait_contains("BAR: another_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  // Ensure initial run is complete before modifying files
  wait_contains("Process finished", &mut stderr_lines).await;

  // Update the multiline value
  std::fs::write(&env_file, "FOO=simple_value\nMULTILINE=\"Updated First Line\nUpdated Second Line\nThird Line\"\nBAR=another_value").unwrap();

  // Wait for restart
  wait_contains("Restarting", &mut stderr_lines).await;

  // Check updated multiline value
  wait_contains("FOO: simple_value", &mut stdout_lines).await;
  let multiline_line = wait_contains("MULTILINE:", &mut stdout_lines).await;
  assert!(
    multiline_line.contains("Updated First Line"),
    "Updated multiline values should be reflected"
  );
  wait_contains("BAR: another_value", &mut stdout_lines).await;
  wait_contains("---", &mut stdout_lines).await;

  check_alive_then_kill(child);
}

#[test(flaky)]
async fn test_watch_env_file() {
  let t = TempDir::new();

  // Create initial .env file
  let env_file = t.path().join(".env");
  std::fs::write(&env_file, "FOO=initial_value\nBAR=test_value").unwrap();

  // Create test that reads environment variables
  let test_file = t.path().join("env_test.ts");
  std::fs::write(
    &test_file,
    r#"
Deno.test("env test", () => {
  console.log("FOO:", Deno.env.get("FOO"));
  console.log("BAR:", Deno.env.get("BAR"));
});
"#,
  )
  .unwrap();

  let mut child = util::deno_cmd()
    .current_dir(t.path())
    .arg("test")
    .arg("--watch")
    .arg("--no-check")
    .arg("--allow-env")
    .arg("--env-file=.env")
    .arg(&test_file)
    .env("NO_COLOR", "1")
    .piped_output()
    .spawn()
    .unwrap();
  let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);

  // Wait for initial run
  wait_contains("FOO: initial_value", &mut stdout_lines).await;
  wait_contains("BAR: test_value", &mut stdout_lines).await;
  wait_contains("Test finished", &mut stderr_lines).await;

  // Change the .env file
  std::fs::write(&env_file, "FOO=updated_value\nBAR=new_value").unwrap();

  // Wait for restart and verify updated values
  wait_contains("Restarting", &mut stderr_lines).await;
  wait_contains("FOO: updated_value", &mut stdout_lines).await;
  wait_contains("BAR: new_value", &mut stdout_lines).await;

  check_alive_then_kill(child);
}
