use chrono::{Duration, Utc};
use defguard_common::db::{
    Id,
    models::{oauth2client::OAuth2Client, settings::OpenIdUsernameHandling},
};
use defguard_core::{
    enterprise::{
        db::models::openid_provider::{
            DirectorySyncTarget, DirectorySyncUserBehavior, OpenIdProviderKind,
        },
        handlers::openid_providers::AddProviderData,
        license::{License, LicenseTier, SupportType, set_cached_license},
    },
    handlers::{Auth, openid_clients::NewOpenIDClient},
};
use reqwest::{StatusCode, Url};
use serde::Deserialize;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

use super::common::{exceed_enterprise_limits, make_client, setup_pool};
use crate::api::PaginatedApiResponse;

#[derive(Deserialize)]
struct UrlResponse {
    url: String,
}

#[sqlx::test]
async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;

    let client = make_client(pool).await;

    let auth = Auth::new("admin", "pass123");
    let response = client.post("/api/v1/auth").json(&auth).send().await;
    assert_eq!(response.status(), StatusCode::OK);

    exceed_enterprise_limits(&client).await;

    let provider_data = AddProviderData {
        name: "test".to_string(),
        // FIXME: this won't work offline.
        base_url: "https://accounts.google.com".to_string(),
        kind: OpenIdProviderKind::Google,
        client_id: "client_id".to_string(),
        client_secret: "client_secret".to_string(),
        display_name: Some("display_name".to_string()),
        admin_email: None,
        google_service_account_email: None,
        google_service_account_key: None,
        directory_sync_enabled: false,
        directory_sync_interval: 100,
        directory_sync_user_behavior: DirectorySyncUserBehavior::Keep.to_string(),
        directory_sync_admin_behavior: DirectorySyncUserBehavior::Keep.to_string(),
        directory_sync_target: DirectorySyncTarget::All.to_string(),
        create_account: false,
        okta_dirsync_client_id: None,
        okta_private_jwk: None,
        directory_sync_group_match: None,
        username_handling: OpenIdUsernameHandling::PruneEmailDomain,
        jumpcloud_api_key: None,
        prefetch_users: false,
        directory_sync_user_groups: None,
    };

    let response = client
        .post("/api/v1/openid/provider")
        .json(&provider_data)
        .send()
        .await;

    assert_eq!(response.status(), StatusCode::CREATED);

    let response = client.get("/api/v1/openid/auth_info").send().await;

    assert_eq!(response.status(), StatusCode::OK);

    let provider = response.json::<UrlResponse>().await;

    let url = Url::parse(&provider.url).unwrap();

    let client_id = url
        .query_pairs()
        .find(|(key, _)| key == "client_id")
        .unwrap();
    assert_eq!(client_id.1, "client_id");

    let mut query_pairs = url.query_pairs();
    let nonce = query_pairs.clone().find(|(key, _)| key == "nonce");
    assert!(nonce.is_some());
    let state = query_pairs.clone().find(|(key, _)| key == "state");
    assert!(state.is_some());
    let redirect_uri = query_pairs.find(|(key, _)| key == "redirect_uri");
    assert!(redirect_uri.is_some());

    // Test that the endpoint is forbidden when the license is expired
    let new_license = License::new(
        "test".to_string(),
        false,
        Some(Utc::now() - Duration::days(1)),
        None,
        None,
        LicenseTier::Business,
        SupportType::Basic,
    );
    set_cached_license(Some(new_license));
    let response = client.get("/api/v1/openid/auth_info").send().await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

// FIXME: this test sometimes fails because of test_openid_providers.
// The license state is possibly preserved between those two. This requires further research.
#[sqlx::test]
async fn test_openid_login(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    let client = make_client(pool).await;
    let auth = Auth::new("admin", "pass123");
    let response = client.post("/api/v1/auth").json(&auth).send().await;
    assert_eq!(response.status(), StatusCode::OK);
    let mut url = client.base_url();
    url.push('/'); // `CoreProviderMetadata::discover_async` expects the slash.

    // Add an OpenID client
    let redirect_uri = format!("{url}/auth/callback");
    // let redirect_uri = String::from("http://localhost:8000/auth/callback");
    let openid_client = NewOpenIDClient {
        name: "Defguard".into(),
        redirect_uri: vec![redirect_uri],
        scope: vec!["openid".into(), "email".into(), "profile".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&openid_client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let response = client.get("/api/v1/oauth").send().await;
    assert_eq!(response.status(), StatusCode::OK);
    let openid_clients = response
        .json::<PaginatedApiResponse<OAuth2Client<Id>>>()
        .await
        .data;
    assert_eq!(openid_clients.len(), 1);
    let openid_client = openid_clients.first().unwrap();
    assert_eq!(openid_client.name, "Defguard");

    // Add the provider (ourselves)
    let provider_data = AddProviderData {
        name: "Custom".into(),
        base_url: url,
        kind: OpenIdProviderKind::Custom,
        client_id: openid_client.client_id.clone(),
        client_secret: openid_client.client_secret.clone(),
        display_name: Some("Defguard".to_string()),
        admin_email: None,
        google_service_account_email: None,
        google_service_account_key: None,
        directory_sync_enabled: false,
        directory_sync_interval: 100,
        directory_sync_user_behavior: DirectorySyncUserBehavior::Keep.to_string(),
        directory_sync_admin_behavior: DirectorySyncUserBehavior::Keep.to_string(),
        directory_sync_target: DirectorySyncTarget::All.to_string(),
        create_account: false,
        okta_dirsync_client_id: None,
        okta_private_jwk: None,
        directory_sync_group_match: None,
        username_handling: OpenIdUsernameHandling::PruneEmailDomain,
        jumpcloud_api_key: None,
        prefetch_users: false,
        directory_sync_user_groups: None,
    };
    let response = client
        .post("/api/v1/openid/provider")
        .json(&provider_data)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);

    // Logout to make sure we start from a clean slate
    client.post("/api/v1/auth/logout").send().await;

    // Get the provider's authorization endpoint (and button display name)
    let response = client.get("/api/v1/openid/auth_info").send().await;
    assert_eq!(
        response.status(),
        StatusCode::OK,
        "{}",
        response.text().await
    );

    #[derive(Deserialize)]
    struct AuthInfoResponse {
        button_display_name: String,
        url: Url,
    }
    let response_body = response.json::<AuthInfoResponse>().await;
    assert_eq!(response_body.button_display_name, "Defguard");

    // Begin OIDC login at the provider's authorization endpoint.
    let url = format!(
        "{}?{}",
        response_body.url.path(),
        response_body.url.query().unwrap()
    );
    let response = client.get(&url).send().await;
    assert_eq!(response.status(), StatusCode::FOUND);

    // A user should now be redirected to the login page
    // let response = client.post("/api/v1/auth").json(&auth).send().await;
    // let login_response = response.json::<UrlResponse>().await;

    // // During the flow, the user may be first redirected to a consent page, simualte that here
    // let url = Url::parse(&login_response.url).unwrap();
    // let path = url.path();
    // let query = url.query().unwrap();
    // let url = format!("{}?{}", path, query);
    // let response = client.get(&url).send().await;
    // assert_eq!(response.status(), StatusCode::FOUND);
    // let location = response.headers().get("location").unwrap();
    // let location = location.to_str().unwrap();
    // assert!(location.starts_with("/consent"));

    // // Consent to everything by adding the allow=true query parameter and sending a post request this time
    // let url = Url::parse(&login_response.url).unwrap();
    // let mut query_pairs = url
    //     .query_pairs()
    //     .into_owned()
    //     .collect::<Vec<(String, String)>>();
    // query_pairs.push(("allow".to_string(), "true".to_string()));
    // let pairs = query_pairs
    //     .iter()
    //     .map(|(key, value)| format!("{key}={value}"))
    //     .collect::<Vec<String>>()
    //     .join("&");
    // let path = format!("{}?{pairs}", url.path());
    // let response = client.post(&path).send().await;
    // assert_eq!(response.status(), StatusCode::FOUND);

    // // logout to make sure the session won't be carried over after the callback later
    // client.post("/api/v1/auth/logout").send().await;

    // // Extract callback data from the response's location header
    // let location = response.headers().get("location").unwrap();
    // let location = location.to_str().unwrap();
    // let url = Url::parse(location).unwrap();
    // let query_pairs = url
    //     .query_pairs()
    //     .into_owned()
    //     .collect::<Vec<(String, String)>>();
    // let code = query_pairs
    //     .iter()
    //     .find(|(key, _)| key == "code")
    //     .unwrap()
    //     .1
    //     .clone();
    // let state = query_pairs
    //     .iter()
    //     .find(|(key, _)| key == "state")
    //     .unwrap()
    //     .1
    //     .clone();

    // // Post the callback with the data inside a json payload
    // #[derive(Serialize)]
    // struct AuthResponse {
    //     code: String,
    //     state: String,
    // }
    // let auth_response = AuthResponse { code, state };
    // let response = client
    //     .post("/api/v1/openid/callback")
    //     .json(&auth_response)
    //     .send()
    //     .await;
    // assert_eq!(response.status(), StatusCode::OK);

    // // Am I logged in?
    // let response = client.get("/api/v1/me").send().await;
    // assert_eq!(response.status(), StatusCode::OK);
}
