use crate::util::matchers::is_success;
use bytes::Bytes;
use claims::{assert_none, assert_ok, assert_some, assert_some_eq};
use googletest::prelude::*;
use serde_json::Value;
use std::collections::HashSet;
use std::marker::PhantomData;
use std::str::from_utf8;

use crates_io::rate_limiter::LimitedAction;
use derive_more::Deref;
use http::{StatusCode, header};

/// A type providing helper methods for working with responses
#[derive(Deref)]
#[must_use]
pub struct Response<T> {
    #[deref]
    response: hyper::Response<Bytes>,
    return_type: PhantomData<T>,
}

impl<T> Response<T>
where
    for<'de> T: serde::Deserialize<'de>,
{
    /// Asserts that the response is good and deserializes the message
    #[track_caller]
    pub fn good(self) -> T {
        assert_that!(self.status(), is_success());
        json(&self.response)
    }
}

impl<T> Response<T> {
    #[track_caller]
    pub(super) fn new(response: hyper::Response<Bytes>) -> Self {
        Self {
            response,
            return_type: PhantomData,
        }
    }

    /// Consumes the response body and converts it to a JSON value
    #[track_caller]
    pub fn json(&self) -> Value {
        json(&self.response)
    }

    #[track_caller]
    pub fn text(&self) -> String {
        let bytes = self.response.body();
        assert_ok!(from_utf8(bytes)).to_string()
    }

    #[track_caller]
    pub fn assert_redirect_ends_with(&self, target: &str) -> &Self {
        let headers = self.response.headers();
        let location = assert_some!(headers.get(header::LOCATION));
        let location = assert_ok!(location.to_str());
        assert_that!(location, ends_with(target));
        self
    }

    /// Asserts that the response carries the given `Cache-Control` header value.
    #[track_caller]
    pub fn assert_cache_control(&self, expected: &str) -> &Self {
        let value = assert_some!(self.response.headers().get(header::CACHE_CONTROL));
        assert_eq!(assert_ok!(value.to_str()), expected);
        self
    }

    /// Asserts that no `Cache-Control` header is present, i.e. the response may be
    /// freely cached by shared caches.
    #[track_caller]
    pub fn assert_no_cache_control(&self) -> &Self {
        assert_none!(self.response.headers().get(header::CACHE_CONTROL));
        self
    }

    /// Asserts that the `Vary` header lists exactly the given values, ignoring
    /// order, case, and how the values are spread across multiple header lines.
    #[track_caller]
    pub fn assert_vary(&self, expected: &[&str]) -> &Self {
        let actual = self
            .response
            .headers()
            .get_all(header::VARY)
            .iter()
            .flat_map(|value| assert_ok!(value.to_str()).split(','))
            .map(|token| token.trim().to_ascii_lowercase())
            .collect::<HashSet<_>>();

        let expected = expected
            .iter()
            .map(|token| token.to_ascii_lowercase())
            .collect::<HashSet<_>>();

        assert_eq!(actual, expected);
        self
    }

    /// Asserts that the status code is 429 and that the body matches a rate limit.
    #[track_caller]
    pub fn assert_rate_limited(self, action: LimitedAction) {
        #[derive(serde::Deserialize)]
        #[serde(deny_unknown_fields)]
        struct ErrorResponse {
            errors: Vec<ErrorDetails>,
        }
        #[derive(Debug, serde::Deserialize)]
        #[serde(deny_unknown_fields)]
        struct ErrorDetails {
            detail: String,
        }

        assert_eq!(self.status(), StatusCode::TOO_MANY_REQUESTS);

        let expected_message_start = format!("{}. Please try again after ", action.error_message());
        let error: ErrorResponse = json(&self.response);
        assert_that!(error.errors, len(eq(1)));
        assert_that!(error.errors[0].detail, starts_with(expected_message_start));
    }
}

fn json<T>(r: &hyper::Response<Bytes>) -> T
where
    for<'de> T: serde::Deserialize<'de>,
{
    fn inner(r: &hyper::Response<Bytes>) -> &Bytes {
        let headers = r.headers();

        assert_some_eq!(headers.get(header::CONTENT_TYPE), "application/json");

        let content_length = assert_some!(
            r.headers().get(header::CONTENT_LENGTH),
            "Missing content-length header"
        );
        let content_length = assert_ok!(content_length.to_str());
        let content_length: usize = assert_ok!(content_length.parse());

        let bytes = r.body();
        assert_that!(*bytes, len(eq(content_length)));

        bytes
    }

    match serde_json::from_slice(inner(r)) {
        Ok(t) => t,
        Err(e) => panic!("failed to decode: {e:?}"),
    }
}
