use super::{Request, Response, Status};
use crate::{Error, IConfiguration};
use anyhow::Result;

/// Resolves the Webmention endpoints for the provided URL.
pub async fn endpoints_for<C>(cfg: &C, url: &str) -> Result<Vec<String>, Error>
where
    C: IConfiguration + Sync + Send + Sized,
{
    let urls = crate::link_rel::for_url(cfg, url, "webmention").await;
    if urls.is_empty() {
        Err(Error::NoEndpointsFound {
            url: url.to_owned(),
            rel: "webmention".to_owned(),
        })
    } else {
        Ok(urls)
    }
}

/// Sends a Webmention.
pub async fn send<C>(cfg: &C, req: &Request) -> Result<Response, Error>
where
    C: IConfiguration + Sync + Send + Sized,
{
    let response = cfg
        .request(Some("Sending a Webmention".to_owned()))
        .request(reqwest::Method::POST, req.endpoint.clone())
        .header("accept", "text/html,application/json")
        .header("content-type", "application/x-www-form-urlencoded")
        .body(serde_qs::to_string(&req).map_err(|e| Error::Other(e.into()))?)
        .send()
        .await
        .map_err(|err| Error::Other(err.into()))?;

    match response.status() {
        reqwest::StatusCode::CREATED => Ok(convert_resp_to_resp(response).await),
        reqwest::StatusCode::OK => Ok(Response {
            status: Status::Sent,
            ..convert_resp_to_resp(response).await
        }),
        reqwest::StatusCode::ACCEPTED => Ok(Response {
            body: response.text_with_charset("utf-8").await.ok(),
            location: None,
            status: Status::Accepted,
        }),
        code => Err(Error::WebmentionUnsupportedStatusCode {
            status_code: code,
            url: req.endpoint.clone(),
        }),
    }
}

async fn convert_resp_to_resp(resp: reqwest::Response) -> Response {
    let location = resp
        .headers()
        .get("location")
        .map(|v| v.to_str().ok().unwrap_or_default().to_owned());
    let body = resp.text_with_charset("utf-8").await.ok().and_then(|body| {
        if body.is_empty() {
            None
        } else {
            Some(body)
        }
    });
    Response {
        body,
        location,
        status: Status::Accepted,
    }
}

/// Dispatchs a Webmention.
pub async fn dispatch<C>(
    cfg: &C,
    source_url: &str,
    target_url: &str,
    additional_options: Option<Request>,
) -> Result<Vec<Response>, Error>
where
    C: IConfiguration + Sync + Send + Sized,
{
    // FIXME: Use `try_flatten_stream` here in the future.
    let endpoints = endpoints_for(cfg, target_url).await?;
    let mut responses = vec![];

    for endpoint in endpoints.into_iter() {
        let options = additional_options.clone().unwrap_or_default();
        let req = Request {
            endpoint,
            source: source_url.to_owned(),
            target: target_url.to_owned(),
            ..options
        };

        responses.push(send(cfg, &req).await?);
    }

    Ok(responses)
}

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

    #[tokio::test]
    async fn send_successful_sync() {
        let cfg = test::init();
        let endpoint_mock = mock("POST", "/endpoint")
            .match_header("content-type", "application/x-www-form-urlencoded")
            .with_header("content-type", "text/plain")
            .with_status(200)
            .with_body("")
            .create();
        let target = format!("{}/web-page", &mockito::server_url());
        let request = super::Request {
            endpoint: format!("{}/endpoint", mockito::server_url()),
            source: format!("{}/source", mockito::server_url()),
            target,
            vouch: None,
        };

        let result = super::send(&cfg, &request).await;
        assert_eq!(
            result.unwrap_or_default(),
            Some(super::Response {
                body: None,
                location: None,
                status: Status::Sent
            })
            .unwrap_or_default()
        );
        endpoint_mock.assert();
    }

    #[tokio::test]
    async fn send_captures_async_response() {
        let cfg = test::init();
        let endpoint_mock = mock("POST", "/endpoint-async-response")
            .match_header("content-type", "application/x-www-form-urlencoded")
            .with_header("content-type", "text/plain")
            .with_status(200)
            .with_body("")
            .create();
        let target = format!("{}/web-page", &mockito::server_url());
        let request = super::Request {
            endpoint: format!("{}/endpoint-async-response", mockito::server_url()),
            source: format!("{}/source", mockito::server_url()),
            target,
            vouch: None,
        };
        let result = super::send(&cfg, &request).await;
        assert_eq!(
            result.unwrap_or_default(),
            Some(super::Response {
                body: None,
                location: None,
                status: Status::Sent
            })
            .unwrap_or_default()
        );
        endpoint_mock.assert();
    }

    #[tokio::test]
    async fn send_captures_resulting_url() {
        let cfg = test::init();
        let expected_location = "https://theresult.com/wow".to_owned();
        let endpoint_mock = mock("POST", "/endpoint-captures-url")
            .match_header("content-type", "application/x-www-form-urlencoded")
            .with_header("content-type", "text/plain")
            .with_header("location", &expected_location)
            .with_status(200)
            .with_body("")
            .create();
        let target = format!("{}/web-page", &mockito::server_url());
        let request = super::Request {
            endpoint: format!("{}/endpoint-captures-url", mockito::server_url()),
            source: format!("{}/source", mockito::server_url()),
            target,
            vouch: None,
        };

        let result = super::send(&cfg, &request).await;
        assert_eq!(
            result.unwrap_or_default(),
            Some(super::Response {
                body: None,
                location: Some(expected_location),
                status: Status::Sent
            })
            .unwrap_or_default()
        );
        endpoint_mock.assert();
    }

    #[tokio::test]
    async fn send_fails_if_status_code_not_in_success_values() {
        let cfg = test::init();
        let endpoint_mock = mock("POST", "/endpoint-crashes")
            .match_header("content-type", "application/x-www-form-urlencoded")
            .with_header("content-type", "application/json")
            .with_status(500)
            .with_body(serde_json::json!({"error":"fake"}).to_string())
            .create();
        let target = format!("{}/web-page", &mockito::server_url());
        let request = super::Request {
            endpoint: format!("{}/endpoint-crashes", mockito::server_url()),
            source: format!("{}/source", mockito::server_url()),
            target,
            vouch: None,
        };
        let result = super::send(&cfg, &request).await;
        assert!(result.is_err());
        assert_eq!(
            result.err().unwrap(),
            Error::WebmentionUnsupportedStatusCode {
                status_code: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
                url: request.endpoint.clone()
            }
        );
        endpoint_mock.assert();
    }

    #[tokio::test]
    async fn dispatch_sends_webmentions_to_every_endpoint() {
        let cfg = test::init();
        let source_url = "https://jacky.wtf/";
        let max_count = 15;
        let webpage_link_html = std::ops::Range {
            start: 0,
            end: max_count,
        }
        .into_iter()
        .map(|i| format!("<link rel='webmention' href='/endpoint/{}' />", i))
        .collect::<Vec<String>>()
        .join("\n");
        let target_url = format!("{}/web-page", &mockito::server_url());
        let endpoint_mock = mock("POST", Matcher::Regex(r"/endpoint/\d".into()))
            .match_header("content-type", "application/x-www-form-urlencoded")
            .match_header("user-agent", Matcher::Any)
            .match_header("accept", Matcher::Any)
            .match_body(Matcher::UrlEncoded("source".into(), source_url.into()))
            .with_header("content-type", "application/json")
            .with_status(201)
            .expect(max_count)
            .create();

        let webpage_mock = mock("GET", "/web-page")
            .with_header("content-type", "text/html")
            .with_status(200)
            .with_body(format!(
                r#"
                <html>
                    <head>
                    {}
                    </head>
                </html>
        "#,
                webpage_link_html
            ))
            .expect(1)
            .create();

        let request = super::Request::default();
        let result = super::dispatch(&cfg, &source_url, &target_url, Some(request)).await;

        webpage_mock.assert();
        endpoint_mock.assert();
        assert!(result.is_ok());
        assert_eq!(
            result.unwrap(),
            std::iter::repeat(Response {
                status: Status::Accepted,
                body: None,
                location: None
            })
            .take(max_count)
            .collect::<Vec<Response>>()
        );
    }
}
