#![allow(dead_code)]
use actix_web::{dev::ServiceResponse, test};
use futures::Future;
use labrinth::models::pats::Scopes;

use super::{
    api_common::Api, database::USER_USER_ID_PARSED,
    environment::TestEnvironment, pats::create_test_pat,
};

// A reusable test type that works for any scope test testing an endpoint that:
// - returns a known 'expected_failure_code' if the scope is not present (defaults to 401)
// - returns a 200-299 if the scope is present
// - returns failure and success JSON bodies for requests that are 200 (for performing non-simple follow-up tests on)
// This uses a builder format, so you can chain methods to set the parameters to non-defaults (most will probably be not need to be set).
pub struct ScopeTest<'a, A> {
    test_env: &'a TestEnvironment<A>,
    // Scopes expected to fail on this test. By default, this is all scopes except the success scopes.
    // (To ensure we have isolated the scope we are testing)
    failure_scopes: Option<Scopes>,
    // User ID to use for the PATs. By default, this is the USER_USER_ID_PARSED constant.
    user_id: i64,
    // The code that is expected to be returned if the scope is not present. By default, this is 401 (Unauthorized)
    expected_failure_code: u16,
}

impl<'a, A: Api> ScopeTest<'a, A> {
    pub fn new(test_env: &'a TestEnvironment<A>) -> Self {
        Self {
            test_env,
            failure_scopes: None,
            user_id: USER_USER_ID_PARSED,
            expected_failure_code: 401,
        }
    }

    // Set non-standard failure scopes
    // If not set, it will be set to all scopes except the success scopes
    // (eg: if a combination of scopes is needed, but you want to make sure that the endpoint does not work with all-but-one of them)
    pub fn with_failure_scopes(mut self, scopes: Scopes) -> Self {
        self.failure_scopes = Some(scopes);
        self
    }

    // Set the user ID to use
    // (eg: a moderator, or friend)
    pub fn with_user_id(mut self, user_id: i64) -> Self {
        self.user_id = user_id;
        self
    }

    // If a non-401 code is expected.
    // (eg: a 404 for a hidden resource, or 200 for a resource with hidden values deeper in)
    pub fn with_failure_code(mut self, code: u16) -> Self {
        self.expected_failure_code = code;
        self
    }

    // Call the endpoint generated by req_gen twice, once with a PAT with the failure scopes, and once with the success scopes.
    // success_scopes : the scopes that we are testing that should succeed
    // returns a tuple of (failure_body, success_body)
    // Should return a String error if on unexpected status code, allowing unwrapping in tests.
    pub async fn test<T, Fut>(
        &self,
        req_gen: T,
        success_scopes: Scopes,
    ) -> Result<(serde_json::Value, serde_json::Value), String>
    where
        T: Fn(Option<String>) -> Fut,
        Fut: Future<Output = ServiceResponse>, // Ensure Fut is Send and 'static
    {
        // First, create a PAT with failure scopes
        let failure_scopes = self
            .failure_scopes
            .unwrap_or(Scopes::all() ^ success_scopes);
        let access_token_all_others =
            create_test_pat(failure_scopes, self.user_id, &self.test_env.db)
                .await;

        // Create a PAT with the success scopes
        let access_token =
            create_test_pat(success_scopes, self.user_id, &self.test_env.db)
                .await;

        // Perform test twice, once with each PAT
        // the first time, we expect a 401 (or known failure code)
        let resp = req_gen(Some(access_token_all_others.clone())).await;
        if resp.status().as_u16() != self.expected_failure_code {
            return Err(format!(
                "Expected failure code {}, got {} ({:#?})",
                self.expected_failure_code,
                resp.status().as_u16(),
                resp.response()
            ));
        }

        let failure_body = if resp.status() == 200
            && resp.headers().contains_key("Content-Type")
            && resp.headers().get("Content-Type").unwrap() == "application/json"
        {
            test::read_body_json(resp).await
        } else {
            serde_json::Value::Null
        };

        // The second time, we expect a success code
        let resp = req_gen(Some(access_token.clone())).await;
        if !(resp.status().is_success() || resp.status().is_redirection()) {
            return Err(format!(
                "Expected success code, got {} ({:#?})",
                resp.status().as_u16(),
                resp.response()
            ));
        }

        let success_body = if resp.status() == 200
            && resp.headers().contains_key("Content-Type")
            && resp.headers().get("Content-Type").unwrap() == "application/json"
        {
            test::read_body_json(resp).await
        } else {
            serde_json::Value::Null
        };
        Ok((failure_body, success_body))
    }
}
