use log::*;
use scraper::{Html, Selector};

async fn get_rels_from_body(html: String, rel: &str) -> Vec<String> {
    let link_rel_redirect_uri_selector =
        Selector::parse(&format!("link[rel=\"{}\"]", rel)).unwrap();
    let a_rel_redirect_uri_selector = Selector::parse(&format!("a[rel=\"{}\"]", rel)).unwrap();

    let html_document_fragment = Html::parse_fragment(html.as_str());

    let mut endpoints = Vec::new();

    endpoints.extend(
        html_document_fragment
            .select(&link_rel_redirect_uri_selector)
            .map(|element| element.value().attr("href").unwrap_or_default().to_owned()),
    );

    endpoints.extend(
        html_document_fragment
            .select(&a_rel_redirect_uri_selector)
            .map(|element| element.value().attr("href").unwrap_or_default().to_owned()),
    );

    endpoints
        .iter()
        .filter(|href| !href.is_empty())
        .cloned()
        .collect()
}

fn get_rels_from_header(headers: &reqwest::header::HeaderMap, rel: &str) -> Vec<String> {
    if let Some(redirect_uri_header_value) = headers.get("Link") {
        let redirect_uri_header = redirect_uri_header_value.to_str().unwrap();
        let endpoints = redirect_uri_header
            .split(',')
            .collect::<Vec<&str>>()
            .iter()
            .map(|link_rel: &&str| {
                let mut parameters = link_rel.split("; ").collect::<Vec<&str>>();
                let uri_value: String = parameters.remove(0).to_owned();
                (uri_value, parameters)
            })
            .filter(|(_uri, parameters)| parameters.contains(&format!("rel=\"{}\"", rel).as_str()))
            .map(|(uri, _parameters)| {
                uri.strip_prefix("<")
                    .unwrap()
                    .strip_suffix(">")
                    .unwrap()
                    .to_owned()
            })
            .collect::<Vec<String>>();
        endpoints
    } else {
        Vec::default()
    }
}

/// Resolves all of the relating links for a particular URL.
pub async fn for_url<C>(config: &C, url: &str, rel: &str) -> Vec<String>
where
    C: crate::IConfiguration + Send + Sync,
{
    let resp_builder = config
        .request(Some(format!(
            "Resolving rel={:?} values for the resource at this URL",
            rel
        )))
        .request(reqwest::Method::GET, url.clone())
        .send();

    match resp_builder.await {
        Ok(resp) => {
            let rels_from_header = get_rels_from_header(resp.headers(), rel);
            let rels_from_body =
                get_rels_from_body(resp.text().await.ok().unwrap_or_default(), rel).await;

            let mut all_links = Vec::default();
            all_links.extend(rels_from_header);
            all_links.extend(rels_from_body);

            all_links = all_links
                .iter()
                .cloned()
                .filter_map(
                    |resolved_url| match url::Url::parse(resolved_url.as_str()) {
                        Ok(_) => Some(resolved_url),
                        Err(err) => {
                            error!(
                                "Root URL merge with {:?} and {:?} failed because {:#?}",
                                url, resolved_url, err
                            );

                            url::Url::parse(url)
                                .ok()?
                                .join(resolved_url.as_str())
                                .ok()
                                .map(|url| url.to_string())
                        }
                    },
                )
                .collect();

            // FIXME: Make this list unique
            all_links.dedup();
            all_links
        }
        Err(err) => {
            error!("Failed to connect to {:?}: {:#?}", url, err);
            Vec::default()
        }
    }
}

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

    #[tokio::test(flavor = "multi_thread")]
    async fn for_url_returns_empty_list_on_error() {
        let cfg = test::init();
        let urls_for_rel = for_url(&cfg, "https://example.com", "payment").await;
        assert_eq!(urls_for_rel.len(), 0);
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn for_url_provides_bad_urls_as_is() {
        let cfg = test::init();
        let url = format!("{}/bad-urls", mockito::server_url());
        let urls = vec![
            "well-dang".to_owned(),
            "https://paypal.not-me/a-user".to_owned(),
        ];
        let expected_urls = vec![
            format!("{}/well-dang", mockito::server_url()),
            urls.get(1).unwrap().clone(),
        ];
        let link_str = urls
            .iter()
            .map(|u| format!(r#"<link rel="payment" href={} />"#, u))
            .collect::<Vec<String>>()
            .join("\n");
        let _m = mock("GET", "/bad-urls")
            .with_status(200)
            .with_header("Content-Type", "text/html")
            .with_body(format!("<html>{}</html>", link_str))
            .create();
        let urls_for_rel = for_url(&cfg, &url, "payment").await;
        assert_eq!(urls_for_rel, expected_urls);
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn for_url_extracts_from_headers() {
        let cfg = test::init();
        let url = format!("{}/bad-urls", mockito::server_url());
        let urls = vec![
            "well-dang".to_owned(),
            "https://paypal.not-me/a-user".to_owned(),
        ];
        let expected_urls = vec![
            format!("{}/well-dang", mockito::server_url()),
            urls.get(1).unwrap().clone(),
        ];
        let link_str = urls
            .iter()
            .map(|u| format!(r#"<{}>; rel="payment""#, u))
            .collect::<Vec<String>>()
            .join(",");
        let _m = mock("GET", "/bad-urls")
            .with_status(200)
            .with_header("Content-Type", "text/html")
            .with_header("Link", &link_str)
            .with_body(String::default())
            .create();
        let urls_for_rel = for_url(&cfg, &url, "payment").await;
        assert_eq!(urls_for_rel, expected_urls);
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn for_url_extracts_from_html_body() {
        let cfg = test::init();
        let url = format!("{}/urls", mockito::server_url());
        let urls = vec![
            "/well-dang".to_owned(),
            "https://paypal.not-me/a-user".to_owned(),
        ];
        let expected_urls = vec![
            format!("{}/well-dang", mockito::server_url()),
            urls.get(1).unwrap().clone(),
        ];
        let link_str = urls
            .iter()
            .map(|u| format!(r#"<link rel="payment" href={} />"#, u))
            .collect::<Vec<String>>()
            .join("\n");
        let _m = mock("GET", "/urls")
            .with_status(200)
            .with_header("Content-Type", "text/html")
            .with_body(format!("<html>{}</html>", link_str))
            .create();
        let urls_for_rel = for_url(&cfg, &url, "payment").await;
        assert_eq!(urls_for_rel, expected_urls);
    }
}
