use std::path::PathBuf;

use apollo_router::_private::create_test_service_factory_from_yaml;
use serde_json::json;
use tower::ServiceExt;

use crate::integration::IntegrationTest;
use crate::integration::common::Query;

#[tokio::test]
async fn test_supergraph_validation_errors_are_passed_on() {
    create_test_service_factory_from_yaml(
        include_str!("../../src/testdata/invalid_supergraph.graphql"),
        "supergraph:\n  introspection: true\n",
    )
    .await;
}

/// <https://github.com/apollographql/router/issues/3388>
#[tokio::test]
async fn test_request_extensions_is_null() {
    // `extensions` is optional:
    // <https://graphql.github.io/graphql-over-http/draft/#sec-Request-Parameters>

    // > Specifying null for optional request parameters is equivalent to not specifying them at all
    // https://graphql.github.io/graphql-over-http/draft/#note-22957

    let request =
        serde_json::json!({"query": "{__typename}", "extensions": serde_json::Value::Null});
    let request = apollo_router::services::router::Request::fake_builder()
        .body(request.to_string())
        .method(hyper::Method::POST)
        .header("content-type", "application/json")
        .build()
        .unwrap();
    let response = apollo_router::TestHarness::builder()
        .schema(include_str!("../fixtures/supergraph.graphql"))
        .build_router()
        .await
        .unwrap()
        .oneshot(request)
        .await
        .unwrap()
        .next_response()
        .await
        .unwrap()
        .unwrap();
    // Used to be an INVALID_GRAPHQL_REQUEST error with "invalid type: null, expected a map"
    assert_eq!(
        String::from_utf8_lossy(&response),
        r#"{"data":{"__typename":"Query"}}"#
    );
}

#[tokio::test]
async fn test_syntax_error() {
    let request = serde_json::json!({"query": "{__typename"});
    let request = apollo_router::services::router::Request::fake_builder()
        .body(request.to_string())
        .method(hyper::Method::POST)
        .header("content-type", "application/json")
        .build()
        .unwrap();
    let response = apollo_router::TestHarness::builder()
        .schema(include_str!("../fixtures/supergraph.graphql"))
        .build_router()
        .await
        .unwrap()
        .oneshot(request)
        .await
        .unwrap()
        .next_response()
        .await
        .unwrap()
        .unwrap();

    let v: serde_json::Value = serde_json::from_slice(&response).unwrap();
    insta::assert_json_snapshot!(v, @r###"
    {
      "errors": [
        {
          "message": "parsing error: syntax error: expected R_CURLY, got EOF",
          "locations": [
            {
              "line": 1,
              "column": 12
            }
          ],
          "extensions": {
            "code": "PARSING_ERROR"
          }
        }
      ]
    }
    "###);
}

#[tokio::test]
async fn test_validation_error() {
    let request = serde_json::json!({"query": "{...a} fragment unused on Query { me { id } } fragment a on Query{me {id} topProducts(first: 5.5) {id}}"});
    let request = apollo_router::services::router::Request::fake_builder()
        .body(request.to_string())
        .method(hyper::Method::POST)
        .header("content-type", "application/json")
        .build()
        .unwrap();
    let response = apollo_router::TestHarness::builder()
        .schema(include_str!("../fixtures/supergraph.graphql"))
        .build_router()
        .await
        .unwrap()
        .oneshot(request)
        .await
        .unwrap()
        .next_response()
        .await
        .unwrap()
        .unwrap();

    let v: serde_json::Value = serde_json::from_slice(&response).unwrap();
    insta::assert_json_snapshot!(v, @r###"
    {
      "errors": [
        {
          "message": "Fragment \"unused\" is never used.",
          "locations": [
            {
              "line": 1,
              "column": 8
            }
          ],
          "extensions": {
            "code": "GRAPHQL_VALIDATION_FAILED"
          }
        },
        {
          "message": "Field \"topProducts\" of type \"Query\" must have a selection of subfields. Did you mean \"topProducts { ... }\"?",
          "locations": [
            {
              "line": 1,
              "column": 75
            }
          ],
          "extensions": {
            "code": "GRAPHQL_VALIDATION_FAILED"
          }
        },
        {
          "message": "Int cannot represent value: 5.5",
          "locations": [
            {
              "line": 1,
              "column": 94
            }
          ],
          "extensions": {
            "code": "GRAPHQL_VALIDATION_FAILED"
          }
        },
        {
          "message": "Cannot query field \"id\" on type \"Product\".",
          "locations": [
            {
              "line": 1,
              "column": 100
            }
          ],
          "extensions": {
            "code": "GRAPHQL_VALIDATION_FAILED"
          }
        }
      ]
    }
    "###);
}

#[tokio::test]
async fn test_lots_of_validation_errors() {
    let query = format!(
        "{{ __typename {} }}",
        "@a".repeat(4_000), // Stay under the token limit: "@a" is two tokens every time
    );

    let request = serde_json::json!({ "query": query });
    let request = apollo_router::services::router::Request::fake_builder()
        .body(request.to_string())
        .method(hyper::Method::POST)
        .header("content-type", "application/json")
        .build()
        .unwrap();
    let response = apollo_router::TestHarness::builder()
        .schema(include_str!("../fixtures/supergraph.graphql"))
        .build_router()
        .await
        .unwrap()
        .oneshot(request)
        .await
        .unwrap()
        .next_response()
        .await
        .unwrap()
        .unwrap();

    let v: serde_json::Value = serde_json::from_slice(&response).unwrap();
    let errors = v["errors"].as_array().unwrap();
    assert!(!errors.is_empty(), "should have errors");
    // Make sure we're actually testing the validation errors, and we're not hitting some other limit
    assert_eq!(
        errors.first().unwrap()["extensions"]["code"],
        serde_json::Value::from("GRAPHQL_VALIDATION_FAILED")
    );
    assert!(errors.len() <= 100, "should return limited error count");
}

#[rstest::rstest]
#[case::enforce(Some("enforce"), true, false)]
#[case::measure(Some("measure"), false, true)]
#[case::missing(None, true, false)]
#[tokio::test(flavor = "multi_thread")]
async fn variable_validation_mode_propagates_fully(
    #[case] strict_variable_validation: Option<&str>,
    #[case] response_should_be_error: bool,
    #[case] logs_should_contain_warning: bool,
) {
    let mut settings = insta::Settings::clone_current();
    settings.set_snapshot_suffix(strict_variable_validation.unwrap_or("missing"));
    settings.set_sort_maps(true);
    let _guard = settings.bind_to_scope();

    let mut config = json!({"supergraph": {}});
    if let Some(strict_variable_validation) = strict_variable_validation {
        config["supergraph"] = json!({ "strict_variable_validation": strict_variable_validation });
    }

    let mut router = IntegrationTest::builder()
        .config(serde_yaml::to_string(&config).unwrap())
        .supergraph(PathBuf::from(
            "tests/fixtures/supergraph_input_variables.graphql",
        ))
        .build()
        .await;

    router.start().await;
    router.assert_started().await;

    // Execute a query to trigger all the callbacks
    let (_trace_id, response) = router
        .execute_query(
            Query::builder()
                .body(json!({
                    "query": "query($msg: MessageInput) { send(message: $msg) { id } }",
                    "variables": {
                        "msg": {
                            "content": "Hello",
                            "author": "Me",
                            "canvas": [{"input": 4, "innput": 4}],
                        }
                    }
                }))
                .build(),
        )
        .await;

    assert_eq!(
        response.status().is_client_error(),
        response_should_be_error
    );
    let response_body: serde_json::Value =
        serde_json::from_slice(response.text().await.unwrap().as_bytes()).unwrap();
    insta::assert_json_snapshot!(response_body);

    router.read_logs();
    const VALIDATION_MESSAGE: &str = "encountered unexpected variable(s)";
    if logs_should_contain_warning {
        router.assert_log_contained(VALIDATION_MESSAGE);
    } else {
        router.assert_log_not_contained(VALIDATION_MESSAGE);
    }

    router.graceful_shutdown().await;
}
