#![cfg(test)]

use http::header::CONTENT_TYPE;
use oauth2::Scope;
use std::str::FromStr;
use time::{Duration, OffsetDateTime};
use url::Url;

use crate::{
    http::CONTENT_TYPE_JSON,
    standards::indieauth::{ensure_discovery, find_metadata, Scopes, VerifiedToken},
};

#[test]
fn scopes_serde() {
    assert_eq!(
        Ok(Scopes(vec![
            Scope::new("read".into()),
            Scope::new("profile".into())
        ])),
        serde_json::from_str(r#""read profile""#).map_err(|e| e.to_string())
    );

    assert_eq!(
        Ok(Scopes(vec![
            Scope::new("read".into()),
            Scope::new("profile".into())
        ])),
        serde_json::from_str(r#"["read", "profile"]"#).map_err(|e| e.to_string())
    );

    #[derive(serde::Deserialize, Debug, PartialEq, Eq)]
    struct K {
        scope: Scopes,
    }

    assert_eq!(
        Ok(K {
            scope: Scopes(vec![
                Scope::new("read".into()),
                Scope::new("profile".into())
            ])
        }),
        serde_qs::from_str("scope=read+profile").map_err(|e| e.to_string())
    );

    assert_eq!(
        Ok(K {
            scope: Scopes(vec![
                Scope::new("read".into()),
                Scope::new("profile".into())
            ])
        }),
        serde_qs::from_str("scope[]=read&scope[]=profile").map_err(|e| e.to_string())
    );
}

#[tokio::test]
async fn find_metadata_minimal() {
    let mut client = crate::test::Client::new().await;

    let metadata_json = serde_json::json!({
        "issuer": "https://issuer.com",
        "authorization_endpoint": "https://endpoint.com/auth",
        "token_endpoint": "https://endpoint.com/token"
    });
    let metadata_endpoint_url: Url = format!(
        "{}/.well-known/oauth-metadata/minimal",
        client.mock_server.url()
    )
    .parse()
    .unwrap();
    let metadata_endpoint_url_mock = client
        .mock_server
        .mock("GET", metadata_endpoint_url.path())
        .match_header("accept", "application/json")
        .with_header("content-type", "application/json")
        .with_status(200)
        .with_body(metadata_json.to_string())
        .expect(1)
        .create_async()
        .await;

    let result = find_metadata(&client, &metadata_endpoint_url).await;

    assert_eq!(
        result.as_ref().map(|c| c.is_some()),
        Ok(true),
        "finds minimal metadata endpoint info"
    );

    metadata_endpoint_url_mock.assert_async().await;
}

#[tokio::test]
async fn find_metadata_complete() {
    let mut client = crate::test::Client::new().await;

    let metadata_json = serde_json::json!({
        "issuer": "https://issuer.com",
        "authorization_endpoint": "https://endpoint.com/auth",
        "token_endpoint": "https://endpoint.com/token",
        "introspection_endpoint": "https://endpoint.com/introspect",
        "introspection_endpoint_auth_methods_supported": [],
        "revocation_endpoint": "https://endpoint.com/revoke",
        "registration_endpoint": "https://endpoint.com/register",
        "jwks_uri": "https://endpoint.com/key.jwt.pub",
        "revocation_endpoint_auth_methods_supported": ["none"],
        "response_modes_supported": ["form_post", "query", "fragment"],
        "scopes_supported": ["read", "profile"],
        "response_types_supported": ["code", "token"],
        "grant_types_supported": [],
        "service_documentation": "https://indieweb.org/Sele",
        "code_challenge_methods_supported": ["S256"],
        "authorization_response_iss_parameter_supported": true,
        "userinfo_endpoint": "https://endpoint.com/userinfo",
        "ui_locales_supported": ["en-US"]
    });
    let metadata_endpoint_url: Url = format!(
        "{}/.well-known/oauth-metadata/recommended",
        client.mock_server.url()
    )
    .parse()
    .unwrap();
    let metadata_endpoint_url_mock = client
        .mock_server
        .mock("GET", metadata_endpoint_url.path())
        .match_header("accept", "application/json")
        .with_header("content-type", "application/json")
        .with_status(200)
        .with_body(metadata_json.to_string())
        .expect(1)
        .create_async()
        .await;

    let result = find_metadata(&client, &metadata_endpoint_url).await;

    assert_eq!(
        result.as_ref().map(|c| c.is_some()),
        Ok(true),
        "finds recommended metadata endpoint info"
    );

    metadata_endpoint_url_mock.assert_async().await;
}

#[tokio::test]
async fn ensure_discovery_works() {
    let mut client = crate::test::Client::new().await;
    let mock_profile = client
        .mock_server
        .mock("GET", "/profile")
        .with_body(
            r#"
            <html>
                <head>
                    <link rel="indieauth-metadata" href="/metadata-discovery-ensure" />
                </head>
            </html>
            "#,
        )
        .expect(1)
        .create_async()
        .await;
    let mock_endpoint = client
        .mock_server
        .mock("GET", "/metadata-discovery-ensure")
        .with_header(CONTENT_TYPE, CONTENT_TYPE_JSON)
        .with_body(
            serde_json::json!({
                "issuer": format!("{}/app", client.mock_server.url()),
                "authorization_endpoint": format!("{}/endpoints/auth", client.mock_server.url()),
                "token_endpoint": format!("{}/endpoints/auth", client.mock_server.url()),
            })
            .to_string(),
        )
        .expect(1)
        .create_async()
        .await;
    let result = ensure_discovery(
        &client,
        &format!("{}/profile", client.mock_server.url())
            .parse()
            .unwrap(),
        &format!("{}/app", client.mock_server.url()).parse().unwrap(),
    )
    .await;
    mock_profile.assert_async().await;
    mock_endpoint.assert_async().await;

    assert_eq!(
        result.as_ref().err(),
        None,
        "finds no errors in the strict discovery of metadata"
    );
}

#[test]
fn verified_token() {
    let issued_at = OffsetDateTime::now_utc() - Duration::minutes(10);
    let expires_at = OffsetDateTime::now_utc() + Duration::days(8);
    assert_eq!(
        Some(serde_json::json!({
            "active": true,
            "me": "http://me.com/",
            "client_id": "http://client.id/",
            "scope": "read profile",
            "exp": expires_at.unix_timestamp(),
            "iat": issued_at.unix_timestamp()
        })),
        serde_json::to_value(VerifiedToken {
            active: true,
            me: "http://me.com".parse().unwrap(),
            client_id: "http://client.id".parse().unwrap(),
            scopes: Scopes::from_str("read profile").unwrap(),
            expires_at,
            issued_at
        })
        .ok()
    );

    assert_eq!(
        Some(serde_json::json!(VerifiedToken {
            active: true,
            me: "http://me.com".parse().unwrap(),
            client_id: "http://client.id".parse().unwrap(),
            scopes: Scopes::from_str("read profile").unwrap(),
            expires_at,
            issued_at
        })),
        serde_json::from_value(serde_json::json!({
            "active": true,
            "me": "http://me.com/",
            "client_id": "http://client.id/",
            "scope": "read profile",
            "exp": expires_at.unix_timestamp(),
            "iat": issued_at.unix_timestamp()
        }))
        .ok()
    );
}
