pub mod auth;
pub mod error;
pub mod request;
pub mod response;
pub mod traits;

mod util;

pub use crate::error::{Error, ErrorCode};
pub use crate::request::Request;
pub use crate::response::Response;

use http::header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use http::{Method, Uri};
use oauth_credentials::Token;
use serde::de;

use crate::response::ResponseFuture;
use crate::traits::HttpService;

#[derive(Clone, Copy, Debug)]
pub struct RateLimit {
    pub limit: u64,
    pub remaining: u64,
    pub reset: u64,
}

pub trait Request: oauth::Request {
    type Data: de::DeserializeOwned;

    const METHOD: Method;
    const URI: &'static str;

    fn send<C, T, S, B>(
        &self,
        token: &Token<C, T>,
        http: &mut S,
    ) -> ResponseFuture<Self::Data, S::Future>
    where
        C: AsRef<str>,
        T: AsRef<str>,
        S: HttpService<B>,
        B: From<Vec<u8>>,
    {
        let req = prepare_request(&Self::METHOD, Self::URI, self, token.as_ref());

        ResponseFuture::new(req.map(Into::into), http)
    }
}

impl RateLimit {
    fn from_headers(headers: &http::HeaderMap) -> Option<Self> {
        pub const RATE_LIMIT_LIMIT: &str = "x-rate-limit-limit";
        pub const RATE_LIMIT_REMAINING: &str = "x-rate-limit-remaining";
        pub const RATE_LIMIT_RESET: &str = "x-rate-limit-reset";

        fn header(headers: &http::HeaderMap, name: &str) -> Option<u64> {
            headers
                .get(name)
                .and_then(|value| atoi::atoi(value.as_bytes()))
        }

        Some(RateLimit {
            limit: header(headers, RATE_LIMIT_LIMIT)?,
            remaining: header(headers, RATE_LIMIT_REMAINING)?,
            reset: header(headers, RATE_LIMIT_RESET)?,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::convert::Infallible;

    use hyper::Body;
    use oauth_credentials::Token;
    use tower::ServiceExt;

    use crate::request::RawRequest;

    use super::*;

    #[tokio::test]
    async fn parse_errors() {
        #[derive(oauth::Request)]
        struct Foo {
            param: u32,
        }

        impl RawRequest for Foo {
            fn method(&self) -> &http::Method {
                &http::Method::GET
            }

            fn uri(&self) -> &'static str {
                "https://api.twitter.com/1.1/test/foo.json"
            }
        }

        let token = Token::from_parts("", "", "", "");

        let (http, mut handle) = tower_test::mock::pair::<http::Request<Vec<u8>>, _>();
        let mut http = http.map_err(|e| -> Infallible { panic!("{:?}", e) });

        let res_fut =
            tokio::spawn(Foo { param: 42 }.send_raw(&token, http.ready_and().await.unwrap()));

        let (_req, tx) = handle.next_request().await.unwrap();
        let payload = br#"{"errors":[{"code":104,"message":"You aren't allowed to add members to this list."}]}"#;
        let res = http::Response::builder()
            .status(http::StatusCode::FORBIDDEN)
            .body(Body::from(&payload[..]))
            .unwrap();
        tx.send_response(res);

        match res_fut.await.unwrap().unwrap_err() {
            Error::Twitter(crate::error::TwitterErrors {
                status: http::StatusCode::FORBIDDEN,
                errors,
                rate_limit: None,
            }) => {
                assert_eq!(errors.len(), 1);
                assert_eq!(errors[0].code, 104);
                assert_eq!(
                    errors[0].message,
                    "You aren't allowed to add members to this list.",
                );
            }
            e => panic!("{:?}", e),
        };
    }
}