// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

use crate::args::ConfigFlag;
use crate::args::Flags;
use crate::args::TaskFlags;
use crate::fs_util;
use crate::fs_util::canonicalize_path;
use crate::fs_util::specifier_parent;
use crate::fs_util::specifier_to_file_path;

use deno_core::anyhow::anyhow;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde::Serializer;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::ModuleSpecifier;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;

pub type MaybeImportsResult =
  Result<Option<Vec<(ModuleSpecifier, Vec<String>)>>, AnyError>;

pub struct JsxImportSourceConfig {
  pub default_specifier: Option<String>,
  pub module: String,
}

/// The transpile options that are significant out of a user provided tsconfig
/// file, that we want to deserialize out of the final config for a transpile.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmitConfigOptions {
  pub check_js: bool,
  pub emit_decorator_metadata: bool,
  pub imports_not_used_as_values: String,
  pub inline_source_map: bool,
  pub inline_sources: bool,
  pub source_map: bool,
  pub jsx: String,
  pub jsx_factory: String,
  pub jsx_fragment_factory: String,
  pub jsx_import_source: Option<String>,
}

/// There are certain compiler options that can impact what modules are part of
/// a module graph, which need to be deserialized into a structure for analysis.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
  pub jsx: Option<String>,
  pub jsx_import_source: Option<String>,
  pub types: Option<Vec<String>>,
}

/// A structure that represents a set of options that were ignored and the
/// path those options came from.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct IgnoredCompilerOptions {
  pub items: Vec<String>,
  pub maybe_specifier: Option<ModuleSpecifier>,
}

impl fmt::Display for IgnoredCompilerOptions {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    let mut codes = self.items.clone();
    codes.sort();
    if let Some(specifier) = &self.maybe_specifier {
      write!(f, "Unsupported compiler options in \"{}\".\n  The following options were ignored:\n    {}", specifier, codes.join(", "))
    } else {
      write!(f, "Unsupported compiler options provided.\n  The following options were ignored:\n    {}", codes.join(", "))
    }
  }
}

impl Serialize for IgnoredCompilerOptions {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    Serialize::serialize(&self.items, serializer)
  }
}

/// A static slice of all the compiler options that should be ignored that
/// either have no effect on the compilation or would cause the emit to not work
/// in Deno.
pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
  "allowSyntheticDefaultImports",
  "allowUmdGlobalAccess",
  "assumeChangesOnlyAffectDirectDependencies",
  "baseUrl",
  "build",
  "charset",
  "composite",
  "declaration",
  "declarationMap",
  "diagnostics",
  "disableSizeLimit",
  "downlevelIteration",
  "emitBOM",
  "emitDeclarationOnly",
  "esModuleInterop",
  "experimentalDecorators",
  "extendedDiagnostics",
  "forceConsistentCasingInFileNames",
  "generateCpuProfile",
  "help",
  "importHelpers",
  "incremental",
  "init",
  "inlineSourceMap",
  "inlineSources",
  "isolatedModules",
  "listEmittedFiles",
  "listFiles",
  "mapRoot",
  "maxNodeModuleJsDepth",
  "module",
  "moduleDetection",
  "moduleResolution",
  "newLine",
  "noEmit",
  "noEmitHelpers",
  "noEmitOnError",
  "noErrorTruncation",
  "noLib",
  "noResolve",
  "out",
  "outDir",
  "outFile",
  "paths",
  "preserveConstEnums",
  "preserveSymlinks",
  "preserveWatchOutput",
  "pretty",
  "project",
  "reactNamespace",
  "resolveJsonModule",
  "rootDir",
  "rootDirs",
  "showConfig",
  "skipDefaultLibCheck",
  "skipLibCheck",
  "sourceMap",
  "sourceRoot",
  "stripInternal",
  "target",
  "traceResolution",
  "tsBuildInfoFile",
  "typeRoots",
  "useDefineForClassFields",
  "version",
  "watch",
];

/// A function that works like JavaScript's `Object.assign()`.
pub fn json_merge(a: &mut Value, b: &Value) {
  match (a, b) {
    (&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
      for (k, v) in b {
        json_merge(a.entry(k.clone()).or_insert(Value::Null), v);
      }
    }
    (a, b) => {
      *a = b.clone();
    }
  }
}

fn parse_compiler_options(
  compiler_options: &HashMap<String, Value>,
  maybe_specifier: Option<ModuleSpecifier>,
) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> {
  let mut filtered: HashMap<String, Value> = HashMap::new();
  let mut items: Vec<String> = Vec::new();

  for (key, value) in compiler_options.iter() {
    let key = key.as_str();
    if IGNORED_COMPILER_OPTIONS.contains(&key) {
      items.push(key.to_string());
    } else {
      filtered.insert(key.to_string(), value.to_owned());
    }
  }
  let value = serde_json::to_value(filtered)?;
  let maybe_ignored_options = if !items.is_empty() {
    Some(IgnoredCompilerOptions {
      items,
      maybe_specifier,
    })
  } else {
    None
  };

  Ok((value, maybe_ignored_options))
}

/// A structure for managing the configuration of TypeScript
#[derive(Debug, Clone)]
pub struct TsConfig(pub Value);

impl TsConfig {
  /// Create a new `TsConfig` with the base being the `value` supplied.
  pub fn new(value: Value) -> Self {
    TsConfig(value)
  }

  pub fn as_bytes(&self) -> Vec<u8> {
    let map = self.0.as_object().unwrap();
    let ordered: BTreeMap<_, _> = map.iter().collect();
    let value = json!(ordered);
    value.to_string().as_bytes().to_owned()
  }

  /// Return the value of the `checkJs` compiler option, defaulting to `false`
  /// if not present.
  pub fn get_check_js(&self) -> bool {
    if let Some(check_js) = self.0.get("checkJs") {
      check_js.as_bool().unwrap_or(false)
    } else {
      false
    }
  }

  pub fn get_declaration(&self) -> bool {
    if let Some(declaration) = self.0.get("declaration") {
      declaration.as_bool().unwrap_or(false)
    } else {
      false
    }
  }

  /// Merge a serde_json value into the configuration.
  pub fn merge(&mut self, value: &Value) {
    json_merge(&mut self.0, value);
  }

  /// Take an optional user provided config file
  /// which was passed in via the `--config` flag and merge `compilerOptions` with
  /// the configuration.  Returning the result which optionally contains any
  /// compiler options that were ignored.
  pub fn merge_tsconfig_from_config_file(
    &mut self,
    maybe_config_file: Option<&ConfigFile>,
  ) -> Result<Option<IgnoredCompilerOptions>, AnyError> {
    if let Some(config_file) = maybe_config_file {
      let (value, maybe_ignored_options) = config_file.to_compiler_options()?;
      self.merge(&value);
      Ok(maybe_ignored_options)
    } else {
      Ok(None)
    }
  }
}

impl Serialize for TsConfig {
  /// Serializes inner hash map which is ordered by the key
  fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    Serialize::serialize(&self.0, serializer)
  }
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct LintRulesConfig {
  pub tags: Option<Vec<String>>,
  pub include: Option<Vec<String>>,
  pub exclude: Option<Vec<String>>,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct SerializedFilesConfig {
  pub include: Vec<String>,
  pub exclude: Vec<String>,
}

impl SerializedFilesConfig {
  pub fn into_resolved(
    self,
    config_file_specifier: &ModuleSpecifier,
  ) -> Result<FilesConfig, AnyError> {
    let config_dir = specifier_parent(config_file_specifier);
    Ok(FilesConfig {
      include: self
        .include
        .into_iter()
        .map(|p| config_dir.join(&p))
        .collect::<Result<Vec<ModuleSpecifier>, _>>()?,
      exclude: self
        .exclude
        .into_iter()
        .map(|p| config_dir.join(&p))
        .collect::<Result<Vec<ModuleSpecifier>, _>>()?,
    })
  }
}

#[derive(Clone, Debug, Default)]
pub struct FilesConfig {
  pub include: Vec<ModuleSpecifier>,
  pub exclude: Vec<ModuleSpecifier>,
}

impl FilesConfig {
  /// Gets if the provided specifier is allowed based on the includes
  /// and excludes in the configuration file.
  pub fn matches_specifier(&self, specifier: &ModuleSpecifier) -> bool {
    // Skip files which is in the exclude list.
    let specifier_text = specifier.as_str();
    if self
      .exclude
      .iter()
      .any(|i| specifier_text.starts_with(i.as_str()))
    {
      return false;
    }

    // Ignore files not in the include list if it's not empty.
    self.include.is_empty()
      || self
        .include
        .iter()
        .any(|i| specifier_text.starts_with(i.as_str()))
  }
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct SerializedLintConfig {
  pub rules: LintRulesConfig,
  pub files: SerializedFilesConfig,
  pub report: Option<String>,
}

impl SerializedLintConfig {
  pub fn into_resolved(
    self,
    config_file_specifier: &ModuleSpecifier,
  ) -> Result<LintConfig, AnyError> {
    Ok(LintConfig {
      rules: self.rules,
      files: self.files.into_resolved(config_file_specifier)?,
      report: self.report,
    })
  }
}

#[derive(Clone, Debug, Default)]
pub struct LintConfig {
  pub rules: LintRulesConfig,
  pub files: FilesConfig,
  pub report: Option<String>,
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub enum ProseWrap {
  Always,
  Never,
  Preserve,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
pub struct FmtOptionsConfig {
  pub use_tabs: Option<bool>,
  pub line_width: Option<u32>,
  pub indent_width: Option<u8>,
  pub single_quote: Option<bool>,
  pub prose_wrap: Option<ProseWrap>,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct SerializedFmtConfig {
  pub options: FmtOptionsConfig,
  pub files: SerializedFilesConfig,
}

impl SerializedFmtConfig {
  pub fn into_resolved(
    self,
    config_file_specifier: &ModuleSpecifier,
  ) -> Result<FmtConfig, AnyError> {
    Ok(FmtConfig {
      options: self.options,
      files: self.files.into_resolved(config_file_specifier)?,
    })
  }
}

#[derive(Clone, Debug, Default)]
pub struct FmtConfig {
  pub options: FmtOptionsConfig,
  pub files: FilesConfig,
}

#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct SerializedTestConfig {
  pub files: SerializedFilesConfig,
}

impl SerializedTestConfig {
  pub fn into_resolved(
    self,
    config_file_specifier: &ModuleSpecifier,
  ) -> Result<TestConfig, AnyError> {
    Ok(TestConfig {
      files: self.files.into_resolved(config_file_specifier)?,
    })
  }
}

#[derive(Clone, Debug, Default)]
pub struct TestConfig {
  pub files: FilesConfig,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigFileJson {
  pub compiler_options: Option<Value>,
  pub import_map: Option<String>,
  pub lint: Option<Value>,
  pub fmt: Option<Value>,
  pub tasks: Option<Value>,
  pub test: Option<Value>,
}

#[derive(Clone, Debug)]
pub struct ConfigFile {
  pub specifier: ModuleSpecifier,
  pub json: ConfigFileJson,
}

impl ConfigFile {
  pub fn discover(flags: &Flags) -> Result<Option<ConfigFile>, AnyError> {
    match &flags.config_flag {
      ConfigFlag::Disabled => Ok(None),
      ConfigFlag::Path(config_path) => Ok(Some(ConfigFile::read(config_path)?)),
      ConfigFlag::Discover => {
        if let Some(config_path_args) = flags.config_path_args() {
          let mut checked = HashSet::new();
          for f in config_path_args {
            if let Some(cf) = Self::discover_from(&f, &mut checked)? {
              return Ok(Some(cf));
            }
          }
          // attempt to resolve the config file from the task subcommand's
          // `--cwd` when specified
          if let crate::args::DenoSubcommand::Task(TaskFlags {
            cwd: Some(path),
            ..
          }) = &flags.subcommand
          {
            let task_cwd = fs_util::canonicalize_path(&PathBuf::from(path))?;
            if let Some(path) = Self::discover_from(&task_cwd, &mut checked)? {
              return Ok(Some(path));
            }
          };
          // From CWD walk up to root looking for deno.json or deno.jsonc
          let cwd = std::env::current_dir()?;
          Self::discover_from(&cwd, &mut checked)
        } else {
          Ok(None)
        }
      }
    }
  }

  pub fn discover_from(
    start: &Path,
    checked: &mut HashSet<PathBuf>,
  ) -> Result<Option<ConfigFile>, AnyError> {
    /// Filenames that Deno will recognize when discovering config.
    const CONFIG_FILE_NAMES: [&str; 2] = ["deno.json", "deno.jsonc"];

    for ancestor in start.ancestors() {
      if checked.insert(ancestor.to_path_buf()) {
        for config_filename in CONFIG_FILE_NAMES {
          let f = ancestor.join(config_filename);
          match ConfigFile::read(f) {
            Ok(cf) => {
              return Ok(Some(cf));
            }
            Err(e) => {
              if let Some(ioerr) = e.downcast_ref::<std::io::Error>() {
                use std::io::ErrorKind::*;
                match ioerr.kind() {
                  InvalidInput | PermissionDenied | NotFound => {
                    // ok keep going
                  }
                  _ => {
                    return Err(e); // Unknown error. Stop.
                  }
                }
              } else {
                return Err(e); // Parse error or something else. Stop.
              }
            }
          }
        }
      }
    }
    // No config file found.
    Ok(None)
  }

  pub fn read(path_ref: impl AsRef<Path>) -> Result<Self, AnyError> {
    let path = Path::new(path_ref.as_ref());
    let config_file = if path.is_absolute() {
      path.to_path_buf()
    } else {
      std::env::current_dir()?.join(path_ref)
    };

    // perf: Check if the config file exists before canonicalizing path.
    if !config_file.exists() {
      return Err(
        std::io::Error::new(
          std::io::ErrorKind::InvalidInput,
          format!(
            "Could not find the config file: {}",
            config_file.to_string_lossy()
          ),
        )
        .into(),
      );
    }

    let config_path = canonicalize_path(&config_file).map_err(|_| {
      std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        format!(
          "Could not find the config file: {}",
          config_file.to_string_lossy()
        ),
      )
    })?;
    let config_specifier = ModuleSpecifier::from_file_path(&config_path)
      .map_err(|_| {
        anyhow!(
          "Could not convert path to specifier. Path: {}",
          config_path.display()
        )
      })?;
    Self::from_specifier(&config_specifier)
  }

  pub fn from_specifier(specifier: &ModuleSpecifier) -> Result<Self, AnyError> {
    let config_path = specifier_to_file_path(specifier)?;
    let config_text = match std::fs::read_to_string(&config_path) {
      Ok(text) => text,
      Err(err) => bail!(
        "Error reading config file {}: {}",
        specifier,
        err.to_string()
      ),
    };
    Self::new(&config_text, specifier)
  }

  pub fn new(
    text: &str,
    specifier: &ModuleSpecifier,
  ) -> Result<Self, AnyError> {
    let jsonc =
      match jsonc_parser::parse_to_serde_value(text, &Default::default()) {
        Ok(None) => json!({}),
        Ok(Some(value)) if value.is_object() => value,
        Ok(Some(_)) => {
          return Err(anyhow!(
            "config file JSON {} should be an object",
            specifier,
          ))
        }
        Err(e) => {
          return Err(anyhow!(
            "Unable to parse config file JSON {} because of {}",
            specifier,
            e.to_string()
          ))
        }
      };
    let json: ConfigFileJson = serde_json::from_value(jsonc)?;

    Ok(Self {
      specifier: specifier.to_owned(),
      json,
    })
  }

  /// Returns true if the configuration indicates that JavaScript should be
  /// type checked, otherwise false.
  pub fn get_check_js(&self) -> bool {
    self
      .json
      .compiler_options
      .as_ref()
      .and_then(|co| co.get("checkJs").and_then(|v| v.as_bool()))
      .unwrap_or(false)
  }

  /// Parse `compilerOptions` and return a serde `Value`.
  /// The result also contains any options that were ignored.
  pub fn to_compiler_options(
    &self,
  ) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> {
    if let Some(compiler_options) = self.json.compiler_options.clone() {
      let options: HashMap<String, Value> =
        serde_json::from_value(compiler_options)
          .context("compilerOptions should be an object")?;
      parse_compiler_options(&options, Some(self.specifier.to_owned()))
    } else {
      Ok((json!({}), None))
    }
  }

  pub fn to_import_map_path(&self) -> Option<String> {
    self.json.import_map.clone()
  }

  pub fn to_lint_config(&self) -> Result<Option<LintConfig>, AnyError> {
    if let Some(config) = self.json.lint.clone() {
      let lint_config: SerializedLintConfig = serde_json::from_value(config)
        .context("Failed to parse \"lint\" configuration")?;
      Ok(Some(lint_config.into_resolved(&self.specifier)?))
    } else {
      Ok(None)
    }
  }

  pub fn to_test_config(&self) -> Result<Option<TestConfig>, AnyError> {
    if let Some(config) = self.json.test.clone() {
      let lint_config: SerializedTestConfig = serde_json::from_value(config)
        .context("Failed to parse \"test\" configuration")?;
      Ok(Some(lint_config.into_resolved(&self.specifier)?))
    } else {
      Ok(None)
    }
  }

  /// Return any tasks that are defined in the configuration file as a sequence
  /// of JSON objects providing the name of the task and the arguments of the
  /// task in a detail field.
  pub fn to_lsp_tasks(&self) -> Option<Value> {
    let value = self.json.tasks.clone()?;
    let tasks: BTreeMap<String, String> = serde_json::from_value(value).ok()?;
    Some(
      tasks
        .into_iter()
        .map(|(key, value)| {
          json!({
            "name": key,
            "detail": value,
          })
        })
        .collect(),
    )
  }

  pub fn to_tasks_config(
    &self,
  ) -> Result<Option<BTreeMap<String, String>>, AnyError> {
    if let Some(config) = self.json.tasks.clone() {
      let tasks_config: BTreeMap<String, String> =
        serde_json::from_value(config)
          .context("Failed to parse \"tasks\" configuration")?;
      Ok(Some(tasks_config))
    } else {
      Ok(None)
    }
  }

  /// If the configuration file contains "extra" modules (like TypeScript
  /// `"types"`) options, return them as imports to be added to a module graph.
  pub fn to_maybe_imports(&self) -> MaybeImportsResult {
    let mut imports = Vec::new();
    let compiler_options_value =
      if let Some(value) = self.json.compiler_options.as_ref() {
        value
      } else {
        return Ok(None);
      };
    let compiler_options: CompilerOptions =
      serde_json::from_value(compiler_options_value.clone())?;
    if let Some(types) = compiler_options.types {
      imports.extend(types);
    }
    if !imports.is_empty() {
      let referrer = self.specifier.clone();
      Ok(Some(vec![(referrer, imports)]))
    } else {
      Ok(None)
    }
  }

  /// Based on the compiler options in the configuration file, return the
  /// JSX import source configuration.
  pub fn to_maybe_jsx_import_source_config(
    &self,
  ) -> Option<JsxImportSourceConfig> {
    let compiler_options_value = self.json.compiler_options.as_ref()?;
    let compiler_options: CompilerOptions =
      serde_json::from_value(compiler_options_value.clone()).ok()?;
    let module = match compiler_options.jsx.as_deref() {
      Some("react-jsx") => Some("jsx-runtime".to_string()),
      Some("react-jsxdev") => Some("jsx-dev-runtime".to_string()),
      _ => None,
    };
    module.map(|module| JsxImportSourceConfig {
      default_specifier: compiler_options.jsx_import_source,
      module,
    })
  }

  pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, AnyError> {
    if let Some(config) = self.json.fmt.clone() {
      let fmt_config: SerializedFmtConfig = serde_json::from_value(config)
        .context("Failed to parse \"fmt\" configuration")?;
      Ok(Some(fmt_config.into_resolved(&self.specifier)?))
    } else {
      Ok(None)
    }
  }

  pub fn resolve_tasks_config(
    &self,
  ) -> Result<BTreeMap<String, String>, AnyError> {
    let maybe_tasks_config = self.to_tasks_config()?;
    if let Some(tasks_config) = maybe_tasks_config {
      for key in tasks_config.keys() {
        if key.is_empty() {
          bail!("Configuration file task names cannot be empty");
        } else if !key
          .chars()
          .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ':'))
        {
          bail!("Configuration file task names must only contain alpha-numeric characters, colons (:), underscores (_), or dashes (-). Task: {}", key);
        } else if !key.chars().next().unwrap().is_ascii_alphabetic() {
          bail!("Configuration file task names must start with an alphabetic character. Task: {}", key);
        }
      }
      Ok(tasks_config)
    } else {
      bail!("No tasks found in configuration file")
    }
  }
}

/// Represents the "default" type library that should be used when type
/// checking the code in the module graph.  Note that a user provided config
/// of `"lib"` would override this value.
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum TsTypeLib {
  DenoWindow,
  DenoWorker,
  UnstableDenoWindow,
  UnstableDenoWorker,
}

impl Default for TsTypeLib {
  fn default() -> Self {
    Self::DenoWindow
  }
}

impl Serialize for TsTypeLib {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    let value = match self {
      Self::DenoWindow => vec!["deno.window".to_string()],
      Self::DenoWorker => vec!["deno.worker".to_string()],
      Self::UnstableDenoWindow => {
        vec!["deno.window".to_string(), "deno.unstable".to_string()]
      }
      Self::UnstableDenoWorker => {
        vec!["deno.worker".to_string(), "deno.unstable".to_string()]
      }
    };
    Serialize::serialize(&value, serializer)
  }
}

/// An enum that represents the base tsc configuration to return.
pub enum TsConfigType {
  /// Return a configuration for bundling, using swc to emit the bundle. This is
  /// independent of type checking.
  Bundle,
  /// Return a configuration to use tsc to type check. This
  /// is independent of either bundling or emitting via swc.
  Check { lib: TsTypeLib },
  /// Return a configuration to use swc to emit single module files.
  Emit,
}

pub struct TsConfigForEmit {
  pub ts_config: TsConfig,
  pub maybe_ignored_options: Option<IgnoredCompilerOptions>,
}

/// For a given configuration type and optionally a configuration file,
/// return a `TsConfig` struct and optionally any user configuration
/// options that were ignored.
pub fn get_ts_config_for_emit(
  config_type: TsConfigType,
  maybe_config_file: Option<&ConfigFile>,
) -> Result<TsConfigForEmit, AnyError> {
  let mut ts_config = match config_type {
    TsConfigType::Bundle => TsConfig::new(json!({
      "checkJs": false,
      "emitDecoratorMetadata": false,
      "importsNotUsedAsValues": "remove",
      "inlineSourceMap": false,
      "inlineSources": false,
      "sourceMap": false,
      "jsx": "react",
      "jsxFactory": "React.createElement",
      "jsxFragmentFactory": "React.Fragment",
    })),
    TsConfigType::Check { lib } => TsConfig::new(json!({
      "allowJs": true,
      "allowSyntheticDefaultImports": true,
      "checkJs": false,
      "emitDecoratorMetadata": false,
      "experimentalDecorators": true,
      "incremental": true,
      "jsx": "react",
      "importsNotUsedAsValues": "remove",
      "inlineSourceMap": true,
      "inlineSources": true,
      "isolatedModules": true,
      "lib": lib,
      "module": "esnext",
      "moduleDetection": "force",
      "noEmit": true,
      "resolveJsonModule": true,
      "sourceMap": false,
      "strict": true,
      "target": "esnext",
      "tsBuildInfoFile": "deno:///.tsbuildinfo",
      "useDefineForClassFields": true,
      // TODO(@kitsonk) remove for Deno 2.0
      "useUnknownInCatchVariables": false,
    })),
    TsConfigType::Emit => TsConfig::new(json!({
      "checkJs": false,
      "emitDecoratorMetadata": false,
      "importsNotUsedAsValues": "remove",
      "inlineSourceMap": true,
      "inlineSources": true,
      "sourceMap": false,
      "jsx": "react",
      "jsxFactory": "React.createElement",
      "jsxFragmentFactory": "React.Fragment",
      "resolveJsonModule": true,
    })),
  };
  let maybe_ignored_options =
    ts_config.merge_tsconfig_from_config_file(maybe_config_file)?;
  Ok(TsConfigForEmit {
    ts_config,
    maybe_ignored_options,
  })
}

impl From<TsConfig> for deno_ast::EmitOptions {
  fn from(config: TsConfig) -> Self {
    let options: EmitConfigOptions = serde_json::from_value(config.0).unwrap();
    let imports_not_used_as_values =
      match options.imports_not_used_as_values.as_str() {
        "preserve" => deno_ast::ImportsNotUsedAsValues::Preserve,
        "error" => deno_ast::ImportsNotUsedAsValues::Error,
        _ => deno_ast::ImportsNotUsedAsValues::Remove,
      };
    let (transform_jsx, jsx_automatic, jsx_development) =
      match options.jsx.as_str() {
        "react" => (true, false, false),
        "react-jsx" => (true, true, false),
        "react-jsxdev" => (true, true, true),
        _ => (false, false, false),
      };
    deno_ast::EmitOptions {
      emit_metadata: options.emit_decorator_metadata,
      imports_not_used_as_values,
      inline_source_map: options.inline_source_map,
      inline_sources: options.inline_sources,
      source_map: options.source_map,
      jsx_automatic,
      jsx_development,
      jsx_factory: options.jsx_factory,
      jsx_fragment_factory: options.jsx_fragment_factory,
      jsx_import_source: options.jsx_import_source,
      transform_jsx,
      var_decl_imports: false,
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use deno_core::serde_json::json;
  use pretty_assertions::assert_eq;

  #[test]
  fn read_config_file_relative() {
    let config_file =
      ConfigFile::read("tests/testdata/module_graph/tsconfig.json")
        .expect("Failed to load config file");
    assert!(config_file.json.compiler_options.is_some());
  }

  #[test]
  fn read_config_file_absolute() {
    let path = test_util::testdata_path().join("module_graph/tsconfig.json");
    let config_file = ConfigFile::read(path.to_str().unwrap())
      .expect("Failed to load config file");
    assert!(config_file.json.compiler_options.is_some());
  }

  #[test]
  fn include_config_path_on_error() {
    let error = ConfigFile::read("404.json").err().unwrap();
    assert!(error.to_string().contains("404.json"));
  }

  #[test]
  fn test_json_merge() {
    let mut value_a = json!({
      "a": true,
      "b": "c"
    });
    let value_b = json!({
      "b": "d",
      "e": false,
    });
    json_merge(&mut value_a, &value_b);
    assert_eq!(
      value_a,
      json!({
        "a": true,
        "b": "d",
        "e": false,
      })
    );
  }

  #[test]
  fn test_parse_config() {
    let config_text = r#"{
      "compilerOptions": {
        "build": true,
        // comments are allowed
        "strict": true
      },
      "lint": {
        "files": {
          "include": ["src/"],
          "exclude": ["src/testdata/"]
        },
        "rules": {
          "tags": ["recommended"],
          "include": ["ban-untagged-todo"]
        }
      },
      "fmt": {
        "files": {
          "include": ["src/"],
          "exclude": ["src/testdata/"]
        },
        "options": {
          "useTabs": true,
          "lineWidth": 80,
          "indentWidth": 4,
          "singleQuote": true,
          "proseWrap": "preserve"
        }
      },
      "tasks": {
        "build": "deno run --allow-read --allow-write build.ts",
        "server": "deno run --allow-net --allow-read server.ts"
      }
    }"#;
    let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap();
    let config_specifier = config_dir.join("tsconfig.json").unwrap();
    let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
    let (options_value, ignored) =
      config_file.to_compiler_options().expect("error parsing");
    assert!(options_value.is_object());
    let options = options_value.as_object().unwrap();
    assert!(options.contains_key("strict"));
    assert_eq!(options.len(), 1);
    assert_eq!(
      ignored,
      Some(IgnoredCompilerOptions {
        items: vec!["build".to_string()],
        maybe_specifier: Some(config_specifier),
      }),
    );

    let lint_config = config_file
      .to_lint_config()
      .expect("error parsing lint object")
      .expect("lint object should be defined");
    assert_eq!(
      lint_config.files.include,
      vec![config_dir.join("src/").unwrap()]
    );
    assert_eq!(
      lint_config.files.exclude,
      vec![config_dir.join("src/testdata/").unwrap()]
    );
    assert_eq!(
      lint_config.rules.include,
      Some(vec!["ban-untagged-todo".to_string()])
    );
    assert_eq!(
      lint_config.rules.tags,
      Some(vec!["recommended".to_string()])
    );
    assert!(lint_config.rules.exclude.is_none());

    let fmt_config = config_file
      .to_fmt_config()
      .expect("error parsing fmt object")
      .expect("fmt object should be defined");
    assert_eq!(
      fmt_config.files.include,
      vec![config_dir.join("src/").unwrap()]
    );
    assert_eq!(
      fmt_config.files.exclude,
      vec![config_dir.join("src/testdata/").unwrap()]
    );
    assert_eq!(fmt_config.options.use_tabs, Some(true));
    assert_eq!(fmt_config.options.line_width, Some(80));
    assert_eq!(fmt_config.options.indent_width, Some(4));
    assert_eq!(fmt_config.options.single_quote, Some(true));

    let tasks_config = config_file.to_tasks_config().unwrap().unwrap();
    assert_eq!(
      tasks_config["build"],
      "deno run --allow-read --allow-write build.ts",
    );
    assert_eq!(
      tasks_config["server"],
      "deno run --allow-net --allow-read server.ts"
    );
  }

  #[test]
  fn test_parse_config_with_empty_file() {
    let config_text = "";
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/tsconfig.json").unwrap();
    let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
    let (options_value, _) =
      config_file.to_compiler_options().expect("error parsing");
    assert!(options_value.is_object());
  }

  #[test]
  fn test_parse_config_with_commented_file() {
    let config_text = r#"//{"foo":"bar"}"#;
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/tsconfig.json").unwrap();
    let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
    let (options_value, _) =
      config_file.to_compiler_options().expect("error parsing");
    assert!(options_value.is_object());
  }

  #[test]
  fn test_parse_config_with_invalid_file() {
    let config_text = "{foo:bar}";
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/tsconfig.json").unwrap();
    // Emit error: Unable to parse config file JSON "<config_path>" because of Unexpected token on line 1 column 6.
    assert!(ConfigFile::new(config_text, &config_specifier).is_err());
  }

  #[test]
  fn test_parse_config_with_not_object_file() {
    let config_text = "[]";
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/tsconfig.json").unwrap();
    // Emit error: config file JSON "<config_path>" should be an object
    assert!(ConfigFile::new(config_text, &config_specifier).is_err());
  }

  #[test]
  fn test_tsconfig_as_bytes() {
    let mut tsconfig1 = TsConfig::new(json!({
      "strict": true,
      "target": "esnext",
    }));
    tsconfig1.merge(&json!({
      "target": "es5",
      "module": "amd",
    }));
    let mut tsconfig2 = TsConfig::new(json!({
      "target": "esnext",
      "strict": true,
    }));
    tsconfig2.merge(&json!({
      "module": "amd",
      "target": "es5",
    }));
    assert_eq!(tsconfig1.as_bytes(), tsconfig2.as_bytes());
  }

  #[test]
  fn discover_from_success() {
    // testdata/fmt/deno.jsonc exists
    let testdata = test_util::testdata_path();
    let c_md = testdata.join("fmt/with_config/subdir/c.md");
    let mut checked = HashSet::new();
    let config_file = ConfigFile::discover_from(&c_md, &mut checked)
      .unwrap()
      .unwrap();
    assert!(checked.contains(c_md.parent().unwrap()));
    assert!(!checked.contains(&testdata));
    let fmt_config = config_file.to_fmt_config().unwrap().unwrap();
    let expected_exclude = ModuleSpecifier::from_file_path(
      testdata.join("fmt/with_config/subdir/b.ts"),
    )
    .unwrap();
    assert_eq!(fmt_config.files.exclude, vec![expected_exclude]);

    // Now add all ancestors of testdata to checked.
    for a in testdata.ancestors() {
      checked.insert(a.to_path_buf());
    }

    // If we call discover_from again starting at testdata, we ought to get None.
    assert!(ConfigFile::discover_from(&testdata, &mut checked)
      .unwrap()
      .is_none());
  }

  #[test]
  fn discover_from_malformed() {
    let testdata = test_util::testdata_path();
    let d = testdata.join("malformed_config/");
    let mut checked = HashSet::new();
    let err = ConfigFile::discover_from(&d, &mut checked).unwrap_err();
    assert!(err.to_string().contains("Unable to parse config file"));
  }

  #[test]
  fn tasks_no_tasks() {
    run_task_error_test(r#"{}"#, "No tasks found in configuration file");
  }

  #[test]
  fn task_name_invalid_chars() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "some%test": "deno bundle mod.ts"
        }
      }"#,
      concat!(
        "Configuration file task names must only contain alpha-numeric ",
        "characters, colons (:), underscores (_), or dashes (-). Task: some%test",
      ),
    );
  }

  #[test]
  fn task_name_non_alpha_starting_char() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "1test": "deno bundle mod.ts"
        }
      }"#,
      concat!(
        "Configuration file task names must start with an ",
        "alphabetic character. Task: 1test",
      ),
    );
  }

  #[test]
  fn task_name_empty() {
    run_task_error_test(
      r#"{
        "tasks": {
          "build": "deno test",
          "": "deno bundle mod.ts"
        }
      }"#,
      "Configuration file task names cannot be empty",
    );
  }

  fn run_task_error_test(config_text: &str, expected_error: &str) {
    let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap();
    let config_specifier = config_dir.join("tsconfig.json").unwrap();
    let config_file = ConfigFile::new(config_text, &config_specifier).unwrap();
    assert_eq!(
      config_file
        .resolve_tasks_config()
        .err()
        .unwrap()
        .to_string(),
      expected_error,
    );
  }
}