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

use std::borrow::Cow;
use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;

use anyhow::Context;
use pretty_assertions::assert_eq;
use serde::Serialize;
use serde::de::DeserializeOwned;
use url::Url;

use crate::assertions::assert_wildcard_match;
use crate::println;
use crate::testdata_path;

#[cfg(feature = "lsp")]
pub fn url_to_uri(url: &Url) -> Result<lsp_types::Uri, anyhow::Error> {
  use std::str::FromStr;

  use url::Position;
  let uri_before_path = lsp_types::Uri::from_str(&url[..Position::BeforePath])
    .map_err(|err| {
      anyhow::anyhow!("Could not convert URL \"{url}\" to URI: {err}")
    })?;
  let mut encoded_path =
    fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new();
  encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(
    &percent_encoding::percent_decode_str(url.path().trim_end_matches('/'))
      .decode_utf8_lossy(),
  );
  let encoded_query = url.query().map(|query| {
    let mut encoded_query = fluent_uri::pct_enc::EString::<
      fluent_uri::pct_enc::encoder::Query,
    >::new();
    encoded_query.encode_str::<fluent_uri::pct_enc::encoder::Query>(query);
    encoded_query
  });
  let encoded_fragment = url.fragment().map(|fragment| {
    let mut encoded_fragment = fluent_uri::pct_enc::EString::<
      fluent_uri::pct_enc::encoder::Fragment,
    >::new();
    encoded_fragment
      .encode_str::<fluent_uri::pct_enc::encoder::Fragment>(fragment);
    encoded_fragment
  });
  let uri = fluent_uri::Uri::builder()
    .scheme(uri_before_path.scheme())
    .optional(
      fluent_uri::build::Builder::authority,
      uri_before_path.authority(),
    )
    .path(encoded_path.as_ref())
    .optional(fluent_uri::build::Builder::query, encoded_query.as_deref())
    .optional(
      fluent_uri::build::Builder::fragment,
      encoded_fragment.as_deref(),
    )
    .build()
    .expect("component constraints should be met by the above")
    .normalize()
    .into();
  Ok(uri)
}

#[cfg(feature = "lsp")]
pub fn url_to_notebook_cell_uri(url: &Url) -> lsp_types::Uri {
  use std::str::FromStr;
  let uri = url_to_uri(url).unwrap();
  lsp_types::Uri::from_str(&format!(
    "vscode-notebook-cell:{}",
    uri.as_str().strip_prefix("file:").unwrap()
  ))
  .unwrap()
}

/// Represents a path on the file system, which can be used
/// to perform specific actions.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)]
pub struct PathRef(PathBuf);

impl AsRef<Path> for PathRef {
  fn as_ref(&self) -> &Path {
    self.as_path()
  }
}

impl AsRef<OsStr> for PathRef {
  fn as_ref(&self) -> &OsStr {
    self.as_path().as_ref()
  }
}

impl std::fmt::Display for PathRef {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{}", self.as_path().display())
  }
}

impl PathRef {
  pub fn new(path: impl AsRef<Path>) -> Self {
    Self(path.as_ref().to_path_buf())
  }

  pub fn parent(&self) -> PathRef {
    PathRef(self.as_path().parent().unwrap().to_path_buf())
  }

  pub fn maybe_parent(&self) -> Option<PathRef> {
    Some(PathRef(self.as_path().parent()?.to_path_buf()))
  }

  pub fn url_dir(&self) -> Url {
    Url::from_directory_path(self.as_path()).unwrap()
  }

  pub fn url_file(&self) -> Url {
    Url::from_file_path(self.as_path()).unwrap()
  }

  #[cfg(feature = "lsp")]
  pub fn uri_dir(&self) -> lsp_types::Uri {
    url_to_uri(&self.url_dir()).unwrap()
  }

  #[cfg(feature = "lsp")]
  pub fn uri_file(&self) -> lsp_types::Uri {
    url_to_uri(&self.url_file()).unwrap()
  }

  pub fn as_path(&self) -> &Path {
    self.0.as_path()
  }

  pub fn to_path_buf(&self) -> PathBuf {
    self.0.to_path_buf()
  }

  pub fn to_string_lossy(&self) -> Cow<'_, str> {
    self.0.to_string_lossy()
  }

  pub fn exists(&self) -> bool {
    self.0.exists()
  }

  pub fn try_exists(&self) -> std::io::Result<bool> {
    self.0.try_exists()
  }

  pub fn is_dir(&self) -> bool {
    self.0.is_dir()
  }

  pub fn is_file(&self) -> bool {
    self.0.is_file()
  }

  pub fn file_name(&self) -> Option<&OsStr> {
    self.0.file_name()
  }

  pub fn join(&self, path: impl AsRef<Path>) -> PathRef {
    PathRef(self.as_path().join(path))
  }

  pub fn with_extension(&self, ext: impl AsRef<OsStr>) -> PathRef {
    PathRef(self.as_path().with_extension(ext))
  }

  pub fn canonicalize(&self) -> PathRef {
    PathRef(strip_unc_prefix(self.as_path().canonicalize().unwrap()))
  }

  pub fn create_dir_all(&self) {
    fs::create_dir_all(self)
      .with_context(|| {
        format!(
          "Could not create directory: {}\n  IsFile: {}\n  IsDir: {}",
          self,
          self.is_file(),
          self.is_dir()
        )
      })
      .unwrap();
  }

  pub fn remove_file(&self) {
    fs::remove_file(self).unwrap();
  }

  pub fn remove_dir_all(&self) {
    fs::remove_dir_all(self).unwrap();
  }

  pub fn read_to_string(&self) -> String {
    self.read_to_string_if_exists().unwrap()
  }

  pub fn read_to_string_if_exists(&self) -> Result<String, anyhow::Error> {
    fs::read_to_string(self)
      .with_context(|| format!("Could not read file: {}", self))
  }

  pub fn read_to_bytes_if_exists(&self) -> Result<Vec<u8>, anyhow::Error> {
    fs::read(self).with_context(|| format!("Could not read file: {}", self))
  }

  #[track_caller]
  pub fn read_json<TValue: DeserializeOwned>(&self) -> TValue {
    serde_json::from_str(&self.read_to_string())
      .with_context(|| format!("Failed deserializing: {}", self))
      .unwrap()
  }

  #[track_caller]
  pub fn read_json_value(&self) -> serde_json::Value {
    serde_json::from_str(&self.read_to_string())
      .with_context(|| format!("Failed deserializing: {}", self))
      .unwrap()
  }

  #[track_caller]
  pub fn read_jsonc_value(&self) -> serde_json::Value {
    jsonc_parser::parse_to_serde_value(
      &self.read_to_string(),
      &Default::default(),
    )
    .with_context(|| format!("Failed to parse {}", self))
    .unwrap()
  }

  #[track_caller]
  pub fn rename(&self, to: impl AsRef<Path>) {
    let to = self.join(to);
    if let Some(parent_path) = to.maybe_parent() {
      parent_path.create_dir_all();
    }
    fs::rename(self, &to)
      .with_context(|| format!("Failed renaming {} to {}", self, to))
      .unwrap();
  }

  #[track_caller]
  pub fn append(&self, text: impl AsRef<str>) {
    let mut file = OpenOptions::new().append(true).open(self).unwrap();
    file.write_all(text.as_ref().as_bytes()).unwrap();
  }

  #[track_caller]
  pub fn write(&self, text: impl AsRef<[u8]>) {
    if let Some(parent_path) = self.as_path().parent() {
      fs::create_dir_all(parent_path).unwrap()
    }
    fs::write(self, text).unwrap();
  }

  #[track_caller]
  pub fn write_json<TValue: Serialize>(&self, value: &TValue) {
    let text = serde_json::to_string_pretty(value).unwrap();
    self.write(text);
  }

  #[track_caller]
  pub fn symlink_dir(
    &self,
    oldpath: impl AsRef<Path>,
    newpath: impl AsRef<Path>,
  ) {
    let oldpath = self.as_path().join(oldpath);
    let newpath = self.as_path().join(newpath);
    if let Some(parent_path) = newpath.parent() {
      fs::create_dir_all(parent_path).unwrap()
    }
    #[cfg(unix)]
    {
      use std::os::unix::fs::symlink;
      symlink(oldpath, newpath).unwrap();
    }
    #[cfg(not(unix))]
    {
      use std::os::windows::fs::symlink_dir;
      symlink_dir(oldpath, newpath).unwrap();
    }
  }

  #[track_caller]
  pub fn symlink_file(
    &self,
    oldpath: impl AsRef<Path>,
    newpath: impl AsRef<Path>,
  ) {
    let oldpath = self.as_path().join(oldpath);
    let newpath = self.as_path().join(newpath);
    if let Some(parent_path) = newpath.as_path().parent() {
      fs::create_dir_all(parent_path).unwrap()
    }
    #[cfg(unix)]
    {
      use std::os::unix::fs::symlink;
      symlink(oldpath, newpath).unwrap();
    }
    #[cfg(not(unix))]
    {
      use std::os::windows::fs::symlink_file;
      symlink_file(oldpath, newpath).unwrap();
    }
  }

  #[track_caller]
  pub fn read_dir(&self) -> fs::ReadDir {
    fs::read_dir(self.as_path())
      .with_context(|| format!("Reading {}", self.as_path().display()))
      .unwrap()
  }

  #[track_caller]
  pub fn copy(&self, to: &impl AsRef<Path>) {
    std::fs::copy(self.as_path(), to)
      .with_context(|| format!("Copying {} to {}", self, to.as_ref().display()))
      .unwrap();
  }

  /// Copies this directory to another directory.
  ///
  /// Note: Does not handle symlinks.
  pub fn copy_to_recursive(&self, to: &PathRef) {
    self.copy_to_recursive_with_exclusions(to, &HashSet::new())
  }

  pub fn copy_to_recursive_with_exclusions(
    &self,
    to: &PathRef,
    file_exclusions: &HashSet<PathRef>,
  ) {
    to.create_dir_all();
    let read_dir = self.read_dir();

    for entry in read_dir {
      let entry = entry.unwrap();
      let file_type = entry.file_type().unwrap();
      let new_from = self.join(entry.file_name());
      let new_to = to.join(entry.file_name());

      if file_type.is_dir() {
        new_from.copy_to_recursive(&new_to);
      } else if file_type.is_file() && !file_exclusions.contains(&new_from) {
        new_from.copy(&new_to);
      }
    }
  }

  #[track_caller]
  pub fn mark_executable(&self) {
    if cfg!(unix) {
      Command::new("chmod").arg("+x").arg(self).output().unwrap();
    }
  }

  #[track_caller]
  pub fn make_dir_readonly(&self) {
    self.create_dir_all();
    if cfg!(windows) {
      Command::new("attrib").arg("+r").arg(self).output().unwrap();
    } else if cfg!(unix) {
      Command::new("chmod").arg("555").arg(self).output().unwrap();
    }
  }

  #[track_caller]
  pub fn assert_matches_file(&self, wildcard_file: impl AsRef<Path>) -> &Self {
    let wildcard_file = testdata_path().join(wildcard_file);
    println!("output path {}", wildcard_file);
    let expected_text = wildcard_file.read_to_string();
    self.assert_matches_text(&expected_text)
  }

  #[track_caller]
  pub fn assert_matches_text(&self, wildcard_text: impl AsRef<str>) -> &Self {
    let actual = self.read_to_string();
    assert_wildcard_match(&actual, wildcard_text.as_ref());
    self
  }

  #[track_caller]
  pub fn assert_matches_json(&self, expected: serde_json::Value) {
    let actual_json = self.read_json_value();
    if actual_json != expected {
      let actual_text = serde_json::to_string_pretty(&actual_json).unwrap();
      let expected_text = serde_json::to_string_pretty(&expected).unwrap();
      assert_eq!(actual_text, expected_text);
    }
  }
}

#[cfg(not(windows))]
#[inline]
fn strip_unc_prefix(path: PathBuf) -> PathBuf {
  path
}

/// Strips the unc prefix (ex. \\?\) from Windows paths.
///
/// Lifted from deno_core for use in the tests.
#[cfg(windows)]
fn strip_unc_prefix(path: PathBuf) -> PathBuf {
  use std::path::Component;
  use std::path::Prefix;

  let mut components = path.components();
  match components.next() {
    Some(Component::Prefix(prefix)) => {
      match prefix.kind() {
        // \\?\device
        Prefix::Verbatim(device) => {
          let mut path = PathBuf::new();
          path.push(format!(r"\\{}\", device.to_string_lossy()));
          path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
          path
        }
        // \\?\c:\path
        Prefix::VerbatimDisk(_) => {
          let mut path = PathBuf::new();
          path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", ""));
          path.extend(components);
          path
        }
        // \\?\UNC\hostname\share_name\path
        Prefix::VerbatimUNC(hostname, share_name) => {
          let mut path = PathBuf::new();
          path.push(format!(
            r"\\{}\{}\",
            hostname.to_string_lossy(),
            share_name.to_string_lossy()
          ));
          path.extend(components.filter(|c| !matches!(c, Component::RootDir)));
          path
        }
        _ => path,
      }
    }
    _ => path,
  }
}

enum TempDirInner {
  TempDir {
    path_ref: PathRef,
    // kept alive for the duration of the temp dir
    _dir: tempfile::TempDir,
  },
  Path(PathRef),
  Symlinked {
    symlink: Arc<TempDirInner>,
    target: Arc<TempDirInner>,
  },
}

impl TempDirInner {
  pub fn path(&self) -> &PathRef {
    match self {
      Self::Path(path_ref) => path_ref,
      Self::TempDir { path_ref, .. } => path_ref,
      Self::Symlinked { symlink, .. } => symlink.path(),
    }
  }

  pub fn target_path(&self) -> &PathRef {
    match self {
      TempDirInner::Symlinked { target, .. } => target.target_path(),
      _ => self.path(),
    }
  }
}

impl Drop for TempDirInner {
  fn drop(&mut self) {
    if let Self::Path(path) = self {
      _ = fs::remove_dir_all(path);
    }
  }
}

/// For creating temporary directories in tests.
///
/// This was done because `tempfiles::TempDir` was very slow on Windows.
///
/// Note: Do not use this in actual code as this does not protect against
/// "insecure temporary file" security vulnerabilities.
#[derive(Clone)]
pub struct TempDir(Arc<TempDirInner>);

impl Default for TempDir {
  fn default() -> Self {
    Self::new()
  }
}

impl TempDir {
  pub fn new() -> Self {
    Self::new_inner(&std::env::temp_dir(), None)
  }

  pub fn new_with_prefix(prefix: &str) -> Self {
    Self::new_inner(&std::env::temp_dir(), Some(prefix))
  }

  pub fn new_in(parent_dir: &Path) -> Self {
    Self::new_inner(parent_dir, None)
  }

  pub fn new_with_path(path: &Path) -> Self {
    Self(Arc::new(TempDirInner::Path(PathRef(path.to_path_buf()))))
  }

  pub fn new_symlinked(target: TempDir) -> Self {
    let target_path = target.path();
    let path = target_path.parent().join(format!(
      "{}_symlinked",
      target_path.as_path().file_name().unwrap().to_str().unwrap()
    ));
    target.symlink_dir(target.path(), &path);
    TempDir(Arc::new(TempDirInner::Symlinked {
      target: target.0,
      symlink: Self::new_with_path(path.as_path()).0,
    }))
  }

  /// Create a new temporary directory with the given prefix as part of its name, if specified.
  fn new_inner(parent_dir: &Path, prefix: Option<&str>) -> Self {
    let mut builder = tempfile::Builder::new();
    builder.prefix(prefix.unwrap_or("deno-cli-test"));
    let dir = builder
      .tempdir_in(parent_dir)
      .expect("Failed to create a temporary directory");
    Self(Arc::new(TempDirInner::TempDir {
      path_ref: PathRef(dir.path().to_path_buf()),
      _dir: dir,
    }))
  }

  pub fn url(&self) -> Url {
    Url::from_directory_path(self.path()).unwrap()
  }

  #[cfg(feature = "lsp")]
  pub fn uri(&self) -> lsp_types::Uri {
    url_to_uri(&self.url()).unwrap()
  }

  pub fn path(&self) -> &PathRef {
    self.0.path()
  }

  /// The resolved final target path if this is a symlink.
  pub fn target_path(&self) -> &PathRef {
    self.0.target_path()
  }

  pub fn create_dir_all(&self, path: impl AsRef<Path>) {
    self.target_path().join(path).create_dir_all()
  }

  pub fn remove_file(&self, path: impl AsRef<Path>) {
    self.target_path().join(path).remove_file()
  }

  pub fn remove_dir_all(&self, path: impl AsRef<Path>) {
    self.target_path().join(path).remove_dir_all()
  }

  pub fn read_to_string(&self, path: impl AsRef<Path>) -> String {
    self.target_path().join(path).read_to_string()
  }

  pub fn rename(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) {
    self.target_path().join(from).rename(to)
  }

  pub fn write(&self, path: impl AsRef<Path>, text: impl AsRef<[u8]>) {
    self.target_path().join(path).write(text)
  }

  #[cfg(feature = "lsp")]
  pub fn source_file(
    &self,
    path: impl AsRef<Path>,
    text: impl AsRef<str>,
  ) -> crate::lsp::SourceFile {
    let path = self.target_path().join(path);
    crate::lsp::source_file(path, text)
  }

  pub fn symlink_dir(
    &self,
    oldpath: impl AsRef<Path>,
    newpath: impl AsRef<Path>,
  ) {
    self.target_path().symlink_dir(oldpath, newpath)
  }

  pub fn symlink_file(
    &self,
    oldpath: impl AsRef<Path>,
    newpath: impl AsRef<Path>,
  ) {
    self.target_path().symlink_file(oldpath, newpath)
  }
}
