use std::{borrow::Cow, marker::PhantomData};

use assert_json_diff::{assert_json_eq, assert_json_include};
use serde::Serialize;
use serde_json::{json, Value};
use utoipa::{
    openapi::{RefOr, Response, ResponseBuilder},
    OpenApi, ToResponse,
};
use utoipa_gen::ToSchema;

mod common;

#[test]
fn derive_openapi_with_security_requirement() {
    #[derive(Default, OpenApi)]
    #[openapi(security(
            (),
            ("my_auth" = ["read:items", "edit:items"]),
            ("token_jwt" = []),
            ("api_key1" = [], "api_key2" = []),
        ))]
    struct ApiDoc;

    let doc_value = serde_json::to_value(&ApiDoc::openapi()).unwrap();

    assert_value! {doc_value=>
        "security.[0]" = "{}", "Optional security requirement"
        "security.[1].my_auth.[0]" = r###""read:items""###, "api_oauth first scope"
        "security.[1].my_auth.[1]" = r###""edit:items""###, "api_oauth second scope"
        "security.[2].token_jwt" = "[]", "jwt_token auth scopes"
        "security.[3].api_key1" = "[]", "api_key1 auth scopes"
        "security.[3].api_key2" = "[]", "api_key2 auth scopes"
    }
}

#[test]
fn derive_openapi_tags() {
    #[derive(OpenApi)]
    #[openapi(tags(
        (name = "random::api", description = "this is random api description"),
        (name = "pets::api", description = "api all about pets", external_docs(
            url = "http://localhost", description = "Find more about pets")
        )
    ))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

    assert_value! {doc=>
        "tags.[0].name" = r###""random::api""###, "Tags random_api name"
        "tags.[0].description" = r###""this is random api description""###, "Tags random_api description"
        "tags.[0].externalDocs" = r###"null"###, "Tags random_api external docs"
        "tags.[1].name" = r###""pets::api""###, "Tags pets_api name"
        "tags.[1].description" = r###""api all about pets""###, "Tags pets_api description"
        "tags.[1].externalDocs.url" = r###""http://localhost""###, "Tags pets_api external docs url"
        "tags.[1].externalDocs.description" = r###""Find more about pets""###, "Tags pets_api external docs description"
    }
}

#[test]
fn derive_openapi_tags_include_str() {
    #[derive(OpenApi)]
    #[openapi(tags(
        (name = "random::api", description = include_str!("testdata/openapi-derive-info-description")),
    ))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

    assert_value! {doc=>
        "tags.[0].name" = r###""random::api""###, "Tags random_api name"
        "tags.[0].description" = r###""this is include description\n""###, "Tags random_api description"
    }
}

#[test]
fn derive_openapi_with_external_docs() {
    #[derive(OpenApi)]
    #[openapi(external_docs(
        url = "http://localhost.more.about.api",
        description = "Find out more"
    ))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

    assert_value! {doc=>
        "externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
        "externalDocs.description" = r###""Find out more""###, "External docs description"
    }
}

#[test]
fn derive_openapi_with_external_docs_only_url() {
    #[derive(OpenApi)]
    #[openapi(external_docs(url = "http://localhost.more.about.api"))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

    assert_value! {doc=>
        "externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
        "externalDocs.description" = r###"null"###, "External docs description"
    }
}

#[test]
fn derive_openapi_with_components_in_different_module() {
    mod custom {
        use utoipa::ToSchema;

        #[derive(ToSchema)]
        #[allow(unused)]
        pub(super) struct Todo {
            name: String,
        }
    }

    #[derive(OpenApi)]
    #[openapi(components(schemas(custom::Todo)))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
    let todo = doc.pointer("/components/schemas/Todo").unwrap();

    assert_ne!(
        todo,
        &Value::Null,
        "Expected components.schemas.Todo not to be null"
    );
}

#[test]
fn derive_openapi_with_responses() {
    #[allow(unused)]
    struct MyResponse;

    impl<'r> ToResponse<'r> for MyResponse {
        fn response() -> (&'r str, RefOr<Response>) {
            (
                "MyResponse",
                ResponseBuilder::new().description("Ok").build().into(),
            )
        }
    }

    #[derive(OpenApi)]
    #[openapi(components(responses(MyResponse)))]
    struct ApiDoc;

    let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
    let responses = doc.pointer("/components/responses").unwrap();

    assert_json_eq!(
        responses,
        json!({
            "MyResponse": {
                "description": "Ok"
            },
        })
    )
}

#[test]
fn derive_openapi_with_servers() {
    #[derive(OpenApi)]
    #[openapi(
        servers(
            (url = "http://localhost:8989", description = "this is description"),
            (url = "http://api.{username}:{port}", description = "remote api", 
                variables(
                    ("username" = (default = "demo", description = "Default username for API")),
                    ("port" = (default = "8080", enum_values("8080", "5000", "3030"), description = "Supported ports for the API"))
                )
            )
        )
    )]
    struct ApiDoc;

    let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
    let servers = value.pointer("/servers");

    assert_json_eq!(
        servers,
        json!([
            {
                "description": "this is description",
                "url": "http://localhost:8989"
            },
            {
                "description": "remote api",
                "url": "http://api.{username}:{port}",
                "variables": {
                    "port": {
                        "default": "8080",
                        "enum": [
                            "8080",
                            "5000",
                            "3030"
                        ],
                        "description": "Supported ports for the API"
                    },
                    "username": {
                        "default": "demo",
                        "description": "Default username for API"
                    }
                }
            }
        ])
    )
}

#[test]
fn derive_openapi_with_custom_info() {
    #[derive(OpenApi)]
    #[openapi(info(
        title = "title override",
        description = "description override",
        version = "1.0.0",
        contact(name = "Test")
    ))]
    struct ApiDoc;

    let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
    let info = value.pointer("/info");

    assert_json_include!(
        actual: info,
        expected:
            json!(
                {
                    "title": "title override",
                    "description": "description override",
                    "license": {
                        "name": "MIT OR Apache-2.0",
                    },
                    "contact": {
                        "name": "Test"
                    },
                    "version": "1.0.0",
                }
            )
    )
}

#[test]
fn derive_openapi_with_include_str_description() {
    #[derive(OpenApi)]
    #[openapi(info(
        title = "title override",
        description = include_str!("./testdata/openapi-derive-info-description"),
        contact(name = "Test")
    ))]
    struct ApiDoc;

    let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
    let info = value.pointer("/info");

    assert_json_include!(
        actual: info,
        expected:
            json!(
            {
                "title": "title override",
                "description": "this is include description\n",
                "license": {
                    "name": "MIT OR Apache-2.0",
                },
                "contact": {
                    "name": "Test"
                }
            }
            )
    )
}

#[test]
fn derive_openapi_with_generic_response() {
    struct Resp;

    #[derive(Serialize, ToResponse)]
    struct Response<'a, Resp> {
        #[serde(skip)]
        _p: PhantomData<Resp>,
        value: Cow<'a, str>,
    }

    #[derive(OpenApi)]
    #[openapi(components(responses(Response<Resp>)))]
    struct ApiDoc;

    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
    let response = doc.pointer("/components/responses/Response");

    assert_json_eq!(
        response,
        json!({
            "content": {
                "application/json": {
                    "schema": {
                        "properties": {
                            "value": {
                                "type": "string"
                            }
                        },
                        "required": ["value"],
                        "type": "object"
                    }
                }
            },
            "description": ""
        })
    )
}

#[test]
fn derive_openapi_with_generic_schema() {
    struct Value;

    #[derive(Serialize, ToSchema)]
    struct Pet<'a, Resp> {
        #[serde(skip)]
        _p: PhantomData<Resp>,
        value: Cow<'a, str>,
    }

    #[derive(OpenApi)]
    #[openapi(components(schemas(Pet<Value>)))]
    struct ApiDoc;

    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
    let schema = doc.pointer("/components/schemas/Pet");

    assert_json_eq!(
        schema,
        json!({
            "properties": {
                "value": {
                    "type": "string"
                }
            },
            "required": ["value"],
            "type": "object"
        })
    )
}

#[test]
fn derive_openapi_with_generic_schema_with_as() {
    struct Value;

    #[derive(Serialize, ToSchema)]
    #[schema(as = api::models::Pet)]
    struct Pet<'a, Resp> {
        #[serde(skip)]
        _p: PhantomData<Resp>,
        value: Cow<'a, str>,
    }

    #[derive(OpenApi)]
    #[openapi(components(schemas(Pet<Value>)))]
    struct ApiDoc;

    let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
    let schema = doc.pointer("/components/schemas/api.models.Pet");

    assert_json_eq!(
        schema,
        json!({
            "properties": {
                "value": {
                    "type": "string"
                }
            },
            "required": ["value"],
            "type": "object"
        })
    )
}
