#![cfg(test)]

use std::str::FromStr;
use http::header::{CONTENT_TYPE, LINK};
use miette::IntoDiagnostic;
use url::Url;

use crate::{
    http::CONTENT_TYPE_JSON,
    standards::indieauth::{Scopes, ServerMetadata},
};

use super::{ClientId, CodeChallenge};

#[tokio::test]
async fn obtain_metadata_none_found() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    assert_eq!(
        resp,
        Err(super::Error::NoMetadataEndpoint.into()),
        "expected no metadata anywhere due to lack of an endpoint"
    );

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;
    Ok(())
}

#[tokio::test]
async fn obtain_metadata_via_individual_endpoints() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .with_body(format!(
            r#"
<html>
    <head>
        <link rel="authorization_endpoint" href="{server_url}/auth" />
        <link rel="token_endpoint" href="{server_url}/token" />
    </head>
</html>
            "#
        ))
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    let metadata = resp?;
    assert_eq!(metadata.issuer, server.url().parse().unwrap());
    assert_eq!(
        metadata.authorization_endpoint,
        format!("{}/auth", server.url()).parse().unwrap()
    );
    assert_eq!(
        metadata.token_endpoint,
        format!("{}/token", server.url()).parse().unwrap()
    );
    assert_eq!(metadata.scopes_supported, super::Scopes::default());
    assert_eq!(
        metadata.code_challenge_methods_supported,
        super::ServerMetadata::recommended_code_challenge_methods()
    );

    Ok(())
}

#[tokio::test]
async fn obtain_metadata_via_individual_endpoints_headers() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let _server_url = server.url();

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(0)
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .with_header(
            LINK,
            &format!(
                r#"<{}/auth>; rel="authorization_endpoint""#,
                server.url()
            ),
        )
        .with_header(
            LINK,
            &format!(r#"<{}/token>; rel="token_endpoint""#, server.url()),
        )
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    let metadata = resp?;
    assert_eq!(metadata.issuer, server.url().parse().unwrap());
    assert_eq!(
        metadata.authorization_endpoint,
        format!("{}/auth", server.url()).parse().unwrap()
    );
    assert_eq!(
        metadata.token_endpoint,
        format!("{}/token", server.url()).parse().unwrap()
    );

    Ok(())
}

#[tokio::test]
async fn obtain_metadata_individual_endpoints_missing_auth() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .with_body(format!(
            r#"
<html>
    <head>
        <link rel="token_endpoint" href="{server_url}/token" />
    </head>
</html>
            "#
        ))
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    assert_eq!(
        resp,
        Err(super::Error::NoMetadataEndpoint.into()),
        "expected no metadata when authorization_endpoint is missing"
    );

    Ok(())
}

#[tokio::test]
async fn obtain_metadata_individual_endpoints_missing_token() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .with_body(format!(
            r#"
<html>
    <head>
        <link rel="authorization_endpoint" href="{server_url}/auth" />
    </head>
</html>
            "#
        ))
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    assert_eq!(
        resp,
        Err(super::Error::NoMetadataEndpoint.into()),
        "expected no metadata when token_endpoint is missing"
    );

    Ok(())
}

#[tokio::test]
async fn obtain_metadata_individual_endpoints_relative_urls() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let _server_url = server.url();

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .with_body(
            r#"
<html>
    <head>
        <link rel="authorization_endpoint" href="auth" />
        <link rel="token_endpoint" href="token" />
    </head>
</html>
            "#,
        )
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    let metadata = resp?;
    assert_eq!(metadata.issuer, server.url().parse().unwrap());
    assert_eq!(
        metadata.authorization_endpoint,
        format!("{}/auth", server.url()).parse().unwrap()
    );
    assert_eq!(
        metadata.token_endpoint,
        format!("{}/token", server.url()).parse().unwrap()
    );

    Ok(())
}

#[tokio::test]
async fn obtain_metadata_fallback_priority() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();
    let metadata = super::ServerMetadata {
        issuer: server.url().parse().unwrap(),
        authorization_endpoint: format!("{server_url}/endpoints/auth").parse().unwrap(),
        token_endpoint: format!("{server_url}/endpoints/token").parse().unwrap(),
        ticket_endpoint: None,
        introspection_endpoint: None,
        revocation_endpoint: None,
        code_challenge_methods_supported: super::ServerMetadata::recommended_code_challenge_methods(),
        scopes_supported: super::Scopes::minimal(),
    };

    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(1)
        .with_body(format!(
            r#"
<html>
    <head>
        <link rel="indieauth-metadata" href="{server_url}/metadata" />
        <link rel="authorization_endpoint" href="{server_url}/auth" />
        <link rel="token_endpoint" href="{server_url}/token" />
    </head>
</html>
            "#
        ))
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .expect_at_most(1)
        .create_async()
        .await;

    let metadata_endpoint_mock = server
        .mock("GET", "/metadata")
        .with_header(CONTENT_TYPE, CONTENT_TYPE_JSON)
        .with_body(serde_json::json!(metadata).to_string())
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    metadata_endpoint_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;

    // Should use metadata endpoint, not individual endpoints
    assert_eq!(resp, Ok(metadata));

    Ok(())
}


#[tokio::test]
async fn obtain_metadata_via_endpoint_headers() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();
    let var_name = ServerMetadata {
        issuer: server.url().parse().unwrap(),
        authorization_endpoint: format!("{server_url}/endpoints/auth").parse().unwrap(),
        token_endpoint: format!("{server_url}/endpoints/token").parse().unwrap(),
        ticket_endpoint: format!("{server_url}/endpoints/ticket").parse().ok(),
        introspection_endpoint: format!("{server_url}/endpoints/token").parse().ok(),
        revocation_endpoint: None,
        code_challenge_methods_supported: ServerMetadata::recommended_code_challenge_methods(),
        scopes_supported: Scopes::minimal(),
    };
    let metadata = var_name;
    let html_remote_page_mock = server
        .mock("GET", "/profile")
        .expect_at_most(0)
        .create_async()
        .await;

    let headers_remote_page_mock = server
        .mock("HEAD", "/profile")
        .with_header(
            LINK,
            &format!(
                r#"<{}/metadata>; rel="{}""#,
                server.url(),
                super::Client::<crate::http::reqwest::Client>::LINK_REL
            ),
        )
        .expect_at_most(1)
        .create_async()
        .await;

    let metadata_endpoint_mock = server
        .mock("GET", "/metadata")
        .with_header(CONTENT_TYPE, CONTENT_TYPE_JSON)
        .with_body(serde_json::json!(metadata).to_string())
        .expect_at_most(1)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let remote_url = format!("{}/profile", server.url()).parse().unwrap();
    let resp = client.obtain_metadata(&remote_url).await;

    headers_remote_page_mock.assert_async().await;
    html_remote_page_mock.assert_async().await;
    metadata_endpoint_mock.assert_async().await;
    assert_eq!(
        resp,
        Ok(metadata),
        "expected no metadata anywhere due to lack of an endpoint"
    );

    Ok(())
}

#[test]
fn server_metadata_new_authorization_request_url() -> miette::Result<()> {
    let issuer: Url = "https://example.com".parse().unwrap();
    let metadata = ServerMetadata {
        authorization_endpoint: format!("{}/auth", issuer.as_str()).parse().unwrap(),
        token_endpoint: format!("{}/token", issuer.as_str()).parse().unwrap(),
        ticket_endpoint: format!("{}/endpoints/ticket", issuer.as_str()).parse().ok(),
        introspection_endpoint: format!("{}/introspect", issuer.as_str()).parse().ok(),
        revocation_endpoint: None,
        code_challenge_methods_supported: vec!["S256".to_string()],
        scopes_supported: Scopes::minimal(),
        issuer,
    };

    let (code_challenge, code_challenge_method) =
        CodeChallenge::generate(super::CodeChallengeMethod::S256)?;
    let formed_url = metadata.new_authorization_request_url(
        super::AuthorizationRequestFields {
            client_id: ClientId::new("http://client.example.com")?,
            redirect_uri: "http://client.example.com/redirect"
                .parse::<Url>()
                .into_diagnostic()?
                .into(),
            state: "nu-state".to_string(),
            challenge: code_challenge,
            challenge_method: code_challenge_method,
            scope: Default::default(),
        },
        Default::default(),
    )?;

    assert!(
        !formed_url.query().unwrap_or_default().contains("scope="),
        "does not includes an empty scope string"
    );

    assert!(
        formed_url
            .query()
            .unwrap_or_default()
            .contains("state=nu-state"),
        "includes the provided state value"
    );

    Ok(())
}

#[test]
fn endpoint_discovery_classic_variant() {
    let auth_url: Url = "https://example.com/auth".parse().unwrap();
    let token_url: Url = "https://example.com/token".parse().unwrap();
    let ticket_url: Url = "https://example.com/ticket".parse().unwrap();

    let discovery = super::EndpointDiscovery::Classic {
        authorization: auth_url.clone(),
        token: token_url.clone(),
        ticket: Some(ticket_url.clone()),
    };

    match discovery {
        super::EndpointDiscovery::Classic {
            authorization,
            token,
            ticket,
        } => {
            assert_eq!(authorization, auth_url);
            assert_eq!(token, token_url);
            assert_eq!(ticket, Some(ticket_url));
        }
        _ => panic!("Expected Classic variant"),
    }
}

#[test]
fn endpoint_discovery_metadata_variant() {
    let metadata_url: Url = "https://example.com/.well-known/oauth-authorization-server"
        .parse()
        .unwrap();

    let discovery = super::EndpointDiscovery::Metadata {
        metadata: metadata_url.clone(),
    };

    match discovery {
        super::EndpointDiscovery::Metadata { metadata } => {
            assert_eq!(metadata, metadata_url);
        }
        _ => panic!("Expected Metadata variant"),
    }
}

#[test]
fn client_builder_with_classic_discovery() -> miette::Result<()> {
    let auth_url: Url = "https://example.com/auth".parse().unwrap();
    let token_url: Url = "https://example.com/token".parse().unwrap();

    let discovery = super::EndpointDiscovery::Classic {
        authorization: auth_url,
        token: token_url,
        ticket: None,
    };

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .discovery(discovery.clone())
        .build()?;

    assert_eq!(
        client.discovery,
        Some(discovery),
        "discovery field should be set"
    );

    Ok(())
}

#[test]
fn client_builder_with_metadata_discovery() -> miette::Result<()> {
    let metadata_url: Url = "https://example.com/.well-known/oauth-authorization-server"
        .parse()
        .unwrap();

    let discovery = super::EndpointDiscovery::Metadata {
        metadata: metadata_url,
    };

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .discovery(discovery.clone())
        .build()?;

    assert_eq!(
        client.discovery,
        Some(discovery),
        "discovery field should be set"
    );

    Ok(())
}

#[test]
fn client_builder_without_discovery() -> miette::Result<()> {
    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("http://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    assert_eq!(client.discovery, None, "discovery field should be None");

    Ok(())
}


#[tokio::test]
async fn introspect_token_active() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    
    let introspection_mock = server
        .mock("POST", "/introspect")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_header("accept", "application/json")
        .match_body("token=test_access_token")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{
            "active": true,
            "scope": "read create",
            "client_id": "https://example.com",
            "me": "https://user.example.com",
            "exp": 1735689600,
            "iat": 1735603200
        }"#)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let introspection_endpoint = format!("{}/introspect", server.url()).parse().unwrap();
    let response = client.introspect_token(&introspection_endpoint, "test_access_token").await?;

    introspection_mock.assert_async().await;

    assert_eq!(response.active, true, "token should be active");
    assert_eq!(
        response.scope,
        Some(super::Scopes::from_str("read create")?),
        "scope should match"
    );
    assert_eq!(
        response.client_id,
        Some(super::ClientId::new("https://example.com")?),
        "client_id should match"
    );
    assert_eq!(
        response.me,
        Some("https://user.example.com".parse().unwrap()),
        "me should match"
    );
    assert_eq!(response.exp, Some(1735689600), "exp should match");
    assert_eq!(response.iat, Some(1735603200), "iat should match");

    Ok(())
}

#[tokio::test]
async fn introspect_token_inactive() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    
    let introspection_mock = server
        .mock("POST", "/introspect")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_header("accept", "application/json")
        .match_body("token=invalid_token")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(r#"{"active": false}"#)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let introspection_endpoint = format!("{}/introspect", server.url()).parse().unwrap();
    let response = client.introspect_token(&introspection_endpoint, "invalid_token").await?;

    introspection_mock.assert_async().await;

    assert_eq!(response.active, false, "token should be inactive");
    assert_eq!(response.scope, None, "scope should be None");
    assert_eq!(response.client_id, None, "client_id should be None");

    Ok(())
}

#[tokio::test]
async fn revoke_token_success() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    
    let revocation_mock = server
        .mock("POST", "/revoke")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_body("token=test_token&token_type_hint=access_token")
        .with_status(200)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
    let result = client.revoke_token(
        &revocation_endpoint,
        "test_token",
        Some(super::TokenTypeHint::AccessToken)
    ).await;

    revocation_mock.assert_async().await;

    assert!(result.is_ok(), "revocation should succeed");

    Ok(())
}

#[tokio::test]
async fn revoke_token_without_hint() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    
    let revocation_mock = server
        .mock("POST", "/revoke")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_body("token=test_token")
        .with_status(200)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
    let result = client.revoke_token(
        &revocation_endpoint,
        "test_token",
        None
    ).await;

    revocation_mock.assert_async().await;

    assert!(result.is_ok(), "revocation should succeed");

    Ok(())
}

#[tokio::test]
async fn revoke_token_with_refresh_token_hint() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    
    let revocation_mock = server
        .mock("POST", "/revoke")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_body("token=refresh_token_value&token_type_hint=refresh_token")
        .with_status(200)
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
    let result = client.revoke_token(
        &revocation_endpoint,
        "refresh_token_value",
        Some(super::TokenTypeHint::RefreshToken)
    ).await;

    revocation_mock.assert_async().await;

    assert!(result.is_ok(), "revocation should succeed");

    Ok(())
}

#[tokio::test]
async fn refresh_token_success() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();
    
    let token_mock = server
        .mock("POST", "/token")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_header("accept", "application/json")
        .match_body("grant_type=refresh_token&refresh_token=test_refresh_token&client_id=https://example.com")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(format!(r#"{{
            "token_type": "Bearer",
            "access_token": "new_access_token",
            "scope": "read create",
            "me": "{}/user",
            "expires_in": 3600
        }}"#, server_url))
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let token_endpoint = format!("{}/token", server.url()).parse().unwrap();
    let refresh_fields = super::RefreshTokenFields {
        refresh_token: "test_refresh_token".to_string(),
        client_id: super::ClientId::new("https://example.com")?,
        scope: None,
    };

    let response = client.refresh_token::<()>(
        &token_endpoint,
        refresh_fields
    ).await?;

    token_mock.assert_async().await;

    match response {
        super::RedemptionResponse::Claim(claim) => {
            assert_eq!(claim.access_token, "new_access_token", "access_token should match");
            assert_eq!(
                claim.scope,
                super::Scopes::from_str("read create")?,
                "scope should match"
            );
            assert_eq!(
                claim.me,
                format!("{}/user", server_url).parse().unwrap(),
                "me should match"
            );
            assert_eq!(claim.expires_in, 3600, "expires_in should match");
        }
        super::RedemptionResponse::Error(err) => {
            panic!("Expected successful response, got error: {:?}", err);
        }
    }

    Ok(())
}

#[tokio::test]
async fn refresh_token_with_scope_restriction() -> miette::Result<()> {
    let mut server = mockito::Server::new_async().await;
    let server_url = server.url();
    
    let token_mock = server
        .mock("POST", "/token")
        .match_header("content-type", "application/x-www-form-urlencoded")
        .match_header("accept", "application/json")
        .match_body("grant_type=refresh_token&refresh_token=test_refresh_token&client_id=https://example.com&scope=read")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(format!(r#"{{
            "token_type": "Bearer",
            "access_token": "new_access_token",
            "scope": "read",
            "me": "{}/user",
            "expires_in": 3600
        }}"#, server_url))
        .create_async()
        .await;

    let client = super::Client::<crate::http::reqwest::Client>::builder()
        .id("https://example.com")
        .client(crate::http::reqwest::Client::default())
        .build()?;

    let token_endpoint = format!("{}/token", server.url()).parse().unwrap();
    let refresh_fields = super::RefreshTokenFields {
        refresh_token: "test_refresh_token".to_string(),
        client_id: super::ClientId::new("https://example.com")?,
        scope: Some(super::Scopes::from_str("read")?),
    };

    let response = client.refresh_token::<()>(
        &token_endpoint,
        refresh_fields
    ).await?;

    token_mock.assert_async().await;

    match response {
        super::RedemptionResponse::Claim(claim) => {
            assert_eq!(
                claim.scope,
                super::Scopes::from_str("read")?,
                "scope should be restricted to 'read'"
            );
        }
        super::RedemptionResponse::Error(err) => {
            panic!("Expected successful response, got error: {:?}", err);
        }
    }

    Ok(())
}

#[test]
fn token_type_hint_serialization() {
    assert_eq!(
        super::TokenTypeHint::AccessToken.to_string(),
        "access_token"
    );
    assert_eq!(
        super::TokenTypeHint::RefreshToken.to_string(),
        "refresh_token"
    );
}

#[test]
fn refresh_token_fields_query_parameters() -> miette::Result<()> {
    let fields = super::RefreshTokenFields {
        refresh_token: "test_token".to_string(),
        client_id: super::ClientId::new("https://example.com")?,
        scope: Some(super::Scopes::from_str("read create")?),
    };

    let params = fields.into_query_parameters();

    assert_eq!(params.len(), 4, "should have 4 parameters");
    assert!(params.contains(&("grant_type".to_string(), "refresh_token".to_string())));
    assert!(params.contains(&("refresh_token".to_string(), "test_token".to_string())));
    assert!(params.contains(&("client_id".to_string(), "https://example.com".to_string())));
    assert!(params.contains(&("scope".to_string(), "read create".to_string())));

    Ok(())
}

#[test]
fn refresh_token_fields_query_parameters_without_scope() -> miette::Result<()> {
    let fields = super::RefreshTokenFields {
        refresh_token: "test_token".to_string(),
        client_id: super::ClientId::new("https://example.com")?,
        scope: None,
    };

    let params = fields.into_query_parameters();

    assert_eq!(params.len(), 3, "should have 3 parameters (no scope)");
    assert!(params.contains(&("grant_type".to_string(), "refresh_token".to_string())));
    assert!(params.contains(&("refresh_token".to_string(), "test_token".to_string())));
    assert!(params.contains(&("client_id".to_string(), "https://example.com".to_string())));

    Ok(())
}
