use crate::with_path_segments;
use lazy_regex::regex;
use log::{debug, warn};
use std::borrow::Cow;

use std::collections::HashMap;
use url::Url;

/// Known VCSes
pub const VCSES: &[&str] = &["git", "bzr", "hg"];

/// Hostnames for known gitlab sites
pub const KNOWN_GITLAB_SITES: &[&str] = &["salsa.debian.org", "invent.kde.org", "0xacab.org"];

/// URL schemes that are considered secure
pub const SECURE_SCHEMES: &[&str] = &["https", "git+ssh", "bzr+ssh", "hg+ssh", "ssh", "svn+ssh"];

/// Known hosting sites
const KNOWN_HOSTING_SITES: &[&str] = &[
    "code.launchpad.net",
    "github.com",
    "launchpad.net",
    "git.openstack.org",
];

/// Check if a string can plausibly be a URL
pub fn plausible_url(url: &str) -> bool {
    url.contains(':')
}

/// Drop VCS prefixes from a URL
///
/// This function removes the VCS prefix from a URL, if it exists. For example,
/// it will convert `git+http://example.com/repo.git` to `http://example.com/repo.git`.
pub fn drop_vcs_in_scheme(url: &Url) -> Option<Url> {
    let scheme = url.scheme();
    match scheme {
        "git+http" | "git+https" => {
            Some(derive_with_scheme(url, scheme.trim_start_matches("git+")))
        }
        "hg+http" | "hg+https" => Some(derive_with_scheme(url, scheme.trim_start_matches("hg+"))),
        "bzr+lp" | "bzr+http" => Some(derive_with_scheme(url, scheme.trim_start_matches("bzr+"))),
        _ => None,
    }
}

/// Split a VCS URL into its components
pub fn split_vcs_url(location: &str) -> (String, Option<String>, Option<String>) {
    let mut url = location;
    let mut branch = None;
    let mut subpath = None;

    // Check for subpath in brackets
    if let Some(idx) = url.find('[') {
        if let Some(idx2) = url.find(']') {
            subpath = Some(url[idx + 1..idx2].to_string());
            url = &url[..idx];
        }
    }

    // Check for branch specification
    if let Some(idx) = url.find(" -b ") {
        branch = Some(url[idx + 4..].to_string());
        url = &url[..idx];
    }

    (url.to_string(), branch, subpath)
}

/// Unsplit a VCS URL
pub fn unsplit_vcs_url(location: &VcsLocation) -> String {
    let mut url = location.url.to_string();
    if let Some(branch_name) = location.branch.as_deref() {
        url = format!("{} -b {}", url, branch_name);
    }
    if let Some(subpath_str) = location.subpath.as_deref() {
        url = format!("{} [{}]", url, subpath_str);
    }
    url
}

/// Check if a URL is plausibly VCS browse URL
pub fn plausible_browse_url(url: &str) -> bool {
    if let Ok(url) = url::Url::parse(url) {
        if url.scheme() == "https" || url.scheme() == "http" {
            return true;
        }
    }
    false
}

/// Strip VCS prefixes from a URL
///
/// Note: This function is not a complete URL parser and should not be used for
/// general URL parsing. Consider using `drop_vcs_in_scheme`
/// instead.
pub fn strip_vcs_prefixes(url: &str) -> &str {
    let prefixes = ["git", "hg"];

    for prefix in prefixes.iter() {
        if url.starts_with(&format!("{}+", prefix)) {
            return &url[prefix.len() + 1..];
        }
    }

    url
}

/// Check if the specified GitHub URL is a valid repository
///
/// If `version` is specified, it checks if the version is available in the
/// repository.
async fn probe_upstream_github_branch_url(url: &url::Url, version: Option<&str>) -> Option<bool> {
    let path = url.path();
    let path = path.strip_suffix(".git").unwrap_or(path);
    let api_path = format!("repos/{}/tags", path.trim_start_matches('/'));
    match crate::github::load_github_json(&api_path).await {
        Ok(json) => {
            if let Some(version) = version {
                let tags = json.as_array()?;
                let tag_names = tags
                    .iter()
                    .map(|x| x["name"].as_str().unwrap())
                    .collect::<Vec<_>>();
                if tag_names.is_empty() {
                    // Uhm, hmm
                    return Some(true);
                }
                return Some(version_in_tags(version, tag_names.as_slice()));
            }
            Some(true)
        }
        Err(crate::HTTPJSONError::Error { status: 404, .. }) => Some(false),
        Err(crate::HTTPJSONError::Error { status: 403, .. }) => {
            debug!("github api rate limit exceeded");
            None
        }
        Err(e) => {
            warn!("failed to probe github api: {:?}", e);
            None
        }
    }
}

fn version_in_tags(version: &str, tag_names: &[&str]) -> bool {
    if tag_names.contains(&version) {
        return true;
    }
    if tag_names.contains(&format!("v{}", version).as_str()) {
        return true;
    }
    if tag_names.contains(&format!("release/{}", version).as_str()) {
        return true;
    }
    if tag_names.contains(&version.replace('.', "_").as_str()) {
        return true;
    }
    for tag_name in tag_names {
        if tag_name.ends_with(&format!("_{}", version)) {
            return true;
        }
        if tag_name.ends_with(&format!("-{}", version)) {
            return true;
        }
        if tag_name.ends_with(&format!("_{}", version.replace('.', "_"))) {
            return true;
        }
    }
    false
}

#[cfg(feature = "breezy")]
fn probe_upstream_breezy_branch_url(url: &url::Url, version: Option<&str>) -> Option<bool> {
    if let Err(e) = breezyshim::try_init() {
        debug!("breezy not available, skipping branch probe: {}", e);
        return None;
    }
    let tags: HashMap<String, breezyshim::RevisionId> = breezyshim::ui::with_silent_ui_factory(
        || -> Result<HashMap<String, breezyshim::RevisionId>, breezyshim::error::Error> {
            use breezyshim::branch::Branch;
            let branch = breezyshim::branch::open_as_generic(url)?;
            branch.tags()?.get_tag_dict()
        },
    )
    .map_err(|e| {
        warn!("failed to probe breezy branch: {:?}", e);
        e
    })
    .ok()?;

    let tag_names = tags.keys().map(|x| x.as_str()).collect::<Vec<_>>();
    if let Some(version) = version {
        Some(version_in_tags(version, tag_names.as_slice()))
    } else {
        Some(true)
    }
}

/// Probe an upstream branch URL for a repository
///
/// This function checks if the specified URL is a valid repository URL
/// and if the specified version is available in the repository.
///
/// # Returns
/// Some(true) if the URL is valid and the version is available,
/// Some(false) if the URL is valid but the version is not available,
/// None if the URL is not valid or if the probe failed.
pub async fn probe_upstream_branch_url(url: &url::Url, version: Option<&str>) -> Option<bool> {
    if url.scheme() == "git+ssh" || url.scheme() == "ssh" || url.scheme() == "bzr+ssh" {
        // Let's not probe anything possibly non-public.
        return None;
    }

    if url.host() == Some(url::Host::Domain("github.com")) {
        probe_upstream_github_branch_url(url, version).await
    } else {
        #[cfg(feature = "breezy")]
        {
            return probe_upstream_breezy_branch_url(url, version);
        }
        #[cfg(not(feature = "breezy"))]
        {
            None
        }
    }
}

/// Check if a repository URL is canonical
pub async fn check_repository_url_canonical(
    mut url: url::Url,
    version: Option<&str>,
) -> std::result::Result<url::Url, crate::CanonicalizeError> {
    if url.host_str() == Some("github.com") {
        let mut segments = url.path_segments().unwrap().collect::<Vec<_>>();
        if segments.len() < 2 {
            return Err(crate::CanonicalizeError::InvalidUrl(
                url,
                "GitHub URL with less than 2 path elements".to_string(),
            ));
        }

        if segments[0] == "sponsors" {
            return Err(crate::CanonicalizeError::InvalidUrl(
                url,
                "GitHub sponsors URL".to_string(),
            ));
        }

        segments[1] = segments[1].trim_end_matches(".git");
        let api_path = format!("repos/{}/{}", segments[0], segments[1]);
        url = match crate::github::load_github_json(&api_path).await {
            Ok(data) => {
                if data["archived"].as_bool().unwrap_or(false) {
                    return Err(crate::CanonicalizeError::InvalidUrl(
                        url,
                        "GitHub URL is archived".to_string(),
                    ));
                }

                if let Some(description) = data["description"].as_str() {
                    if description.contains("DEPRECATED") {
                        return Err(crate::CanonicalizeError::InvalidUrl(
                            url,
                            "GitHub URL is deprecated".to_string(),
                        ));
                    }

                    if description.starts_with("Moved to") {
                        let url = url::Url::parse(
                            description
                                .trim_start_matches("Moved to ")
                                .trim_end_matches('.'),
                        )
                        .unwrap();
                        return Box::pin(check_repository_url_canonical(url, version)).await;
                    }

                    if description.contains("has moved") {
                        return Err(crate::CanonicalizeError::InvalidUrl(
                            url,
                            "GitHub URL has moved".to_string(),
                        ));
                    }

                    if description.starts_with("Mirror of ") {
                        let url = url::Url::parse(
                            description
                                .trim_start_matches("Mirror of ")
                                .trim_end_matches('.'),
                        )
                        .unwrap();
                        return Box::pin(check_repository_url_canonical(url, version)).await;
                    }
                }

                if let Some(homepage) = data["homepage"].as_str() {
                    if is_gitlab_site(homepage, None).await {
                        return Err(crate::CanonicalizeError::InvalidUrl(
                            url,
                            format!("homepage is on GitLab: {}", homepage),
                        ));
                    }
                }

                // TODO(jelmer): Look at the contents of the repository; if it contains just a
                // single README file with < 10 lines, assume the worst.
                // return data['clone_url']

                Ok(url::Url::parse(data["clone_url"].as_str().unwrap()).unwrap())
            }
            Err(crate::HTTPJSONError::Error { status: 404, .. }) => {
                return Err(crate::CanonicalizeError::InvalidUrl(
                    url,
                    "GitHub URL does not exist".to_string(),
                ))
            }
            Err(crate::HTTPJSONError::Error { status: 403, .. }) => {
                return Err(crate::CanonicalizeError::Unverifiable(
                    url,
                    "GitHub URL rate-limited".to_string(),
                ))
            }
            Err(e) => {
                return Err(crate::CanonicalizeError::Unverifiable(
                    url,
                    format!("GitHub URL failed to load: {:?}", e),
                ))
            }
        }?;
    }

    let is_valid = probe_upstream_branch_url(&url, version).await;
    if is_valid.is_none() {
        return Err(crate::CanonicalizeError::Unverifiable(
            url,
            "unable to probe".to_string(),
        ));
    }

    if is_valid.unwrap() {
        return Ok(url);
    }

    Err(crate::CanonicalizeError::InvalidUrl(
        url,
        "unable to successfully probe URL".to_string(),
    ))
}

/// Check if the specified hostname contains a GitLab site
pub async fn is_gitlab_site(hostname: &str, net_access: Option<bool>) -> bool {
    if KNOWN_GITLAB_SITES.contains(&hostname) {
        return true;
    }

    if hostname.starts_with("gitlab.") {
        return true;
    }

    if net_access.unwrap_or(false) {
        probe_gitlab_host(hostname).await
    } else {
        false
    }
}

/// Probe a GitLab host to check if it is a valid GitLab instance
pub async fn probe_gitlab_host(hostname: &str) -> bool {
    let url = format!("https://{}/api/v4/version", hostname);
    match crate::load_json_url(&url::Url::parse(url.as_str()).unwrap(), None).await {
        Ok(_data) => true,
        Err(crate::HTTPJSONError::Error {
            status: 401,
            response,
            ..
        }) => {
            if let Ok(data) = response.json::<serde_json::Value>().await {
                if let Some(message) = data["message"].as_str() {
                    if message == "401 Unauthorized" {
                        true
                    } else {
                        debug!("failed to parse JSON response: {:?}", data);
                        false
                    }
                } else {
                    debug!("failed to parse JSON response: {:?}", data);
                    false
                }
            } else {
                debug!("failed to parse JSON response");
                false
            }
        }
        Err(e) => {
            debug!("failed to probe GitLab host: {:?}", e);
            false
        }
    }
}

/// Guess a repository URL from a URL
pub async fn guess_repo_from_url(url: &url::Url, net_access: Option<bool>) -> Option<String> {
    let net_access = net_access.unwrap_or(false);
    let path_segments = url.path_segments().unwrap().collect::<Vec<_>>();
    match url.host_str()? {
        "github.com" => {
            if path_segments.len() < 2 {
                return None;
            }

            Some(
                with_path_segments(url, &path_segments[0..2])
                    .unwrap()
                    .to_string(),
            )
        }
        "travis-ci.org" => {
            if path_segments.len() < 2 {
                return None;
            }

            Some(format!(
                "https://github.com/{}/{}",
                path_segments[0], path_segments[1]
            ))
        }
        "coveralls.io" => {
            if path_segments.len() < 3 {
                return None;
            }
            if path_segments[0] != "r" {
                return None;
            }
            Some(format!(
                "https://github.com/{}/{}",
                path_segments[1], path_segments[2]
            ))
        }
        "launchpad.net" => Some(
            url::Url::parse(format!("https://code.launchpad.net/{}", path_segments[0]).as_str())
                .unwrap()
                .to_string(),
        ),
        "git.savannah.gnu.org" => {
            if path_segments.len() < 2 {
                return None;
            }
            if path_segments[0] != "git" {
                return None;
            }
            Some(url.to_string())
        }
        "freedesktop.org" | "www.freedesktop.org" => {
            if path_segments.len() >= 2 && path_segments[0] == "software" {
                Some(
                    url::Url::parse(
                        format!("https://github.com/freedesktop/{}", path_segments[1]).as_str(),
                    )
                    .unwrap()
                    .to_string(),
                )
            } else if path_segments.len() >= 3 && path_segments[0..2] == ["wiki", "Software"] {
                Some(
                    url::Url::parse(
                        format!("https://github.com/freedesktop/{}", path_segments[2]).as_str(),
                    )
                    .unwrap()
                    .to_string(),
                )
            } else {
                None
            }
        }
        "download.gnome.org" => {
            if path_segments.len() < 2 {
                return None;
            }
            if path_segments[0] != "sources" {
                return None;
            }
            Some(
                url::Url::parse(
                    format!("https://gitlab.gnome.org/GNOME/{}.git", path_segments[1]).as_str(),
                )
                .unwrap()
                .to_string(),
            )
        }
        "download.kde.org" => {
            if path_segments.len() < 2 {
                return None;
            }
            if path_segments[0] != "stable" && path_segments[0] != "unstable" {
                return None;
            }
            Some(
                url::Url::parse(format!("https://invent.kde.org/{}", path_segments[1]).as_str())
                    .unwrap()
                    .to_string(),
            )
        }
        "ftp.gnome.org" => {
            if path_segments.len() >= 4
                && path_segments[0] == "pub"
                && path_segments[1] == "GNOME"
                && path_segments[2] == "sources"
            {
                Some(
                    url::Url::parse(
                        format!("https://gitlab.gnome.org/GNOME/{}.git", path_segments[3]).as_str(),
                    )
                    .unwrap()
                    .to_string(),
                )
            } else {
                None
            }
        }
        "sourceforge.net" => {
            if path_segments.len() >= 4 && path_segments[0] == "p" && path_segments[3] == "ci" {
                Some(
                    url::Url::parse(
                        format!(
                            "https://sourceforge.net/p/{}/{}",
                            path_segments[1], path_segments[2]
                        )
                        .as_str(),
                    )
                    .unwrap()
                    .to_string(),
                )
            } else {
                None
            }
        }
        "www.apache.org" => {
            if path_segments.len() >= 2 && path_segments[0] == "dist" {
                Some(
                    url::Url::parse(
                        format!("https://svn.apache.org/repos/asf/{}", path_segments[1]).as_str(),
                    )
                    .unwrap()
                    .to_string(),
                )
            } else {
                None
            }
        }
        "bitbucket.org" => {
            if path_segments.len() < 2 {
                return None;
            }

            Some(
                with_path_segments(url, &path_segments[0..2])
                    .unwrap()
                    .to_string(),
            )
        }
        "ftp.gnu.org" => {
            if path_segments.len() < 2 {
                return None;
            }
            if path_segments[0] != "gnu" {
                return None;
            }
            Some(
                url::Url::parse(
                    format!("https://git.savannah.gnu.org/git/{}", path_segments[1]).as_str(),
                )
                .unwrap()
                .to_string(),
            )
        }
        "download.savannah.gnu.org" => {
            if path_segments.len() < 2 {
                return None;
            }
            if path_segments[0] != "releases" {
                return None;
            }
            Some(
                url::Url::parse(
                    format!("https://git.savannah.gnu.org/git/{}", path_segments[1]).as_str(),
                )
                .unwrap()
                .to_string(),
            )
        }
        u if is_gitlab_site(u, Some(net_access)).await => {
            if path_segments.is_empty() {
                return None;
            }
            let proj_segments = if path_segments.contains(&"-") {
                path_segments[0..path_segments.iter().position(|s| s.contains('-')).unwrap()]
                    .to_vec()
            } else if path_segments.contains(&"tags") {
                path_segments[0..path_segments.iter().position(|s| s == &"tags").unwrap()].to_vec()
            } else if path_segments.contains(&"blob") {
                path_segments[0..path_segments.iter().position(|s| s == &"blob").unwrap()].to_vec()
            } else {
                path_segments.to_vec()
            };

            Some(with_path_segments(url, &proj_segments).unwrap().to_string())
        }
        "git.php.net" => {
            if path_segments[0] == "repository" {
                Some(url.to_string())
            } else if path_segments.is_empty() {
                let qs = url.query_pairs().collect::<HashMap<_, _>>();
                qs.get("p")
                    .map(|p| {
                        url::Url::parse(format!("https://git.php.net/repository/?{}", p).as_str())
                            .unwrap()
                    })
                    .map(|u| u.to_string())
            } else {
                None
            }
        }
        u if KNOWN_HOSTING_SITES.contains(&u) => Some(url.to_string()),
        u if u.starts_with("svn.") => {
            // 'svn' subdomains are often used for hosting SVN repositories
            Some(url.to_string())
        }
        _ => {
            if net_access {
                match check_repository_url_canonical(url.clone(), None).await {
                    Ok(url) => Some(url.to_string()),
                    Err(_) => {
                        debug!("Failed to canonicalize URL: {}", url);
                        None
                    }
                }
            } else {
                None
            }
        }
    }
}

#[tokio::test]
async fn test_guess_repo_url() {
    assert_eq!(
        Some("https://github.com/jelmer/blah".to_string()),
        guess_repo_from_url(
            &"https://github.com/jelmer/blah".parse().unwrap(),
            Some(false)
        )
        .await
    );

    assert_eq!(
        Some("https://github.com/jelmer/blah".to_string()),
        guess_repo_from_url(
            &"https://github.com/jelmer/blah/blob/README"
                .parse()
                .unwrap(),
            Some(false)
        )
        .await
    );
    assert_eq!(
        None,
        guess_repo_from_url(&"https://github.com/jelmer".parse().unwrap(), Some(false)).await
    );

    assert_eq!(
        None,
        guess_repo_from_url(&"https://www.jelmer.uk/".parse().unwrap(), Some(false)).await
    );

    assert_eq!(
        Some("http://code.launchpad.net/blah".to_string()),
        guess_repo_from_url(
            &"http://code.launchpad.net/blah".parse().unwrap(),
            Some(false)
        )
        .await,
    );

    assert_eq!(
        Some("https://code.launchpad.net/bzr".to_string()),
        guess_repo_from_url(
            &"http://launchpad.net/bzr/+download".parse().unwrap(),
            Some(false)
        )
        .await,
    );

    assert_eq!(
        Some("https://git.savannah.gnu.org/git/auctex.git".to_string()),
        guess_repo_from_url(
            &"https://git.savannah.gnu.org/git/auctex.git"
                .parse()
                .unwrap(),
            Some(false)
        )
        .await,
    );

    assert_eq!(
        None,
        guess_repo_from_url(
            &"https://git.savannah.gnu.org/blah/auctex.git"
                .parse()
                .unwrap(),
            Some(false)
        )
        .await,
    );

    assert_eq!(
        Some("https://bitbucket.org/fenics-project/dolfin".to_string()),
        guess_repo_from_url(
            &"https://bitbucket.org/fenics-project/dolfin/downloads/"
                .parse()
                .unwrap(),
            Some(false)
        )
        .await,
    );
}

/// Canonicalize a Git repository URL
pub async fn canonical_git_repo_url(repo_url: &Url, net_access: Option<bool>) -> Option<Url> {
    if let Some(hostname) = repo_url.host_str() {
        if (is_gitlab_site(hostname, net_access).await || hostname == "github.com")
            && !repo_url.path().ends_with(".git")
        {
            let mut url = repo_url.clone();
            url.set_path(&(url.path().to_owned() + ".git"));
            return Some(url);
        }
    }
    None
}

/// Determine the browse URL from a repo URL
pub async fn browse_url_from_repo_url(
    location: &VcsLocation,
    net_access: Option<bool>,
) -> Option<url::Url> {
    if location.url.host_str() == Some("github.com") {
        let mut path = location
            .url
            .path_segments()
            .unwrap()
            .take(3)
            .collect::<Vec<&str>>()
            .join("/");
        if path.ends_with(".git") {
            path = path[..path.len() - 4].to_string();
        }
        if location.subpath.is_some() || location.branch.is_some() {
            path.push_str(&format!(
                "/tree/{}",
                location.branch.as_deref().unwrap_or("HEAD")
            ));
        }
        if let Some(subpath_str) = location.subpath.as_deref() {
            path.push_str(&format!("/{}", subpath_str));
        }
        Some(
            Url::parse("https://github.com")
                .unwrap()
                .join(&path)
                .unwrap(),
        )
    } else if location.url.host_str() == Some("gopkg.in") {
        let mut els = location
            .url
            .path_segments()
            .unwrap()
            .take(3)
            .collect::<Vec<&str>>();
        if els.len() != 2 {
            return None;
        }
        if let Some(version) = els[2].strip_prefix(".v") {
            els[2] = "";
            let mut path = els.join("/");
            path.push_str(&format!("/tree/{}", version));
            if let Some(subpath_str) = location.subpath.as_deref() {
                path.push_str(&format!("/{}", subpath_str));
            }
            Some(
                Url::parse("https://github.com")
                    .unwrap()
                    .join(&path)
                    .unwrap(),
            )
        } else {
            None
        }
    } else if location.url.host_str() == Some("code.launchpad.net")
        || location.url.host_str() == Some("launchpad.net")
    {
        let mut path = location.url.path().to_string();
        if let Some(subpath_str) = location.subpath.as_deref() {
            path.push_str(&format!("/view/head:{}", subpath_str));
            return Some(
                Url::parse(format!("https://bazaar.launchpad.net{}", path).as_str()).unwrap(),
            );
        } else {
            return Some(
                Url::parse(format!("https://code.launchpad.net{}", path).as_str()).unwrap(),
            );
        }
    } else if location.url.host_str() == Some("svn.apache.org") {
        let path_elements = location
            .url
            .path_segments()
            .map(|segments| segments.into_iter().collect::<Vec<&str>>())
            .unwrap_or_default();
        if path_elements.len() >= 2 && path_elements[0] == "repos" && path_elements[1] == "asf" {
            let mut path_elements = path_elements.into_iter().skip(1).collect::<Vec<&str>>();
            path_elements[0] = "viewvc";
            if let Some(subpath_str) = location.subpath.as_deref() {
                path_elements.push(subpath_str);
            }
            return Some(
                Url::parse(format!("https://svn.apache.org{}", path_elements.join("/")).as_str())
                    .unwrap(),
            );
        } else {
            None
        }
    } else if location.url.host_str() == Some("git.savannah.gnu.org")
        || location.url.host_str() == Some("git.sv.gnu.org")
    {
        let mut path_elements = location.url.path_segments().unwrap().collect::<Vec<&str>>();
        if location.url.scheme() == "https" && path_elements.first() == Some(&"git") {
            path_elements.remove(0);
        }
        path_elements.insert(0, "cgit");
        if let Some(subpath_str) = location.subpath.as_deref() {
            path_elements.push("tree");
            path_elements.push(subpath_str);
        }
        Some(
            Url::parse(format!("https://git.savannah.gnu.org{}", path_elements.join("/")).as_str())
                .unwrap(),
        )
    } else if location.url.host_str().is_some()
        && is_gitlab_site(location.url.host_str().unwrap(), net_access).await
    {
        let mut path = location.url.path().to_string();
        if path.ends_with(".git") {
            path = path[..path.len() - 4].to_string();
        }
        if let Some(subpath_str) = location.subpath.as_deref() {
            path.push_str(&format!("/-/blob/HEAD/{}", subpath_str));
        }
        Some(
            Url::parse(format!("https://{}{}", location.url.host_str().unwrap(), path).as_str())
                .unwrap(),
        )
    } else {
        None
    }
}

/// Find a public repository URL from a given repository URL
pub async fn find_public_repo_url(repo_url: &str, net_access: Option<bool>) -> Option<String> {
    let parsed = match Url::parse(repo_url) {
        Ok(parsed) => parsed,
        Err(_) => {
            if repo_url.contains(':') {
                let re = regex!(r"^(?P<user>[^@:/]+@)?(?P<host>[^/:]+):(?P<path>.*)$");
                if let Some(captures) = re.captures(repo_url) {
                    let host = captures.name("host").unwrap().as_str();
                    let path = captures.name("path").unwrap().as_str();
                    if host == "github.com" || is_gitlab_site(host, net_access).await {
                        return Some(format!("https://{}/{}", host, path));
                    }
                }
            }
            return None;
        }
    };

    let revised_url: Option<String>;
    match parsed.host_str() {
        Some("github.com") => {
            if ["https", "http", "git"].contains(&parsed.scheme()) {
                return Some(repo_url.to_string());
            }
            revised_url = Some(
                Url::parse("https://github.com")
                    .unwrap()
                    .join(parsed.path())
                    .unwrap()
                    .to_string(),
            );
        }
        Some(hostname) if is_gitlab_site(hostname, net_access).await => {
            if ["https", "http"].contains(&parsed.scheme()) {
                return Some(repo_url.to_string());
            }
            if parsed.scheme() == "ssh" {
                revised_url = Some(format!(
                    "https://{}{}",
                    parsed.host_str().unwrap(),
                    parsed.path(),
                ));
            } else {
                revised_url = None;
            }
        }
        Some("code.launchpad.net") | Some("bazaar.launchpad.net") | Some("git.launchpad.net") => {
            if parsed.scheme().starts_with("http") || parsed.scheme() == "lp" {
                return Some(repo_url.to_string());
            }
            if ["ssh", "bzr+ssh"].contains(&parsed.scheme()) {
                revised_url = Some(format!(
                    "https://{}{}",
                    parsed.host_str().unwrap(),
                    parsed.path()
                ));
            } else {
                revised_url = None;
            }
        }
        _ => revised_url = None,
    }

    revised_url
}

/// Fix up a Git URL to use the correct scheme
#[cfg(feature = "breezy")]
pub fn fixup_rcp_style_git_repo_url(url: &str) -> Option<Url> {
    if breezyshim::try_init().is_err() {
        return None;
    }
    breezyshim::location::rcp_location_to_url(url).ok()
}

/// Try to open a branch from a URL
#[cfg(feature = "breezy")]
pub fn try_open_branch(
    url: &url::Url,
    branch_name: Option<&str>,
) -> Option<Box<breezyshim::branch::GenericBranch>> {
    if let Err(e) = breezyshim::try_init() {
        debug!("breezy not available, skipping branch open: {}", e);
        return None;
    }
    use breezyshim::branch::Branch;
    let old_ui_factory = breezyshim::ui::get_ui_factory();
    breezyshim::ui::install_ui_factory(&breezyshim::ui::SilentUIFactory::new());

    let controldir = match breezyshim::controldir::open(url, None) {
        Ok(c) => c,
        Err(_) => return None,
    };

    let rev = match controldir.open_branch(branch_name) {
        Ok(b) => {
            b.last_revision();
            Some(b)
        }
        Err(_) => None,
    };

    breezyshim::ui::install_ui_factory(old_ui_factory.as_ref());
    rev
}

/// Find a secure repository URL
pub async fn find_secure_repo_url(
    mut url: url::Url,
    branch: Option<&str>,
    net_access: Option<bool>,
) -> Option<url::Url> {
    if SECURE_SCHEMES.contains(&url.scheme()) {
        return Some(url);
    }

    // Sites we know to be available over https
    if let Some(hostname) = url.host_str() {
        if is_gitlab_site(hostname, net_access).await
            || [
                "github.com",
                "git.launchpad.net",
                "bazaar.launchpad.net",
                "code.launchpad.net",
            ]
            .contains(&hostname)
        {
            url = derive_with_scheme(&url, "https");
        }
    }

    if url.scheme() == "lp" {
        // lp: URLs like "lp:test-package" are parsed with the package name as the path
        // We need to preserve this path when converting to https://code.launchpad.net/test-package
        let original_path = url.path().to_string();
        url = derive_with_scheme(&url, "https");
        url.set_host(Some("code.launchpad.net")).unwrap();
        // Ensure the path starts with /
        if !original_path.starts_with('/') {
            url.set_path(&format!("/{}", original_path));
        }
    }

    if let Some(host) = url.host_str() {
        if ["git.savannah.gnu.org", "git.sv.gnu.org"].contains(&host) {
            if url.scheme() == "http" {
                url = derive_with_scheme(&url, "https");
            } else {
                url = derive_with_scheme(&url, "https");
                url.set_path(format!("/git{}", url.path()).as_str());
            }
        }
    } else {
        return None;
    }

    #[cfg(feature = "breezy")]
    if net_access.unwrap_or(true) {
        use breezyshim::branch::Branch;
        let secure_repo_url = derive_with_scheme(&url, "https");
        let insecure_branch = try_open_branch(&url, branch);
        let secure_branch = try_open_branch(&secure_repo_url, branch);
        if let Some(secure_branch) = secure_branch {
            if insecure_branch.is_none()
                || secure_branch.last_revision() == insecure_branch.unwrap().last_revision()
            {
                url = secure_repo_url;
            }
        }
    }

    if SECURE_SCHEMES.contains(&url.scheme()) {
        Some(url)
    } else {
        // Can't find a secure URI :(
        None
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// A VCS location
pub struct VcsLocation {
    /// URL of the repository
    pub url: url::Url,

    /// Name of the branch
    pub branch: Option<String>,

    /// Subpath within the repository
    pub subpath: Option<String>,
}

impl VcsLocation {
    async fn from_str(url: &str) -> Result<Self, url::ParseError> {
        let (url, branch, subpath) = split_vcs_url(url);
        let url = fixup_git_url(url.as_str()).await;
        Ok(VcsLocation {
            url: url.parse()?,
            branch,
            subpath,
        })
    }
}

impl std::fmt::Display for VcsLocation {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", unsplit_vcs_url(self))
    }
}

impl From<VcsLocation> for url::Url {
    fn from(v: VcsLocation) -> Self {
        v.url
    }
}

impl From<url::Url> for VcsLocation {
    fn from(url: url::Url) -> Self {
        VcsLocation {
            url,
            branch: None,
            subpath: None,
        }
    }
}

fn derive_with_scheme(url: &url::Url, scheme: &str) -> url::Url {
    let mut s = url.to_string();
    s.replace_range(..url.scheme().len(), scheme);
    url::Url::parse(&s).unwrap()
}

fn fix_path_in_port(url: &str) -> Option<String> {
    let (_, scheme, host, port, rest) =
        lazy_regex::regex_captures!(r"^([^:]+)://([^:]+):([^/]+)(/.*)$", url)?;

    if port.ends_with(']') {
        return None;
    }

    if let Ok(port) = port.parse::<u16>() {
        Some(format!("{}://{}:{}{}", scheme, host, port, rest))
    } else {
        Some(format!("{}://{}/{}{}", scheme, host, port, rest))
    }
}

async fn fix_gitlab_scheme(url: &str) -> Option<String> {
    if let Ok(url) = url::Url::parse(url) {
        if let Some(host) = url.host_str() {
            if is_gitlab_site(host, None).await {
                return Some(derive_with_scheme(&url, "https").to_string());
            }
        }
    }
    None
}

fn fix_github_scheme(url: &str) -> Option<String> {
    // GitHub no longer supports the git:// scheme
    if let Ok(url) = url::Url::parse(url) {
        if url.host_str() == Some("github.com") {
            return Some(derive_with_scheme(&url, "https").to_string());
        }
    }
    None
}

fn fix_salsa_cgit_url(url: &str) -> Option<String> {
    if let Ok(url) = url::Url::parse(url) {
        if url.host_str() == Some("salsa.debian.org") {
            if let Some(suffix) = url.path().strip_prefix("/cgit/") {
                let mut url = url.clone();
                url.set_path(suffix);
                return Some(url.to_string());
            }
        }
    }
    None
}

async fn fix_gitlab_tree_in_url(location: &VcsLocation) -> Option<VcsLocation> {
    if is_gitlab_site(location.url.host_str()?, None).await {
        let segments = location.url.path_segments().unwrap().collect::<Vec<_>>();
        if let Some(p) = segments.iter().position(|p| *p == "tree") {
            let branch = segments[(p + 1)..].join("/");
            let path = segments[..p].join("/");
            let mut url = location.url.clone();
            url.set_path(path.as_str());
            return Some(VcsLocation {
                url,
                branch: Some(branch),
                subpath: location.subpath.clone(),
            });
        }
    }
    None
}

fn fix_double_slash(url: &str) -> Option<String> {
    if let Ok(mut url) = url::Url::parse(url) {
        if url.path().starts_with("//") {
            let path = url
                .path()
                .to_string()
                .strip_prefix("//")
                .unwrap()
                .to_string();
            url.set_path(path.as_str());
            return Some(url.to_string());
        }
    }
    None
}

fn fix_extra_colon(url: &str) -> Option<String> {
    if let Ok(mut url) = url::Url::parse(url) {
        if url.path().starts_with(':') {
            let path = url
                .path()
                .to_string()
                .strip_prefix(':')
                .unwrap()
                .to_string();
            url.set_path(&path);
            return Some(url.to_string());
        }
    }
    None
}

fn drop_git_username(url: &str) -> Option<String> {
    if let Ok(mut url) = url::Url::parse(url) {
        if let Some(host) = url.host_str() {
            if !["github.com", "salsa.debian.org", "gitlab.com"].contains(&host) {
                return None;
            }
        } else {
            return None;
        }
        if !["git", "http", "https"].contains(&url.scheme()) {
            return None;
        }
        if url.username() == "git" {
            url.set_username("").unwrap();
            return Some(url.to_string());
        }
    }
    None
}

fn fix_branch_argument(location: &VcsLocation) -> Option<VcsLocation> {
    if location.url.host_str() == Some("github.com") {
        // TODO(jelmer): Handle gitlab sites too?
        let path_elements = location.url.path_segments().unwrap().collect::<Vec<_>>();
        if path_elements.len() > 2 && path_elements[2] == "tree" {
            let branch = path_elements[3..].join("/");
            let path = path_elements[..2].join("/");
            let mut url = location.url.clone();
            url.set_path(path.as_str());
            Some(VcsLocation {
                url,
                branch: Some(branch),
                subpath: location.subpath.clone(),
            })
        } else {
            None
        }
    } else {
        None
    }
}

fn fix_git_gnome_org_url(url: &str) -> Option<String> {
    if let Ok(url) = url::Url::parse(url) {
        if url.host_str() == Some("git.gnome.org") {
            let mut path_segments = url.path_segments().unwrap().collect::<Vec<_>>();
            if path_segments.first().map(|p| *p == "browse") == Some(true) {
                path_segments.remove(0);
            }

            let mut url = derive_with_scheme(&url, "https");
            url.set_host(Some("gitlab.gnome.org")).unwrap();
            url.set_path(format!("GNOME/{}", path_segments.join("/")).as_str());
            return Some(url.to_string());
        }
    }
    None
}

fn fix_kde_anongit_url(url: &str) -> Option<String> {
    if let Ok(url) = url::Url::parse(url) {
        if url.host_str() == Some("anongit.kde.org") {
            let url = derive_with_scheme(&url, "https");
            return Some(url.to_string());
        }
    }
    None
}

fn fix_freedesktop_org_url(url: &str) -> Option<String> {
    if let Ok(url) = url::Url::parse(url) {
        if url.host_str() == Some("anongit.freedesktop.org") {
            let suffix = url.path().strip_prefix("/git/");
            let mut url = derive_with_scheme(&url, "https");
            if let Some(suffix) = suffix {
                url.set_path(suffix);
            }
            url.set_host(Some("gitlab.freedesktop.org")).unwrap();
            return Some(url.to_string());
        }
    }
    None
}

type AsyncLocationFixer = for<'a> fn(
    &'a VcsLocation,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Option<VcsLocation>> + Send + 'a>,
>;

const LOCATION_FIXERS: &[AsyncLocationFixer] = &[
    |loc| Box::pin(async move { fix_gitlab_tree_in_url(loc).await }),
    |loc| Box::pin(async move { fix_branch_argument(loc) }),
];

/// Attempt to fix up broken Git URLs.
pub async fn fixup_git_location(location: &VcsLocation) -> Cow<'_, VcsLocation> {
    let mut location = Cow::Borrowed(location);
    for cb in LOCATION_FIXERS {
        location = cb(&location).await.map_or(location, Cow::Owned);
    }
    location
}

type AsyncFixer = for<'a> fn(
    &'a str,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Option<String>> + Send + 'a>,
>;

const URL_FIXERS: &[AsyncFixer] = &[
    |url| Box::pin(async move { fix_path_in_port(url) }),
    |url| Box::pin(async move { fix_gitlab_scheme(url).await }),
    |url| Box::pin(async move { fix_github_scheme(url) }),
    |url| Box::pin(async move { fix_salsa_cgit_url(url) }),
    |url| Box::pin(async move { fix_double_slash(url) }),
    |url| Box::pin(async move { fix_extra_colon(url) }),
    |url| Box::pin(async move { drop_git_username(url) }),
    |url| Box::pin(async move { fix_freedesktop_org_url(url) }),
    |url| Box::pin(async move { fix_kde_anongit_url(url) }),
    |url| Box::pin(async move { fix_git_gnome_org_url(url) }),
];

/// Fixup a Git URL
pub async fn fixup_git_url(url: &str) -> String {
    let mut url = url.to_string();
    for cb in URL_FIXERS {
        url = cb(&url).await.unwrap_or(url);
    }
    url
}

/// Convert a CVS URL to a Breezy URL.
#[cfg(feature = "breezy")]
pub fn convert_cvs_list_to_str(urls: &[&str]) -> Option<String> {
    if urls[0].starts_with(":extssh:") || urls[0].starts_with(":pserver:") {
        if breezyshim::try_init().is_err() {
            return None;
        }
        let url = breezyshim::location::cvs_to_url(urls[0]);
        Some(format!("{}#{}", url, urls[1]))
    } else {
        None
    }
}

type AsyncSanitizer = for<'a> fn(
    &'a str,
) -> std::pin::Pin<
    Box<dyn std::future::Future<Output = Option<Url>> + Send + 'a>,
>;

const SANITIZERS: &[AsyncSanitizer] = &[
    |url| Box::pin(async move { drop_vcs_in_scheme(&url.parse().ok()?) }),
    |url| {
        Box::pin(async move {
            let location = VcsLocation::from_str(url).await.ok()?;
            Some(fixup_git_location(&location).await.url.clone())
        })
    },
    #[cfg(feature = "breezy")]
    |url| Box::pin(async move { fixup_rcp_style_git_repo_url(url) }),
    |url| {
        Box::pin(async move {
            find_public_repo_url(url.to_string().as_str(), None)
                .await
                .and_then(|u| u.parse().ok())
        })
    },
    |url| Box::pin(async move { canonical_git_repo_url(&url.parse().ok()?, None).await }),
    |url| Box::pin(async move { find_secure_repo_url(url.parse().ok()?, None, Some(false)).await }),
];

/// Sanitize a VCS URL
pub async fn sanitize_url(url: &str) -> String {
    let mut url: Cow<'_, str> = Cow::Borrowed(url);
    for sanitizer in SANITIZERS {
        url = sanitizer(url.as_ref())
            .await
            .map_or(url, |f| Cow::Owned(f.to_string()));
    }
    url.into_owned()
}

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

    async fn fixup_git_location(url: &str) -> String {
        let location = super::VcsLocation::from_str(url)
            .await
            .expect("Failed to parse URL in test");
        super::fixup_git_location(&location).await.to_string()
    }

    #[test]
    fn test_plausible_url() {
        use super::plausible_url;
        assert!(!plausible_url("the"));
        assert!(!plausible_url("1"));
        assert!(plausible_url("git@foo:blah"));
        assert!(plausible_url("git+ssh://git@foo/blah"));
        assert!(plausible_url("https://foo/blah"));
    }

    #[tokio::test]
    async fn test_is_gitlab_site() {
        use super::is_gitlab_site;

        assert!(is_gitlab_site("gitlab.com", Some(false)).await);
        assert!(is_gitlab_site("gitlab.example.com", Some(false)).await);
        assert!(is_gitlab_site("salsa.debian.org", Some(false)).await);
        assert!(!is_gitlab_site("github.com", Some(false)).await);
        assert!(!is_gitlab_site("foo.example.com", Some(false)).await);
    }

    #[tokio::test]
    async fn test_canonicalize_github() {
        use super::canonical_git_repo_url;
        use url::Url;
        assert_eq!(
            Some(
                "https://github.com/jelmer/example.git"
                    .parse::<Url>()
                    .unwrap()
            ),
            canonical_git_repo_url(
                &"https://github.com/jelmer/example".parse::<Url>().unwrap(),
                Some(false)
            )
            .await
        );
    }

    #[tokio::test]
    async fn test_canonicalize_github_ssh() {
        use super::canonical_git_repo_url;
        use url::Url;
        assert_eq!(
            Some(
                "https://salsa.debian.org/jelmer/example.git"
                    .parse::<Url>()
                    .unwrap()
            ),
            canonical_git_repo_url(
                &"https://salsa.debian.org/jelmer/example"
                    .parse::<Url>()
                    .unwrap(),
                Some(false)
            )
            .await
        );
        assert_eq!(
            None,
            canonical_git_repo_url(
                &"https://salsa.debian.org/jelmer/example.git"
                    .parse::<Url>()
                    .unwrap(),
                Some(false)
            )
            .await
        );
    }

    #[tokio::test]
    async fn test_find_public_github() {
        use super::find_public_repo_url;
        assert_eq!(
            "https://github.com/jelmer/example",
            find_public_repo_url("ssh://git@github.com/jelmer/example", Some(false))
                .await
                .unwrap()
        );
        assert_eq!(
            Some("https://github.com/jelmer/example"),
            find_public_repo_url("https://github.com/jelmer/example", Some(false))
                .await
                .as_deref()
        );
        assert_eq!(
            "https://github.com/jelmer/example",
            find_public_repo_url("git@github.com:jelmer/example", Some(false))
                .await
                .unwrap()
                .as_str()
        );
    }

    #[tokio::test]
    async fn test_find_public_salsa() {
        use super::find_public_repo_url;
        assert_eq!(
            "https://salsa.debian.org/jelmer/example",
            find_public_repo_url("ssh://salsa.debian.org/jelmer/example", Some(false))
                .await
                .unwrap()
                .as_str()
        );
        assert_eq!(
            "https://salsa.debian.org/jelmer/example",
            find_public_repo_url("https://salsa.debian.org/jelmer/example", Some(false))
                .await
                .unwrap()
                .as_str()
        );
    }
    #[cfg(feature = "breezy")]
    #[test]
    fn test_fixup_rcp_style() {
        use super::fixup_rcp_style_git_repo_url;
        use url::Url;
        assert_eq!(
            Some(
                "ssh://git@github.com/jelmer/example"
                    .parse::<Url>()
                    .unwrap()
            ),
            fixup_rcp_style_git_repo_url("git@github.com:jelmer/example")
        );

        assert_eq!(
            Some("ssh://github.com/jelmer/example".parse::<Url>().unwrap()),
            fixup_rcp_style_git_repo_url("github.com:jelmer/example")
        );
    }

    #[cfg(feature = "breezy")]
    #[test]
    fn test_fixup_rcp_leave() {
        use super::fixup_rcp_style_git_repo_url;
        assert_eq!(
            None,
            fixup_rcp_style_git_repo_url("https://salsa.debian.org/jelmer/example")
        );
        assert_eq!(
            None,
            fixup_rcp_style_git_repo_url("ssh://git@salsa.debian.org/jelmer/example")
        );
    }

    #[tokio::test]
    async fn test_guess_repo_url_travis_ci_org() {
        use super::guess_repo_from_url;
        assert_eq!(
            Some("https://github.com/jelmer/dulwich"),
            guess_repo_from_url(
                &"https://travis-ci.org/jelmer/dulwich".parse().unwrap(),
                Some(false)
            )
            .await
            .as_deref(),
        );
    }

    #[tokio::test]
    async fn test_guess_repo_url_coveralls() {
        use super::guess_repo_from_url;
        assert_eq!(
            Some("https://github.com/jelmer/dulwich"),
            guess_repo_from_url(
                &"https://coveralls.io/r/jelmer/dulwich".parse().unwrap(),
                Some(false)
            )
            .await
            .as_deref(),
        );
    }

    #[tokio::test]
    async fn test_guess_repo_url_gitlab() {
        use super::guess_repo_from_url;
        assert_eq!(
            Some("https://gitlab.com/jelmer/dulwich"),
            guess_repo_from_url(
                &"https://gitlab.com/jelmer/dulwich".parse().unwrap(),
                Some(false)
            )
            .await
            .as_deref(),
        );
        assert_eq!(
            Some("https://gitlab.com/jelmer/dulwich"),
            guess_repo_from_url(
                &"https://gitlab.com/jelmer/dulwich/tags".parse().unwrap(),
                Some(false)
            )
            .await
            .as_deref(),
        );
    }

    #[tokio::test]
    async fn test_fixup_git_location() {
        use super::{fixup_git_location, VcsLocation};
        assert_eq!(
            VcsLocation {
                url: "https://github.com/jelmer/dulwich".parse().unwrap(),
                branch: None,
                subpath: None,
            },
            fixup_git_location(&VcsLocation {
                url: "https://github.com/jelmer/dulwich".parse().unwrap(),
                branch: None,
                subpath: None,
            })
            .await
            .into_owned()
        );
    }

    #[tokio::test]
    async fn test_browse_url_from_repo() {
        use super::browse_url_from_repo_url;
        assert_eq!(
            Some("https://github.com/jelmer/dulwich".parse().unwrap()),
            browse_url_from_repo_url(
                &super::VcsLocation {
                    url: "https://github.com/jelmer/dulwich".parse().unwrap(),
                    branch: None,
                    subpath: None,
                },
                Some(false)
            )
            .await
        );
        assert_eq!(
            Some("https://github.com/jelmer/dulwich".parse().unwrap()),
            browse_url_from_repo_url(
                &super::VcsLocation {
                    url: "https://github.com/jelmer/dulwich.git".parse().unwrap(),
                    branch: None,
                    subpath: None,
                },
                Some(false)
            )
            .await
        );
        assert_eq!(
            Some(
                "https://github.com/jelmer/dulwich/tree/foo"
                    .parse()
                    .unwrap()
            ),
            browse_url_from_repo_url(
                &super::VcsLocation {
                    url: "https://github.com/jelmer/dulwich.git".parse().unwrap(),
                    branch: Some("foo".to_string()),
                    subpath: None,
                },
                Some(false)
            )
            .await
        );
        assert_eq!(
            Some(
                "https://github.com/jelmer/dulwich/tree/HEAD/foo"
                    .parse()
                    .unwrap()
            ),
            browse_url_from_repo_url(
                &super::VcsLocation {
                    url: "https://github.com/jelmer/dulwich.git".parse().unwrap(),
                    branch: None,
                    subpath: Some("foo".to_string()),
                },
                Some(false)
            )
            .await
        );
    }

    #[test]
    fn test_fix_github_scheme() {
        use super::fix_github_scheme;
        assert_eq!(
            Some("https://github.com/jelmer/example"),
            fix_github_scheme("git://github.com/jelmer/example").as_deref()
        );
    }

    #[test]
    fn test_fix_git_gnome_org_url() {
        use super::fix_git_gnome_org_url;
        assert_eq!(
            Some("https://gitlab.gnome.org/GNOME/example".to_string()),
            fix_git_gnome_org_url("https://git.gnome.org/browse/example")
        );
    }

    #[tokio::test]
    async fn test_fixup() {
        assert_eq!(
            "https://github.com/jelmer/dulwich",
            fixup_git_url("https://github.com:jelmer/dulwich").await,
        );
        assert_eq!(
            "https://github.com/jelmer/dulwich -b blah",
            fixup_git_location("https://github.com:jelmer/dulwich -b blah").await
        );
        assert_eq!(
            "https://github.com/jelmer/dulwich",
            fixup_git_url("git://github.com/jelmer/dulwich").await,
        );
    }

    #[tokio::test]
    async fn test_preserves() {
        assert_eq!(
            "https://github.com/jelmer/dulwich",
            fixup_git_url("https://github.com/jelmer/dulwich").await
        );
    }

    #[tokio::test]
    async fn test_salsa_not_https() {
        assert_eq!(
            "https://salsa.debian.org/jelmer/dulwich",
            fixup_git_url("git://salsa.debian.org/jelmer/dulwich").await
        );
    }

    #[tokio::test]
    async fn test_salsa_uses_cgit() {
        assert_eq!(
            "https://salsa.debian.org/jelmer/dulwich",
            fixup_git_url("https://salsa.debian.org/cgit/jelmer/dulwich").await
        );
    }

    #[tokio::test]
    async fn test_salsa_tree_branch() {
        assert_eq!(
            "https://salsa.debian.org/jelmer/dulwich -b master",
            fixup_git_location("https://salsa.debian.org/jelmer/dulwich/tree/master").await
        );
    }

    #[tokio::test]
    async fn test_strip_extra_slash() {
        assert_eq!(
            "https://salsa.debian.org/salve/auctex.git",
            fixup_git_url("https://salsa.debian.org//salve/auctex.git").await
        );
    }

    #[tokio::test]
    async fn test_strip_extra_colon() {
        assert_eq!(
            "https://salsa.debian.org/mckinstry/lcov.git",
            fixup_git_url("https://salsa.debian.org:/mckinstry/lcov.git").await
        );
    }

    #[tokio::test]
    async fn test_strip_username() {
        assert_eq!(
            "https://github.com/RPi-Distro/pgzero.git",
            fixup_git_url("git://git@github.com:RPi-Distro/pgzero.git").await
        );
        assert_eq!(
            "https://salsa.debian.org/debian-astro-team/pyavm.git",
            fixup_git_url("https://git@salsa.debian.org:debian-astro-team/pyavm.git").await
        );
    }

    #[tokio::test]
    async fn test_github_tree_url() {
        assert_eq!(
            "https://github.com/blah/blah -b master",
            fixup_git_location("https://github.com/blah/blah/tree/master").await
        );
    }

    #[tokio::test]
    async fn test_freedesktop() {
        assert_eq!(
            "https://gitlab.freedesktop.org/xorg/xserver",
            fixup_git_url("git://anongit.freedesktop.org/xorg/xserver").await
        );
        assert_eq!(
            "https://gitlab.freedesktop.org/xorg/lib/libSM",
            fixup_git_url("git://anongit.freedesktop.org/git/xorg/lib/libSM").await
        );
    }

    #[tokio::test]
    async fn test_anongit() {
        assert_eq!(
            "https://anongit.kde.org/kdev-php.git",
            fixup_git_url("git://anongit.kde.org/kdev-php.git").await
        );
    }

    #[tokio::test]
    async fn test_gnome() {
        assert_eq!(
            "https://gitlab.gnome.org/GNOME/alacarte",
            fixup_git_url("https://git.gnome.org/browse/alacarte").await
        );
    }

    #[tokio::test]
    async fn test_cvs_url_parsing_error() {
        // Test that CVS URLs that aren't valid HTTP URLs return an error instead of panicking
        // This was the bug reported in upstream-ontologist-cvs-array-panic.md
        let cvs_url = ":extssh:_anoncvs@anoncvs.example.org:/cvs";
        let result = super::VcsLocation::from_str(cvs_url).await;
        assert!(result.is_err(), "CVS URL should return an error, not panic");
    }

    #[tokio::test]
    async fn test_sanitize_url_with_cvs() {
        // Test that sanitize_url handles CVS URLs gracefully without panicking
        let cvs_url = ":extssh:_anoncvs@anoncvs.example.org:/cvs";
        let result = super::sanitize_url(cvs_url).await;
        // The URL should be returned as-is since none of the sanitizers can process it
        assert_eq!(result, cvs_url);
    }

    #[tokio::test]
    async fn test_sanitize_url_with_invalid_url() {
        // Test that other invalid URLs are also handled gracefully
        let invalid_url = "not a valid url at all";
        let result = super::sanitize_url(invalid_url).await;
        // Should not panic, and should return the original string
        assert_eq!(result, invalid_url);
    }

    #[tokio::test]
    async fn test_find_secure_repo_url_lp() {
        use super::find_secure_repo_url;
        use url::Url;

        // Test basic lp: URL conversion
        let url = Url::parse("lp:test-package").unwrap();
        let result = find_secure_repo_url(url, None, Some(false)).await;
        assert_eq!(
            result,
            Some(Url::parse("https://code.launchpad.net/test-package").unwrap())
        );

        // Test lp: URL with path
        let url = Url::parse("lp:ubuntu/test-package").unwrap();
        let result = find_secure_repo_url(url, None, Some(false)).await;
        assert_eq!(
            result,
            Some(Url::parse("https://code.launchpad.net/ubuntu/test-package").unwrap())
        );
    }
}
