use crate::normalize_path;
use std::error::Error;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use url::ParseError;
use url::Url;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ModuleResolutionError {
  InvalidUrl(ParseError),
  InvalidBaseUrl(ParseError),
  InvalidPath(PathBuf),
  ImportPrefixMissing(String, Option<String>),
}
use ModuleResolutionError::*;
impl Error for ModuleResolutionError {
  fn source(&self) -> Option<&(dyn Error + 'static)> {
    match self {
      InvalidUrl(ref err) | InvalidBaseUrl(ref err) => Some(err),
      _ => None,
    }
  }
}
impl fmt::Display for ModuleResolutionError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      InvalidUrl(ref err) => write!(f, "invalid URL: {err}"),
      InvalidBaseUrl(ref err) => {
        write!(f, "invalid base URL for relative import: {err}")
      }
      InvalidPath(ref path) => write!(f, "invalid module path: {path:?}"),
      ImportPrefixMissing(ref specifier, ref maybe_referrer) => write!(
        f,
        "Relative import path \"{}\" not prefixed with / or ./ or ../{}",
        specifier,
        match maybe_referrer {
          Some(referrer) => format!(" from \"{referrer}\""),
          None => String::new(),
        }
      ),
    }
  }
}
pub type ModuleSpecifier = Url;
pub fn resolve_import(
  specifier: &str,
  base: &str,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
  let url = match Url::parse(specifier) {
            Ok(url) => url,
                    Err(ParseError::RelativeUrlWithoutBase)
      if !(specifier.starts_with('/')
        || specifier.starts_with("./")
        || specifier.starts_with("../")) =>
    {
      let maybe_referrer = if base.is_empty() {
        None
      } else {
        Some(base.to_string())
      };
      return Err(ImportPrefixMissing(specifier.to_string(), maybe_referrer));
    }
            Err(ParseError::RelativeUrlWithoutBase) => {
      let base = Url::parse(base).map_err(InvalidBaseUrl)?;
      base.join(specifier).map_err(InvalidUrl)?
    }
                    Err(err) => return Err(InvalidUrl(err)),
  };
  Ok(url)
}
pub fn resolve_url(
  url_str: &str,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
  Url::parse(url_str).map_err(ModuleResolutionError::InvalidUrl)
}
pub fn resolve_url_or_path(
  specifier: &str,
  current_dir: &Path,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
  if specifier_has_uri_scheme(specifier) {
    resolve_url(specifier)
  } else {
    resolve_path(specifier, current_dir)
  }
}
pub fn resolve_path(
  path_str: impl AsRef<Path>,
  current_dir: &Path,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
  let path = current_dir.join(path_str);
  let path = normalize_path(path);
  Url::from_file_path(&path)
    .map_err(|()| ModuleResolutionError::InvalidPath(path))
}
pub fn specifier_has_uri_scheme(specifier: &str) -> bool {
  let mut chars = specifier.chars();
  let mut len = 0usize;
    match chars.next() {
    Some(c) if c.is_ascii_alphabetic() => len += 1,
    _ => return false,
  }
      loop {
    match chars.next() {
      Some(c) if c.is_ascii_alphanumeric() || "+-.".contains(c) => len += 1,
      Some(':') if len >= 2 => return true,
      _ => return false,
    }
  }
}
#[cfg(test)]
mod tests {
  use super::*;
  use crate::serde_json::from_value;
  use crate::serde_json::json;
  use std::env::current_dir;
  use std::path::Path;
  #[test]
  fn test_resolve_import() {
    let tests = vec![
      (
        "./005_more_imports.ts",
        "http://deno.land/core/tests/006_url_imports.ts",
        "http://deno.land/core/tests/005_more_imports.ts",
      ),
      (
        "../005_more_imports.ts",
        "http://deno.land/core/tests/006_url_imports.ts",
        "http://deno.land/core/005_more_imports.ts",
      ),
      (
        "http://deno.land/core/tests/005_more_imports.ts",
        "http://deno.land/core/tests/006_url_imports.ts",
        "http://deno.land/core/tests/005_more_imports.ts",
      ),
      (
        "data:text/javascript,export default 'grapes';",
        "http://deno.land/core/tests/006_url_imports.ts",
        "data:text/javascript,export default 'grapes';",
      ),
      (
        "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f",
        "http://deno.land/core/tests/006_url_imports.ts",
        "blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f",
      ),
      (
        "javascript:export default 'artichokes';",
        "http://deno.land/core/tests/006_url_imports.ts",
        "javascript:export default 'artichokes';",
      ),
      (
        "data:text/plain,export default 'kale';",
        "http://deno.land/core/tests/006_url_imports.ts",
        "data:text/plain,export default 'kale';",
      ),
      (
        "/dev/core/tests/005_more_imports.ts",
        "file:///home/yeti",
        "file:///dev/core/tests/005_more_imports.ts",
      ),
      (
        "//zombo.com/1999.ts",
        "https://cherry.dev/its/a/thing",
        "https://zombo.com/1999.ts",
      ),
      (
        "http://deno.land/this/url/is/valid",
        "base is clearly not a valid url",
        "http://deno.land/this/url/is/valid",
      ),
      (
        "//server/some/dir/file",
        "file:///home/yeti/deno",
        "file://server/some/dir/file",
      ),
                                              ];
    for (specifier, base, expected_url) in tests {
      let url = resolve_import(specifier, base).unwrap().to_string();
      assert_eq!(url, expected_url);
    }
  }
  #[test]
  fn test_resolve_import_error() {
    use url::ParseError::*;
    use ModuleResolutionError::*;
    let tests = vec![
      (
        "awesome.ts",
        "<unknown>",
        ImportPrefixMissing(
          "awesome.ts".to_string(),
          Some("<unknown>".to_string()),
        ),
      ),
      (
        "005_more_imports.ts",
        "http://deno.land/core/tests/006_url_imports.ts",
        ImportPrefixMissing(
          "005_more_imports.ts".to_string(),
          Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
        ),
      ),
      (
        ".tomato",
        "http://deno.land/core/tests/006_url_imports.ts",
        ImportPrefixMissing(
          ".tomato".to_string(),
          Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
        ),
      ),
      (
        "..zucchini.mjs",
        "http://deno.land/core/tests/006_url_imports.ts",
        ImportPrefixMissing(
          "..zucchini.mjs".to_string(),
          Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
        ),
      ),
      (
        r".\yam.es",
        "http://deno.land/core/tests/006_url_imports.ts",
        ImportPrefixMissing(
          r".\yam.es".to_string(),
          Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
        ),
      ),
      (
        r"..\yam.es",
        "http://deno.land/core/tests/006_url_imports.ts",
        ImportPrefixMissing(
          r"..\yam.es".to_string(),
          Some("http://deno.land/core/tests/006_url_imports.ts".to_string()),
        ),
      ),
      (
        "https://eggplant:b/c",
        "http://deno.land/core/tests/006_url_imports.ts",
        InvalidUrl(InvalidPort),
      ),
      (
        "https://eggplant@/c",
        "http://deno.land/core/tests/006_url_imports.ts",
        InvalidUrl(EmptyHost),
      ),
      (
        "./foo.ts",
        "/relative/base/url",
        InvalidBaseUrl(RelativeUrlWithoutBase),
      ),
    ];
    for (specifier, base, expected_err) in tests {
      let err = resolve_import(specifier, base).unwrap_err();
      assert_eq!(err, expected_err);
    }
  }
  #[test]
  fn test_resolve_url_or_path() {
        let mut tests: Vec<(&str, String)> = vec![
      (
        "http://deno.land/core/tests/006_url_imports.ts",
        "http://deno.land/core/tests/006_url_imports.ts".to_string(),
      ),
      (
        "https://deno.land/core/tests/006_url_imports.ts",
        "https://deno.land/core/tests/006_url_imports.ts".to_string(),
      ),
    ];
            let cwd = if cfg!(miri) {
      PathBuf::from("/miri")
    } else {
      current_dir().unwrap()
    };
    let cwd_str = cwd.to_str().unwrap();
    if cfg!(target_os = "windows") {
            let expected_url = "file:///C:/deno/tests/006_url_imports.ts";
      tests.extend(vec![
        (
          r"C:/deno/tests/006_url_imports.ts",
          expected_url.to_string(),
        ),
        (
          r"C:\deno\tests\006_url_imports.ts",
          expected_url.to_string(),
        ),
        (
          r"\\?\C:\deno\tests\006_url_imports.ts",
          expected_url.to_string(),
        ),
                                      ]);
            let expected_url = format!(
        "file:///{}:/deno/tests/006_url_imports.ts",
        cwd_str.get(..1).unwrap(),
      );
      tests.extend(vec![
        (r"/deno/tests/006_url_imports.ts", expected_url.to_string()),
        (r"\deno\tests\006_url_imports.ts", expected_url.to_string()),
        (
          r"\deno\..\deno\tests\006_url_imports.ts",
          expected_url.to_string(),
        ),
        (r"\deno\.\tests\006_url_imports.ts", expected_url),
      ]);
            let expected_url = format!(
        "file:///{}/tests/006_url_imports.ts",
        cwd_str.replace('\\', "/")
      );
      tests.extend(vec![
        (r"tests/006_url_imports.ts", expected_url.to_string()),
        (r"tests\006_url_imports.ts", expected_url.to_string()),
        (r"./tests/006_url_imports.ts", (*expected_url).to_string()),
        (r".\tests\006_url_imports.ts", (*expected_url).to_string()),
      ]);
            let expected_url = "file://server/share/deno/cool";
      tests.extend(vec![
        (r"\\server\share\deno\cool", expected_url.to_string()),
        (r"\\server/share/deno/cool", expected_url.to_string()),
                      ]);
    } else {
            let expected_url = "file:///deno/tests/006_url_imports.ts";
      tests.extend(vec![
        ("/deno/tests/006_url_imports.ts", expected_url.to_string()),
        ("//deno/tests/006_url_imports.ts", expected_url.to_string()),
      ]);
            let expected_url = format!("file://{cwd_str}/tests/006_url_imports.ts");
      tests.extend(vec![
        ("tests/006_url_imports.ts", expected_url.to_string()),
        ("./tests/006_url_imports.ts", expected_url.to_string()),
        (
          "tests/../tests/006_url_imports.ts",
          expected_url.to_string(),
        ),
        ("tests/./006_url_imports.ts", expected_url),
      ]);
    }
    for (specifier, expected_url) in tests {
      let url = resolve_url_or_path(specifier, &cwd).unwrap().to_string();
      assert_eq!(url, expected_url);
    }
  }
  #[test]
  fn test_resolve_url_or_path_deprecated_error() {
    use url::ParseError::*;
    use ModuleResolutionError::*;
    let mut tests = vec![
      ("https://eggplant:b/c", InvalidUrl(InvalidPort)),
      ("https://:8080/a/b/c", InvalidUrl(EmptyHost)),
    ];
    if cfg!(target_os = "windows") {
      let p = r"\\.\c:/stuff/deno/script.ts";
      tests.push((p, InvalidPath(PathBuf::from(p))));
    }
    for (specifier, expected_err) in tests {
      let err =
        resolve_url_or_path(specifier, &PathBuf::from("/")).unwrap_err();
      assert_eq!(err, expected_err);
    }
  }
  #[test]
  fn test_specifier_has_uri_scheme() {
    let tests = vec![
      ("http://foo.bar/etc", true),
      ("HTTP://foo.bar/etc", true),
      ("http:ftp:", true),
      ("http:", true),
      ("hTtP:", true),
      ("ftp:", true),
      ("mailto:spam@please.me", true),
      ("git+ssh://git@github.com/denoland/deno", true),
      ("blob:https://whatwg.org/mumbojumbo", true),
      ("abc.123+DEF-ghi:", true),
      ("abc.123+def-ghi:@", true),
      ("", false),
      (":not", false),
      ("http", false),
      ("c:dir", false),
      ("X:", false),
      ("./http://not", false),
      ("1abc://kinda/but/no", false),
      ("schluẞ://no/more", false),
    ];
    for (specifier, expected) in tests {
      let result = specifier_has_uri_scheme(specifier);
      assert_eq!(result, expected);
    }
  }
  #[test]
  fn test_normalize_path() {
    assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b"));
    assert_eq!(normalize_path(Path::new("a/./b/")), PathBuf::from("a/b/"));
    assert_eq!(
      normalize_path(Path::new("a/./b/../c")),
      PathBuf::from("a/c")
    );
    if cfg!(windows) {
      assert_eq!(
        normalize_path(Path::new("C:\\a\\.\\b\\..\\c")),
        PathBuf::from("C:\\a\\c")
      );
    }
  }
  #[test]
  fn test_deserialize_module_specifier() {
    let actual: ModuleSpecifier =
      from_value(json!("http://deno.land/x/mod.ts")).unwrap();
    let expected = resolve_url("http://deno.land/x/mod.ts").unwrap();
    assert_eq!(actual, expected);
  }
}