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

use crate::ast;
use crate::ast::parse;
use crate::ast::Location;
use crate::ast::ParsedModule;
use crate::file_fetcher::TextDocument;
use crate::import_map::ImportMap;
use crate::lockfile::Lockfile;
use crate::media_type::MediaType;
use crate::specifier_handler::CachedModule;
use crate::specifier_handler::DependencyMap;
use crate::specifier_handler::EmitMap;
use crate::specifier_handler::EmitType;
use crate::specifier_handler::FetchFuture;
use crate::specifier_handler::SpecifierHandler;
use crate::tsc_config::IgnoredCompilerOptions;
use crate::tsc_config::TsConfig;
use crate::version;
use crate::AnyError;

use deno_core::futures::stream::FuturesUnordered;
use deno_core::futures::stream::StreamExt;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier;
use regex::Regex;
use serde::Deserialize;
use serde::Deserializer;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::error::Error;
use std::fmt;
use std::rc::Rc;
use std::result;
use std::sync::Mutex;
use std::time::Instant;
use swc_ecmascript::dep_graph::DependencyKind;

pub type BuildInfoMap = HashMap<EmitType, TextDocument>;

lazy_static! {
  /// Matched the `@deno-types` pragma.
  static ref DENO_TYPES_RE: Regex =
    Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#)
      .unwrap();
  /// Matches a `/// <reference ... />` comment reference.
  static ref TRIPLE_SLASH_REFERENCE_RE: Regex =
    Regex::new(r"(?i)^/\s*<reference\s.*?/>").unwrap();
  /// Matches a path reference, which adds a dependency to a module
  static ref PATH_REFERENCE_RE: Regex =
    Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap();
  /// Matches a types reference, which for JavaScript files indicates the
  /// location of types to use when type checking a program that includes it as
  /// a dependency.
  static ref TYPES_REFERENCE_RE: Regex =
    Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap();
}

/// A group of errors that represent errors that can occur when interacting with
/// a module graph.
#[allow(unused)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum GraphError {
  /// A module using the HTTPS protocol is trying to import a module with an
  /// HTTP schema.
  InvalidDowngrade(ModuleSpecifier, Location),
  /// A remote module is trying to import a local module.
  InvalidLocalImport(ModuleSpecifier, Location),
  /// A remote module is trying to import a local module.
  InvalidSource(ModuleSpecifier, String),
  /// A module specifier could not be resolved for a given import.
  InvalidSpecifier(String, Location),
  /// An unexpected dependency was requested for a module.
  MissingDependency(ModuleSpecifier, String),
  /// An unexpected specifier was requested.
  MissingSpecifier(ModuleSpecifier),
  /// Snapshot data was not present in a situation where it was required.
  MissingSnapshotData,
  /// The current feature is not supported.
  NotSupported(String),
}
use GraphError::*;

impl fmt::Display for GraphError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n  Importing: {}\n    at {}:{}:{}", specifier, location.filename, location.line, location.col),
      InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules.\n  Importing: {}\n    at {}:{}:{}", specifier, location.filename, location.line, location.col),
      InvalidSource(ref specifier, ref lockfile) => write!(f, "The source code is invalid, as it does not match the expected hash in the lock file.\n  Specifier: {}\n  Lock file: {}", specifier, lockfile),
      InvalidSpecifier(ref specifier, ref location) => write!(f, "Unable to resolve dependency specifier.\n  Specifier: {}\n    at {}:{}:{}", specifier, location.filename, location.line, location.col),
      MissingDependency(ref referrer, specifier) => write!(
        f,
        "The graph is missing a dependency.\n  Specifier: {} from {}",
        specifier, referrer
      ),
      MissingSpecifier(ref specifier) => write!(
        f,
        "The graph is missing a specifier.\n  Specifier: {}",
        specifier
      ),
      MissingSnapshotData => write!(f, "Snapshot data was not supplied, but required."),
      NotSupported(ref msg) => write!(f, "{}", msg),
    }
  }
}

impl Error for GraphError {}

/// A trait, implemented by `Graph` that provides the interfaces that the
/// compiler ops require to be able to retrieve information about the graph.
pub trait ModuleProvider {
  /// Get the source for a given module specifier.  If the module is not part
  /// of the graph, the result will be `None`.
  fn get_source(&self, specifier: &ModuleSpecifier) -> Option<String>;
  /// Given a string specifier and a referring module specifier, provide the
  /// resulting module specifier and media type for the module that is part of
  /// the graph.
  fn resolve(
    &self,
    specifier: &str,
    referrer: &ModuleSpecifier,
  ) -> Result<(ModuleSpecifier, MediaType), AnyError>;
}

/// An enum which represents the parsed out values of references in source code.
#[derive(Debug, Clone, Eq, PartialEq)]
enum TypeScriptReference {
  Path(String),
  Types(String),
}

/// Determine if a comment contains a triple slash reference and optionally
/// return its kind and value.
fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
  if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
    None
  } else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
    Some(TypeScriptReference::Path(
      captures.get(1).unwrap().as_str().to_string(),
    ))
  } else if let Some(captures) = TYPES_REFERENCE_RE.captures(comment) {
    Some(TypeScriptReference::Types(
      captures.get(1).unwrap().as_str().to_string(),
    ))
  } else {
    None
  }
}

/// Determine if a comment contains a `@deno-types` pragma and optionally return
/// its value.
fn parse_deno_types(comment: &str) -> Option<String> {
  if let Some(captures) = DENO_TYPES_RE.captures(comment) {
    if let Some(m) = captures.get(1) {
      Some(m.as_str().to_string())
    } else if let Some(m) = captures.get(2) {
      Some(m.as_str().to_string())
    } else {
      panic!("unreachable");
    }
  } else {
    None
  }
}

/// A hashing function that takes the source code, version and optionally a
/// user provided config and generates a string hash which can be stored to
/// determine if the cached emit is valid or not.
fn get_version(source: &TextDocument, version: &str, config: &[u8]) -> String {
  crate::checksum::gen(&[
    source.to_str().unwrap().as_bytes(),
    version.as_bytes(),
    config,
  ])
}

/// A logical representation of a module within a graph.
#[derive(Debug, Clone)]
struct Module {
  dependencies: DependencyMap,
  emits: EmitMap,
  is_dirty: bool,
  is_hydrated: bool,
  is_parsed: bool,
  maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
  maybe_parsed_module: Option<ParsedModule>,
  maybe_types: Option<(String, ModuleSpecifier)>,
  maybe_version: Option<String>,
  media_type: MediaType,
  specifier: ModuleSpecifier,
  source: TextDocument,
}

impl Default for Module {
  fn default() -> Self {
    Module {
      dependencies: HashMap::new(),
      emits: HashMap::new(),
      is_dirty: false,
      is_hydrated: false,
      is_parsed: false,
      maybe_import_map: None,
      maybe_parsed_module: None,
      maybe_types: None,
      maybe_version: None,
      media_type: MediaType::Unknown,
      specifier: ModuleSpecifier::resolve_url("https://deno.land/x/").unwrap(),
      source: TextDocument::new(Vec::new(), Option::<&str>::None),
    }
  }
}

impl Module {
  pub fn new(
    specifier: ModuleSpecifier,
    maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
  ) -> Self {
    Module {
      specifier,
      maybe_import_map,
      ..Module::default()
    }
  }

  /// Return `true` if the current hash of the module matches the stored
  /// version.
  pub fn emit_valid(&self, config: &[u8]) -> bool {
    if let Some(version) = self.maybe_version.clone() {
      version == get_version(&self.source, version::DENO, config)
    } else {
      false
    }
  }

  pub fn hydrate(&mut self, cached_module: CachedModule) {
    self.media_type = cached_module.media_type;
    self.source = cached_module.source;
    if self.maybe_import_map.is_none() {
      if let Some(dependencies) = cached_module.maybe_dependencies {
        self.dependencies = dependencies;
        self.is_parsed = true;
      }
    }
    self.maybe_types = if let Some(ref specifier) = cached_module.maybe_types {
      Some((
        specifier.clone(),
        self
          .resolve_import(&specifier, None)
          .expect("could not resolve module"),
      ))
    } else {
      None
    };
    self.is_dirty = false;
    self.emits = cached_module.emits;
    self.maybe_version = cached_module.maybe_version;
    self.is_hydrated = true;
  }

  pub fn parse(&mut self) -> Result<(), AnyError> {
    let parsed_module =
      parse(&self.specifier, &self.source.to_str()?, &self.media_type)?;

    // parse out any triple slash references
    for comment in parsed_module.get_leading_comments().iter() {
      if let Some(ts_reference) = parse_ts_reference(&comment.text) {
        let location: Location = parsed_module.get_location(&comment.span);
        match ts_reference {
          TypeScriptReference::Path(import) => {
            let specifier = self.resolve_import(&import, Some(location))?;
            let dep = self.dependencies.entry(import).or_default();
            dep.maybe_code = Some(specifier);
          }
          TypeScriptReference::Types(import) => {
            let specifier = self.resolve_import(&import, Some(location))?;
            if self.media_type == MediaType::JavaScript
              || self.media_type == MediaType::JSX
            {
              // TODO(kitsonk) we need to specifically update the cache when
              // this value changes
              self.maybe_types = Some((import.clone(), specifier));
            } else {
              let dep = self.dependencies.entry(import).or_default();
              dep.maybe_type = Some(specifier);
            }
          }
        }
      }
    }

    // Parse out all the syntactical dependencies for a module
    let dependencies = parsed_module.analyze_dependencies();
    for desc in dependencies
      .iter()
      .filter(|desc| desc.kind != DependencyKind::Require)
    {
      let location = Location {
        filename: self.specifier.to_string(),
        col: desc.col,
        line: desc.line,
      };
      let specifier =
        self.resolve_import(&desc.specifier, Some(location.clone()))?;

      // Parse out any `@deno-types` pragmas and modify dependency
      let maybe_types_specifier = if !desc.leading_comments.is_empty() {
        let comment = desc.leading_comments.last().unwrap();
        if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() {
          Some(self.resolve_import(deno_types, Some(location))?)
        } else {
          None
        }
      } else {
        None
      };

      let dep = self
        .dependencies
        .entry(desc.specifier.to_string())
        .or_default();
      if desc.kind == DependencyKind::ExportType
        || desc.kind == DependencyKind::ImportType
      {
        dep.maybe_type = Some(specifier);
      } else {
        dep.maybe_code = Some(specifier);
      }
      if let Some(types_specifier) = maybe_types_specifier {
        dep.maybe_type = Some(types_specifier);
      }
    }

    self.maybe_parsed_module = Some(parsed_module);
    Ok(())
  }

  fn resolve_import(
    &self,
    specifier: &str,
    maybe_location: Option<Location>,
  ) -> Result<ModuleSpecifier, AnyError> {
    let maybe_resolve = if let Some(import_map) = self.maybe_import_map.clone()
    {
      import_map
        .borrow()
        .resolve(specifier, self.specifier.as_str())?
    } else {
      None
    };
    let specifier = if let Some(module_specifier) = maybe_resolve {
      module_specifier
    } else {
      ModuleSpecifier::resolve_import(specifier, self.specifier.as_str())?
    };

    let referrer_scheme = self.specifier.as_url().scheme();
    let specifier_scheme = specifier.as_url().scheme();
    let location = maybe_location.unwrap_or(Location {
      filename: self.specifier.to_string(),
      line: 0,
      col: 0,
    });

    // Disallow downgrades from HTTPS to HTTP
    if referrer_scheme == "https" && specifier_scheme == "http" {
      return Err(InvalidDowngrade(specifier.clone(), location).into());
    }

    // Disallow a remote URL from trying to import a local URL
    if (referrer_scheme == "https" || referrer_scheme == "http")
      && !(specifier_scheme == "https" || specifier_scheme == "http")
    {
      return Err(InvalidLocalImport(specifier.clone(), location).into());
    }

    Ok(specifier)
  }

  /// Calculate the hashed version of the module and update the `maybe_version`.
  pub fn set_version(&mut self, config: &[u8]) {
    self.maybe_version = Some(get_version(&self.source, version::DENO, config))
  }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Stats(Vec<(String, u128)>);

impl<'de> Deserialize<'de> for Stats {
  fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let items: Vec<(String, u128)> = Deserialize::deserialize(deserializer)?;
    Ok(Stats(items))
  }
}

impl fmt::Display for Stats {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    for (key, value) in self.0.clone() {
      write!(f, "{}: {}", key, value)?;
    }

    Ok(())
  }
}

/// A structure which provides options when transpiling modules.
#[derive(Debug, Default)]
pub struct TranspileOptions {
  /// If `true` then debug logging will be output from the isolate.
  pub debug: bool,
  /// An optional string that points to a user supplied TypeScript configuration
  /// file that augments the the default configuration passed to the TypeScript
  /// compiler.
  pub maybe_config_path: Option<String>,
}

/// A dependency graph of modules, were the modules that have been inserted via
/// the builder will be loaded into the graph.  Also provides an interface to
/// be able to manipulate and handle the graph.

#[derive(Debug)]
pub struct Graph {
  build_info: BuildInfoMap,
  handler: Rc<RefCell<dyn SpecifierHandler>>,
  modules: HashMap<ModuleSpecifier, Module>,
  roots: Vec<ModuleSpecifier>,
}

impl Graph {
  /// Create a new instance of a graph, ready to have modules loaded it.
  ///
  /// The argument `handler` is an instance of a structure that implements the
  /// `SpecifierHandler` trait.
  ///
  pub fn new(handler: Rc<RefCell<dyn SpecifierHandler>>) -> Self {
    Graph {
      build_info: HashMap::new(),
      handler,
      modules: HashMap::new(),
      roots: Vec::new(),
    }
  }

  /// Update the handler with any modules that are marked as _dirty_ and update
  /// any build info if present.
  fn flush(&mut self, emit_type: &EmitType) -> Result<(), AnyError> {
    let mut handler = self.handler.borrow_mut();
    for (_, module) in self.modules.iter_mut() {
      if module.is_dirty {
        let (code, maybe_map) = module.emits.get(emit_type).unwrap();
        handler.set_cache(
          &module.specifier,
          &emit_type,
          code.clone(),
          maybe_map.clone(),
        )?;
        module.is_dirty = false;
        if let Some(version) = &module.maybe_version {
          handler.set_version(&module.specifier, version.clone())?;
        }
      }
    }
    for root_specifier in self.roots.iter() {
      if let Some(build_info) = self.build_info.get(&emit_type) {
        handler.set_build_info(
          root_specifier,
          &emit_type,
          build_info.to_owned(),
        )?;
      }
    }

    Ok(())
  }

  /// Verify the subresource integrity of the graph based upon the optional
  /// lockfile, updating the lockfile with any missing resources.  This will
  /// error if any of the resources do not match their lock status.
  pub fn lock(
    &self,
    maybe_lockfile: &Option<Mutex<Lockfile>>,
  ) -> Result<(), AnyError> {
    if let Some(lf) = maybe_lockfile {
      let mut lockfile = lf.lock().unwrap();
      for (ms, module) in self.modules.iter() {
        let specifier = module.specifier.to_string();
        let code = module.source.to_string()?;
        let valid = lockfile.check_or_insert(&specifier, &code);
        if !valid {
          return Err(
            InvalidSource(ms.clone(), lockfile.filename.clone()).into(),
          );
        }
      }
    }

    Ok(())
  }

  /// Transpile (only transform) the graph, updating any emitted modules
  /// with the specifier handler.  The result contains any performance stats
  /// from the compiler and optionally any user provided configuration compiler
  /// options that were ignored.
  ///
  /// # Arguments
  ///
  /// - `options` - A structure of options which impact how the code is
  ///   transpiled.
  ///
  pub fn transpile(
    &mut self,
    options: TranspileOptions,
  ) -> Result<(Stats, Option<IgnoredCompilerOptions>), AnyError> {
    let start = Instant::now();
    let emit_type = EmitType::Cli;

    let mut ts_config = TsConfig::new(json!({
      "checkJs": false,
      "emitDecoratorMetadata": false,
      "jsx": "react",
      "jsxFactory": "React.createElement",
      "jsxFragmentFactory": "React.Fragment",
    }));

    let maybe_ignored_options =
      ts_config.merge_user_config(options.maybe_config_path)?;

    let compiler_options = ts_config.as_transpile_config()?;
    let check_js = compiler_options.check_js;
    let transform_jsx = compiler_options.jsx == "react";
    let emit_options = ast::TranspileOptions {
      emit_metadata: compiler_options.emit_decorator_metadata,
      inline_source_map: true,
      jsx_factory: compiler_options.jsx_factory,
      jsx_fragment_factory: compiler_options.jsx_fragment_factory,
      transform_jsx,
    };

    let mut emit_count: u128 = 0;
    for (_, module) in self.modules.iter_mut() {
      // TODO(kitsonk) a lot of this logic should be refactored into `Module` as
      // we start to support other methods on the graph.  Especially managing
      // the dirty state is something the module itself should "own".

      // if the module is a Dts file we should skip it
      if module.media_type == MediaType::Dts {
        continue;
      }
      // if we don't have check_js enabled, we won't touch non TypeScript
      // modules
      if !(check_js
        || module.media_type == MediaType::TSX
        || module.media_type == MediaType::TypeScript)
      {
        continue;
      }
      let config = ts_config.as_bytes();
      // skip modules that already have a valid emit
      if module.emits.contains_key(&emit_type) && module.emit_valid(&config) {
        continue;
      }
      if module.maybe_parsed_module.is_none() {
        module.parse()?;
      }
      let parsed_module = module.maybe_parsed_module.clone().unwrap();
      let emit = parsed_module.transpile(&emit_options)?;
      emit_count += 1;
      module.emits.insert(emit_type.clone(), emit);
      module.set_version(&config);
      module.is_dirty = true;
    }
    self.flush(&emit_type)?;

    let stats = Stats(vec![
      ("Files".to_string(), self.modules.len() as u128),
      ("Emitted".to_string(), emit_count),
      ("Total time".to_string(), start.elapsed().as_millis()),
    ]);

    Ok((stats, maybe_ignored_options))
  }
}

impl<'a> ModuleProvider for Graph {
  fn get_source(&self, specifier: &ModuleSpecifier) -> Option<String> {
    if let Some(module) = self.modules.get(specifier) {
      if let Ok(source) = module.source.to_string() {
        Some(source)
      } else {
        None
      }
    } else {
      None
    }
  }

  fn resolve(
    &self,
    specifier: &str,
    referrer: &ModuleSpecifier,
  ) -> Result<(ModuleSpecifier, MediaType), AnyError> {
    if !self.modules.contains_key(referrer) {
      return Err(MissingSpecifier(referrer.to_owned()).into());
    }
    let module = self.modules.get(referrer).unwrap();
    if !module.dependencies.contains_key(specifier) {
      return Err(
        MissingDependency(referrer.to_owned(), specifier.to_owned()).into(),
      );
    }
    let dependency = module.dependencies.get(specifier).unwrap();
    // If there is a @deno-types pragma that impacts the dependency, then the
    // maybe_type property will be set with that specifier, otherwise we use the
    // specifier that point to the runtime code.
    let resolved_specifier =
      if let Some(type_specifier) = dependency.maybe_type.clone() {
        type_specifier
      } else if let Some(code_specifier) = dependency.maybe_code.clone() {
        code_specifier
      } else {
        return Err(
          MissingDependency(referrer.to_owned(), specifier.to_owned()).into(),
        );
      };
    if !self.modules.contains_key(&resolved_specifier) {
      return Err(
        MissingDependency(referrer.to_owned(), resolved_specifier.to_string())
          .into(),
      );
    }
    let dep_module = self.modules.get(&resolved_specifier).unwrap();
    // In the case that there is a X-TypeScript-Types or a triple-slash types,
    // then the `maybe_types` specifier will be populated and we should use that
    // instead.
    let result = if let Some((_, types)) = dep_module.maybe_types.clone() {
      if let Some(types_module) = self.modules.get(&types) {
        (types, types_module.media_type)
      } else {
        return Err(
          MissingDependency(referrer.to_owned(), types.to_string()).into(),
        );
      }
    } else {
      (resolved_specifier, dep_module.media_type)
    };

    Ok(result)
  }
}

/// A structure for building a dependency graph of modules.
pub struct GraphBuilder {
  fetched: HashSet<ModuleSpecifier>,
  graph: Graph,
  maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
  pending: FuturesUnordered<FetchFuture>,
}

impl GraphBuilder {
  pub fn new(
    handler: Rc<RefCell<dyn SpecifierHandler>>,
    maybe_import_map: Option<ImportMap>,
  ) -> Self {
    let internal_import_map = if let Some(import_map) = maybe_import_map {
      Some(Rc::new(RefCell::new(import_map)))
    } else {
      None
    };
    GraphBuilder {
      graph: Graph::new(handler),
      fetched: HashSet::new(),
      maybe_import_map: internal_import_map,
      pending: FuturesUnordered::new(),
    }
  }

  /// Request a module to be fetched from the handler and queue up its future
  /// to be awaited to be resolved.
  fn fetch(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> {
    if self.fetched.contains(&specifier) {
      return Ok(());
    }

    self.fetched.insert(specifier.clone());
    let future = self.graph.handler.borrow_mut().fetch(specifier.clone());
    self.pending.push(future);

    Ok(())
  }

  /// Visit a module that has been fetched, hydrating the module, analyzing its
  /// dependencies if required, fetching those dependencies, and inserting the
  /// module into the graph.
  fn visit(&mut self, cached_module: CachedModule) -> Result<(), AnyError> {
    let specifier = cached_module.specifier.clone();
    let mut module =
      Module::new(specifier.clone(), self.maybe_import_map.clone());
    module.hydrate(cached_module);
    if !module.is_parsed {
      let has_types = module.maybe_types.is_some();
      module.parse()?;
      if self.maybe_import_map.is_none() {
        let mut handler = self.graph.handler.borrow_mut();
        handler.set_deps(&specifier, module.dependencies.clone())?;
        if !has_types {
          if let Some((types, _)) = module.maybe_types.clone() {
            handler.set_types(&specifier, types)?;
          }
        }
      }
    }
    for (_, dep) in module.dependencies.iter() {
      if let Some(specifier) = dep.maybe_code.as_ref() {
        self.fetch(specifier)?;
      }
      if let Some(specifier) = dep.maybe_type.as_ref() {
        self.fetch(specifier)?;
      }
    }
    if let Some((_, specifier)) = module.maybe_types.as_ref() {
      self.fetch(specifier)?;
    }
    self.graph.modules.insert(specifier, module);

    Ok(())
  }

  /// Insert a module into the graph based on a module specifier.  The module
  /// and any dependencies will be fetched from the handler.  The module will
  /// also be treated as a _root_ module in the graph.
  pub async fn insert(
    &mut self,
    specifier: &ModuleSpecifier,
  ) -> Result<(), AnyError> {
    self.fetch(specifier)?;

    loop {
      let cached_module = self.pending.next().await.unwrap()?;
      self.visit(cached_module)?;
      if self.pending.is_empty() {
        break;
      }
    }

    if !self.graph.roots.contains(specifier) {
      self.graph.roots.push(specifier.clone());
    }

    Ok(())
  }

  /// Move out the graph from the builder to be utilized further.  An optional
  /// lockfile can be provided, where if the sources in the graph do not match
  /// the expected lockfile, the method with error instead of returning the
  /// graph.
  pub fn get_graph(
    self,
    maybe_lockfile: &Option<Mutex<Lockfile>>,
  ) -> Result<Graph, AnyError> {
    self.graph.lock(maybe_lockfile)?;
    Ok(self.graph)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  use crate::specifier_handler::tests::MockSpecifierHandler;

  use std::env;
  use std::path::PathBuf;
  use std::sync::Mutex;

  #[test]
  fn test_get_version() {
    let doc_a =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let version_a = get_version(&doc_a, "1.2.3", b"");
    let doc_b =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let version_b = get_version(&doc_b, "1.2.3", b"");
    assert_eq!(version_a, version_b);

    let version_c = get_version(&doc_a, "1.2.3", b"options");
    assert_ne!(version_a, version_c);

    let version_d = get_version(&doc_b, "1.2.3", b"options");
    assert_eq!(version_c, version_d);

    let version_e = get_version(&doc_a, "1.2.4", b"");
    assert_ne!(version_a, version_e);

    let version_f = get_version(&doc_b, "1.2.4", b"");
    assert_eq!(version_e, version_f);
  }

  #[test]
  fn test_module_emit_valid() {
    let source =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let maybe_version = Some(get_version(&source, version::DENO, b""));
    let module = Module {
      source,
      maybe_version,
      ..Module::default()
    };
    assert!(module.emit_valid(b""));

    let source =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let old_source =
      TextDocument::new(b"console.log(43);".to_vec(), Option::<&str>::None);
    let maybe_version = Some(get_version(&old_source, version::DENO, b""));
    let module = Module {
      source,
      maybe_version,
      ..Module::default()
    };
    assert!(!module.emit_valid(b""));

    let source =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let maybe_version = Some(get_version(&source, "0.0.0", b""));
    let module = Module {
      source,
      maybe_version,
      ..Module::default()
    };
    assert!(!module.emit_valid(b""));

    let source =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let module = Module {
      source,
      ..Module::default()
    };
    assert!(!module.emit_valid(b""));
  }

  #[test]
  fn test_module_set_version() {
    let source =
      TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None);
    let expected = Some(get_version(&source, version::DENO, b""));
    let mut module = Module {
      source,
      ..Module::default()
    };
    assert!(module.maybe_version.is_none());
    module.set_version(b"");
    assert_eq!(module.maybe_version, expected);
  }

  #[tokio::test]
  async fn test_graph_builder() {
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures,
      ..MockSpecifierHandler::default()
    }));
    let mut builder = GraphBuilder::new(handler, None);
    let specifier =
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    let graph = builder.get_graph(&None).expect("error getting graph");
    let actual = graph
      .resolve("./a.ts", &specifier)
      .expect("module to resolve");
    let expected = (
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts")
        .expect("unable to resolve"),
      MediaType::TypeScript,
    );
    assert_eq!(actual, expected);
  }

  #[tokio::test]
  async fn test_graph_builder_import_map() {
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures,
      ..MockSpecifierHandler::default()
    }));
    let import_map = ImportMap::from_json(
      "https://deno.land/x/import_map.ts",
      r#"{
      "imports": {
        "jquery": "./jquery.js",
        "lodash": "https://unpkg.com/lodash/index.js"
      }
    }"#,
    )
    .expect("could not load import map");
    let mut builder = GraphBuilder::new(handler, Some(import_map));
    let specifier =
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/import_map.ts")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    let graph = builder.get_graph(&None).expect("could not get graph");
    let actual_jquery = graph
      .resolve("jquery", &specifier)
      .expect("module to resolve");
    let expected_jquery = (
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/jquery.js")
        .expect("unable to resolve"),
      MediaType::JavaScript,
    );
    assert_eq!(actual_jquery, expected_jquery);
    let actual_lodash = graph
      .resolve("lodash", &specifier)
      .expect("module to resolve");
    let expected_lodash = (
      ModuleSpecifier::resolve_url_or_path("https://unpkg.com/lodash/index.js")
        .expect("unable to resolve"),
      MediaType::JavaScript,
    );
    assert_eq!(actual_lodash, expected_lodash);
  }

  #[tokio::test]
  async fn test_graph_transpile() {
    // This is a complex scenario of transpiling, where we have TypeScript
    // importing a JavaScript file (with type definitions) which imports
    // TypeScript, JavaScript, and JavaScript with type definitions.
    // For scenarios where we transpile, we only want the TypeScript files
    // to be actually emitted.
    //
    // This also exercises "@deno-types" and type references.
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures,
      ..MockSpecifierHandler::default()
    }));
    let mut builder = GraphBuilder::new(handler.clone(), None);
    let specifier =
      ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    let mut graph = builder.get_graph(&None).expect("could not get graph");
    let (stats, maybe_ignored_options) =
      graph.transpile(TranspileOptions::default()).unwrap();
    assert_eq!(stats.0.len(), 3);
    assert_eq!(maybe_ignored_options, None);
    let h = handler.borrow();
    assert_eq!(h.cache_calls.len(), 2);
    assert_eq!(h.cache_calls[0].1, EmitType::Cli);
    assert!(h.cache_calls[0]
      .2
      .to_string()
      .unwrap()
      .contains("# sourceMappingURL=data:application/json;base64,"));
    assert_eq!(h.cache_calls[0].3, None);
    assert_eq!(h.cache_calls[1].1, EmitType::Cli);
    assert!(h.cache_calls[1]
      .2
      .to_string()
      .unwrap()
      .contains("# sourceMappingURL=data:application/json;base64,"));
    assert_eq!(h.cache_calls[0].3, None);
    assert_eq!(h.deps_calls.len(), 7);
    assert_eq!(
      h.deps_calls[0].0,
      ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts").unwrap()
    );
    assert_eq!(h.deps_calls[0].1.len(), 1);
    assert_eq!(
      h.deps_calls[1].0,
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.js")
        .unwrap()
    );
    assert_eq!(h.deps_calls[1].1.len(), 3);
    assert_eq!(
      h.deps_calls[2].0,
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.d.ts")
        .unwrap()
    );
    assert_eq!(h.deps_calls[2].1.len(), 3, "should have 3 dependencies");
    // sometimes the calls are not deterministic, and so checking the contents
    // can cause some failures
    assert_eq!(h.deps_calls[3].1.len(), 0, "should have no dependencies");
    assert_eq!(h.deps_calls[4].1.len(), 0, "should have no dependencies");
    assert_eq!(h.deps_calls[5].1.len(), 0, "should have no dependencies");
    assert_eq!(h.deps_calls[6].1.len(), 0, "should have no dependencies");
  }

  #[tokio::test]
  async fn test_graph_transpile_user_config() {
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures: fixtures.clone(),
      ..MockSpecifierHandler::default()
    }));
    let mut builder = GraphBuilder::new(handler.clone(), None);
    let specifier =
      ModuleSpecifier::resolve_url_or_path("https://deno.land/x/transpile.tsx")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    let mut graph = builder.get_graph(&None).expect("could not get graph");
    let (_, maybe_ignored_options) = graph
      .transpile(TranspileOptions {
        debug: false,
        maybe_config_path: Some("tests/module_graph/tsconfig.json".to_string()),
      })
      .unwrap();
    assert_eq!(
      maybe_ignored_options.unwrap().items,
      vec!["target".to_string()],
      "the 'target' options should have been ignored"
    );
    let h = handler.borrow();
    assert_eq!(h.cache_calls.len(), 1, "only one file should be emitted");
    assert!(
      h.cache_calls[0]
        .2
        .to_string()
        .unwrap()
        .contains("<div>Hello world!</div>"),
      "jsx should have been preserved"
    );
  }

  #[tokio::test]
  async fn test_graph_with_lockfile() {
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let lockfile_path = fixtures.join("lockfile.json");
    let lockfile =
      Lockfile::new(lockfile_path.to_string_lossy().to_string(), false)
        .expect("could not load lockfile");
    let maybe_lockfile = Some(Mutex::new(lockfile));
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures,
      ..MockSpecifierHandler::default()
    }));
    let mut builder = GraphBuilder::new(handler.clone(), None);
    let specifier =
      ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    builder
      .get_graph(&maybe_lockfile)
      .expect("could not get graph");
  }

  #[tokio::test]
  async fn test_graph_with_lockfile_fail() {
    let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let fixtures = c.join("tests/module_graph");
    let lockfile_path = fixtures.join("lockfile_fail.json");
    let lockfile =
      Lockfile::new(lockfile_path.to_string_lossy().to_string(), false)
        .expect("could not load lockfile");
    let maybe_lockfile = Some(Mutex::new(lockfile));
    let handler = Rc::new(RefCell::new(MockSpecifierHandler {
      fixtures,
      ..MockSpecifierHandler::default()
    }));
    let mut builder = GraphBuilder::new(handler.clone(), None);
    let specifier =
      ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
        .expect("could not resolve module");
    builder
      .insert(&specifier)
      .await
      .expect("module not inserted");
    builder
      .get_graph(&maybe_lockfile)
      .expect_err("expected an error");
  }
}