use std::borrow::Cow;

use defguard_common::db::{
    Id,
    models::{
        OAuth2AuthorizedApp,
        oauth2client::{OAuth2Client, OAuth2ClientSafe},
    },
};
use defguard_core::handlers::{Auth, openid_clients::NewOpenIDClient};
use reqwest::{StatusCode, Url, header::CONTENT_TYPE};
use serde_json::json;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

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

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

    let (client, pool) = make_client_with_db(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);

    // create OAuth2 client
    let oauth2client = NewOpenIDClient {
        name: "My test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let oauth_client: OAuth2Client<Id> = response.json().await;

    // authorize client for test user
    OAuth2AuthorizedApp::new(1, oauth_client.id)
        .save(&pool)
        .await
        .unwrap();

    // wrong response type
    let response = client
        .get(
            "/api/v1/oauth/authorize?\
            response_type=wrong&\
            client_id=MyClient&\
            redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&\
            scope=default-scope&\
            state=ABCDEF",
        )
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // error response
    let response = client
        .get(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id=MyClient&\
            redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&\
            scope=openid&\
            state=ABCDEF",
        )
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    assert_eq!(redirect_url.domain().unwrap(), "localhost");
    let mut pairs = redirect_url.query_pairs();
    assert_eq!(pairs.count(), 2);
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("error"), Cow::Borrowed("unauthorized_client")))
    );
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("state"), Cow::Borrowed("ABCDEF")))
    );

    // error response without state
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Flocalhost%3A3000%2F&\
            scope=invalid",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    assert_eq!(redirect_url.domain().unwrap(), "localhost");
    let mut pairs = redirect_url.query_pairs();
    assert_eq!(pairs.count(), 1);
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("error"), Cow::Borrowed("invalid_scope")))
    );

    // successful response
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=ABCDEF",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    assert_eq!(redirect_url.domain().unwrap(), "test.server.tnt");
    let mut pairs = redirect_url.query_pairs();
    assert_eq!(pairs.count(), 2);
    assert_eq!(pairs.next().unwrap().0, Cow::Borrowed("code"),);
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("state"), Cow::Borrowed("ABCDEF")))
    );
}

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

    let (client, _) = make_client_with_db(pool).await;

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

    // add app
    let oauth2client = NewOpenIDClient {
        name: "My test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);

    // list apps
    let response = client.get("/api/v1/oauth").send().await;
    assert_eq!(response.status(), StatusCode::OK);
    let apps = response
        .json::<PaginatedApiResponse<OAuth2Client<Id>>>()
        .await
        .data;
    assert_eq!(apps.len(), 1);
    let test_app = &apps[0];
    assert_eq!(test_app.name, oauth2client.name);

    // fetch app details
    let response = client
        .get(format!("/api/v1/oauth/{}", test_app.client_id))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);
    let app: OAuth2Client<Id> = response.json().await;
    assert_eq!(app.name, oauth2client.name);

    // edit app
    let oauth2client = NewOpenIDClient {
        name: "Changed test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid email".into()],
        enabled: true,
    };
    let response = client
        .put(format!("/api/v1/oauth/{}", test_app.client_id))
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);

    // change app state
    let data = json!(
        {"enabled": false}
    );
    let response = client
        .post(format!("/api/v1/oauth/{}", test_app.client_id))
        .json(&data)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);

    // fetch changed app details
    let response = client
        .get(format!("/api/v1/oauth/{}", test_app.client_id))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);
    let app: OAuth2Client<Id> = response.json().await;
    assert_eq!(app.name, oauth2client.name);
    assert!(!app.enabled);

    // delete app
    let response = client
        .delete(format!("/api/v1/oauth/{}", test_app.client_id))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);

    // list apps
    let response = client.get("/api/v1/oauth").send().await;
    assert_eq!(response.status(), StatusCode::OK);
    let apps = response
        .json::<PaginatedApiResponse<OAuth2Client<Id>>>()
        .await
        .data;
    assert_eq!(apps.len(), 0);

    // add another app for further testing
    let oauth2client = NewOpenIDClient {
        name: "New test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid phone".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let response = client.get("/api/v1/oauth").send().await;
    assert_eq!(response.status(), StatusCode::OK);
    let apps = response
        .json::<PaginatedApiResponse<OAuth2Client<Id>>>()
        .await
        .data;
    let test_app = &apps[0];

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

    // standard user cannot list apps
    let response = client.get("/api/v1/oauth").send().await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);

    // standard user cannot get sensitive app details
    let response = client
        .get(format!("/api/v1/oauth/{}", test_app.client_id))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);
    let _response_details: OAuth2ClientSafe = response.json().await;

    // standard user cannot add apps
    let oauth2client = NewOpenIDClient {
        name: "Another test client".into(),
        redirect_uri: vec!["http://test.com/redirect".into()],
        scope: vec!["openid profile".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);

    // standard user cannot edit apps
    let response = client
        .put(format!("/api/v1/oauth/{}", test_app.client_id))
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);

    // standard user cannot change app status
    let data = json!(
        {"enabled": false}
    );
    let response = client
        .post(format!("/api/v1/oauth/{}", test_app.client_id))
        .json(&data)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);

    // standard user cannot delete apps
    let response = client
        .delete(format!("/api/v1/oauth/{}", test_app.client_id))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

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

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

    // Create an OAuth2 client.
    let oauth2client = NewOpenIDClient {
        name: "Consent test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let oauth_client: OAuth2Client<Id> = response.json().await;

    // User consents via the POST /authorize handler (secure_authorization).
    let response = client
        .post(format!(
            "/api/v1/oauth/authorize?\
            allow=true&\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=ABCDEF",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);

    // Extract the authorization code and verify state is echoed back.
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    assert_eq!(redirect_url.domain().unwrap(), "test.server.tnt");
    let code = redirect_url
        .query_pairs()
        .find(|(k, _)| k == "code")
        .unwrap()
        .1
        .into_owned();
    assert!(
        redirect_url
            .query_pairs()
            .any(|(k, v)| k == "state" && v == "ABCDEF")
    );

    // Exchange the authorization code for a token.
    // Credentials are passed as form fields; the token endpoint accepts either
    // Basic auth or client_id/client_secret in the body.
    let response = client
        .post("/api/v1/oauth/token")
        .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
        .body(format!(
            "grant_type=authorization_code&\
            code={code}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            client_id={}&\
            client_secret={}",
            oauth_client.client_id, oauth_client.client_secret
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::OK);
}

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

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

    // POST with a non-existent client_id. The handler cannot validate the
    // redirect_uri (is_redirect_allowed stays false), so it redirects to the
    // defguard base URL with error=unauthorized_client instead of the provided
    // redirect_uri. This prevents open redirects (DG25-17).
    let response = client
        .post(
            "/api/v1/oauth/authorize?\
            allow=true&\
            response_type=code&\
            client_id=NonExistentClient&\
            redirect_uri=http%3A%2F%2Fattacker.example.com%2F&\
            scope=openid&\
            state=ABCDEF",
        )
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    // Must NOT redirect to the caller-supplied redirect_uri.
    assert_ne!(redirect_url.domain().unwrap(), "attacker.example.com");
    let mut pairs = redirect_url.query_pairs();
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("error"), Cow::Borrowed("unauthorized_client")))
    );
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("state"), Cow::Borrowed("ABCDEF")))
    );
}

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

    let (client, _) = make_client_with_db(pool).await;

    let response = client
        .post("/api/v1/oauth/token")
        .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
        .body("client_id=WrongClient&client_secret=WrongSecret&grant_type=code")
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

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

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

    // Create an OAuth2 client.
    let oauth2client = NewOpenIDClient {
        name: "State test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let oauth_client: OAuth2Client<Id> = response.json().await;

    // Pre-authorise the app for the admin user (id=1).
    OAuth2AuthorizedApp::new(1, oauth_client.id)
        .save(&pool)
        .await
        .unwrap();

    // A numeric-only state value (e.g. "123456") must be accepted: all digits are
    // within VSCHAR (%x30-39) so the backend should echo the state back in the redirect.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=123456",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let redirect_url = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    // Redirect must carry the auth code and echo the state back unchanged.
    assert!(
        redirect_url
            .query_pairs()
            .any(|(k, v)| k == "state" && v == "123456")
    );

    // VSCHAR boundary: space (0x20) is the lowest valid character - must be accepted.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=%20",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    assert!(
        Url::parse(
            response
                .headers()
                .get("Location")
                .unwrap()
                .to_str()
                .unwrap(),
        )
        .unwrap()
        .query_pairs()
        .any(|(k, v)| k == "state" && v == " ")
    );

    // VSCHAR boundary: tilde (0x7E) is the highest valid character - must be accepted.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=~",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    assert!(
        Url::parse(
            response
                .headers()
                .get("Location")
                .unwrap()
                .to_str()
                .unwrap(),
        )
        .unwrap()
        .query_pairs()
        .any(|(k, v)| k == "state" && v == "~")
    );

    // VSCHAR boundary: DEL (0x7F) is one above the valid range - must be rejected with 400.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=%7F",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // VSCHAR boundary: US (0x1F) is one below the valid range - must be rejected with 400.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=%1F",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // An empty state (state= present but empty) must be rejected with 400.
    // RFC 6749 Appendix A.5 requires 1*VSCHAR - at least one character.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // A state containing bytes outside VSCHAR (%x20-7E) must be rejected with 400.
    // The raw bytes \xEE\xFF\x02\x03 are percent-encoded below.
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=%EE%FF%02%03",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

/// Regression test for DG26-6: scope parameter must be validated per individual element, not as a
/// whole. Sending scope=openid%20email when the client only allows ["openid"] must be rejected
/// with invalid_scope.
#[sqlx::test]
async fn dg26_6_test_authorize_scope_validation(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    let (client, pool) = make_client_with_db(pool).await;

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

    // create client with only "openid" scope
    let oauth2client = NewOpenIDClient {
        name: "Scope test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let oauth2client: OAuth2Client<Id> = response.json().await;

    // authorize client for the test user so session-based redirect returns a code, not consent
    OAuth2AuthorizedApp::new(1, oauth2client.id)
        .save(&pool)
        .await
        .unwrap();

    // valid request: scope=openid (the only allowed scope) - must succeed
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            state=valid",
            oauth2client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let location = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    assert_eq!(location.domain().unwrap(), "test.server.tnt");
    assert!(location.query().unwrap().contains("code="));

    // forbidden single scope: scope=email - must be rejected
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=email&\
            state=forbidden_single",
            oauth2client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let location = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    let mut pairs = location.query_pairs();
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("error"), Cow::Borrowed("invalid_scope")))
    );
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("state"), Cow::Borrowed("forbidden_single")))
    );

    // mixed scope=openid%20email - second token must not be accepted
    let response = client
        .get(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid%20email&\
            state=mixed",
            oauth2client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::FOUND);
    let location = Url::parse(
        response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap(),
    )
    .unwrap();
    let mut pairs = location.query_pairs();
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("error"), Cow::Borrowed("invalid_scope")))
    );
    assert_eq!(
        pairs.next(),
        Some((Cow::Borrowed("state"), Cow::Borrowed("mixed")))
    );
}

#[sqlx::test]
async fn dg26_7_test_state_parameter_secure_authorization(
    _: PgPoolOptions,
    options: PgConnectOptions,
) {
    let pool = setup_pool(options).await;
    let (client, pool) = make_client_with_db(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);

    // Create an OAuth2 client
    let oauth2client = NewOpenIDClient {
        name: "State POST test client".into(),
        redirect_uri: vec!["http://test.server.tnt:12345/".into()],
        scope: vec!["openid".into()],
        enabled: true,
    };
    let response = client
        .post("/api/v1/oauth")
        .json(&oauth2client)
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::CREATED);
    let oauth_client: OAuth2Client<Id> = response.json().await;

    // Pre-authorise app
    OAuth2AuthorizedApp::new(1, oauth_client.id)
        .save(&pool)
        .await
        .unwrap();

    // Non-VSCHAR state on POST must be rejected with 400
    let response = client
        .post(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            allow=true&\
            state=%EE%FF%02%03",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);

    // Empty state on POST must also be rejected with 400
    let response = client
        .post(format!(
            "/api/v1/oauth/authorize?\
            response_type=code&\
            client_id={}&\
            redirect_uri=http%3A%2F%2Ftest.server.tnt%3A12345%2F&\
            scope=openid&\
            allow=true&\
            state=",
            oauth_client.client_id
        ))
        .send()
        .await;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
