use serde_json::Value::Null;

use crate::common::Server;
use crate::json;

#[actix_rt::test]
async fn error_network_not_enabled() {
    let server = Server::new().await;

    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Using the /network route requires enabling the `network` experimental feature. See https://github.com/orgs/meilisearch/discussions/805",
      "code": "feature_not_enabled",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
    }
    "###);

    let (response, code) = server.set_network(json!({"self": "myself"})).await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Using the /network route requires enabling the `network` experimental feature. See https://github.com/orgs/meilisearch/discussions/805",
      "code": "feature_not_enabled",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#feature_not_enabled"
    }
    "###);
}

#[actix_rt::test]
async fn errors_on_param() {
    let server = Server::new().await;

    let (response, code) = server.set_features(json!({"network": true})).await;
    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response["network"]), @r#"true"#);

    // non-existing param
    let (response, code) = server.set_network(json!({"selfie": "myself"})).await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Unknown field `selfie`: expected one of `remotes`, `shards`, `previousShards`, `self`, `leader`, `previousRemotes`",
      "code": "bad_request",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#bad_request"
    }
    "###);

    // self not a string
    let (response, code) = server.set_network(json!({"self": 42})).await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Invalid value type at `.self`: expected a string, but found a positive integer: `42`",
      "code": "invalid_network_self",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_self"
    }
    "###);

    // remotes not an object
    let (response, code) = server.set_network(json!({"remotes": 42})).await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Invalid value type at `.remotes`: expected an object, but found a positive integer: `42`",
      "code": "invalid_network_remotes",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_remotes"
    }
    "###);

    // new remote without url
    let (response, code) = server
        .set_network(json!({"remotes": {
            "new": {
                "searchApiKey": "http://localhost:7700"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Missing field `.remotes.new.url`",
      "code": "missing_network_url",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#missing_network_url"
    }
    "###);

    // remote with url not a string
    let (response, code) = server
        .set_network(json!({"remotes": {
            "new": {
                "url": 7700
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Invalid value type at `.remotes.new.url`: expected a string, but found a positive integer: `7700`",
      "code": "invalid_network_url",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_url"
    }
    "###);

    // remote with url not valid
    let (response, code) = server
        .set_network(json!({"remotes": {
            "new": {
                "url": "no-http-scheme"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Invalid `.remotes.new.url` (`no-http-scheme`): relative URL without a base",
      "code": "invalid_network_url",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_url"
    }
    "###);

    // remote with non-existing param
    let (response, code) = server
        .set_network(json!({"remotes": {
            "new": {
                "url": "http://localhost:7700",
                "doggo": "Intel the Beagle"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Unknown field `doggo` inside `.remotes.new`: expected one of `url`, `searchApiKey`, `writeApiKey`",
      "code": "invalid_network_remotes",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_remotes"
    }
    "###);

    // remote with non-string searchApiKey
    let (response, code) = server
        .set_network(json!({"remotes": {
            "new": {
                "url": "http://localhost:7700",
                "searchApiKey": 1204664602099962445u64,
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Invalid value type at `.remotes.new.searchApiKey`: expected a string, but found a positive integer: `1204664602099962445`",
      "code": "invalid_network_search_api_key",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_search_api_key"
    }
    "###);

    // setting `null` on URL a posteriori
    let (response, code) = server
        .set_network(json!({"remotes": {
            "kefir": {
                "url": "http://localhost:7700",
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": null,
      "remotes": {
        "kefir": {
          "url": "http://localhost:7700",
          "searchApiKey": null,
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);
    let (response, code) = server
        .set_network(json!({"remotes": {
            "kefir": {
                "url": Null,
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"400 Bad Request");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Field `.remotes.kefir.url` cannot be set to `null`",
      "code": "invalid_network_url",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_url"
    }
    "###);
}

#[cfg(feature = "enterprise")]
#[actix_rt::test]
async fn errors_on_param_sharding() {
    let server = Server::new().await;

    let (response, code) = server.set_features(json!({"network": true})).await;
    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response["network"]), @r#"true"#);

    // 1. not leader
    let (response, code) =
        server.set_network(json!({"self": "myself", "leader": "someoneelse"})).await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "This remote is not the leader of the network.\n  - Note: only the leader `someoneelse` can receive new tasks.",
      "code": "not_leader",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#not_leader"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");

    // 2. leader not in remotes
    let (response, code) = server.set_network(json!({"self": "myself", "leader": "myself"})).await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "leader `myself` is missing from remotes",
      "code": "invalid_network_remotes",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_remotes"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");

    // 3. no shards
    let (response, code) = server
        .set_network(json!({"self": "myself", "leader": "myself",
        "remotes": {
          "myself": {"url": "http://localhost:7700"}
        }}))
        .await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "there must be at least one shard owned by at least one remote",
      "code": "invalid_network_shards",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_shards"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");

    // 4. remoteless-shard
    let (response, code) = server
        .set_network(json!({"self": "myself", "leader": "myself",
          "remotes": {
            "myself": {"url": "http://localhost:7700"}
          },
          "shards": {
            "all": {
              "remotes": []
            }
          }
        }))
        .await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "there must be at least one shard owned by at least one remote",
      "code": "invalid_network_shards",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_shards"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");

    // 5. shard with unknown remote
    let (response, code) = server
        .set_network(json!({"self": "myself", "leader": "myself",
          "remotes": {
            "myself": {"url": "http://localhost:7700"}
          },
          "shards": {
            "all": {
              "remotes": [ "unknown" ]
            }
          }
        }))
        .await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "unknown remote `unknown` in `.all.remotes`",
      "code": "invalid_network_shards",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_shards"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");

    // 6. renaming
    let (response, code) = server
        .set_network(json!({"self": "myself", "leader": null,
          "remotes": {
            "myself": {"url": "http://localhost:7700"}
          },
          "shards": {
            "all": {
              "remotes": [ "myself" ]
            }
          }
        }))
        .await;
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {
        "myself": {
          "url": "http://localhost:7700",
          "searchApiKey": null,
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {
        "all": {
          "remotes": [
            "myself"
          ]
        }
      },
      "leader": null,
      "version": "[version]"
    }
    "###);
    meili_snap::snapshot!(code, @"200 OK");

    let (response, code) = server
        .set_network(json!({"self": "someoneelse", "leader": "someoneelse",
          "remotes": {
            "myself": {"url": "http://localhost:7700"}
          },
          "shards": {
            "all": {
              "remotes": [ "myself" ]
            }
          }
        }))
        .await;
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "Renaming a remote is not supported when a leader is defined.\n  - Note: applying this change would rename `myself` to `someoneelse`.\n  - Hint: Send this change to `someoneelse` if it already exists.",
      "code": "invalid_network_self",
      "type": "invalid_request",
      "link": "https://docs.meilisearch.com/errors#invalid_network_self"
    }
    "###);
    meili_snap::snapshot!(code, @"400 Bad Request");
}

#[actix_rt::test]
async fn auth() {
    let mut server = Server::new_auth().await;
    server.use_api_key("MASTER_KEY");

    let (response, code) = server.set_features(json!({"network": true})).await;
    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response["network"]), @r#"true"#);

    let (get_network_key, code) = server
        .add_api_key(json!({
          "actions": ["network.get"],
          "indexes": ["*"],
          "expiresAt": serde_json::Value::Null
        }))
        .await;
    meili_snap::snapshot!(code, @"201 Created");
    let get_network_key = get_network_key["key"].clone();

    let (update_network_key, code) = server
        .add_api_key(json!({
          "actions": ["network.update"],
          "indexes": ["*"],
          "expiresAt": serde_json::Value::Null
        }))
        .await;
    meili_snap::snapshot!(code, @"201 Created");
    let update_network_key = update_network_key["key"].clone();

    let (search_api_key, code) = server
        .add_api_key(json!({
          "actions": ["search"],
          "indexes": ["*"],
          "expiresAt": serde_json::Value::Null
        }))
        .await;
    meili_snap::snapshot!(code, @"201 Created");
    let search_api_key = search_api_key["key"].clone();

    // try with master key
    let (response, code) = server
        .set_network(json!({
          "self": "master"
        }))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "master",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "master",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // try get with get permission
    server.use_api_key(get_network_key.as_str().unwrap());
    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "master",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // try update with update permission
    server.use_api_key(update_network_key.as_str().unwrap());

    let (response, code) = server
        .set_network(json!({
          "self": "api_key"
        }))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "api_key",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // try with the other's permission
    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"403 Forbidden");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "The provided API key is invalid.",
      "code": "invalid_api_key",
      "type": "auth",
      "link": "https://docs.meilisearch.com/errors#invalid_api_key"
    }
    "###);

    server.use_api_key(get_network_key.as_str().unwrap());
    let (response, code) = server
        .set_network(json!({
          "self": "get_api_key"
        }))
        .await;

    meili_snap::snapshot!(code, @"403 Forbidden");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "The provided API key is invalid.",
      "code": "invalid_api_key",
      "type": "auth",
      "link": "https://docs.meilisearch.com/errors#invalid_api_key"
    }
    "###);
    // try either with bad permission
    server.use_api_key(search_api_key.as_str().unwrap());
    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"403 Forbidden");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "The provided API key is invalid.",
      "code": "invalid_api_key",
      "type": "auth",
      "link": "https://docs.meilisearch.com/errors#invalid_api_key"
    }
    "###);

    let (response, code) = server
        .set_network(json!({
          "self": "get_api_key"
        }))
        .await;

    meili_snap::snapshot!(code, @"403 Forbidden");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "message": "The provided API key is invalid.",
      "code": "invalid_api_key",
      "type": "auth",
      "link": "https://docs.meilisearch.com/errors#invalid_api_key"
    }
    "###);
}

#[actix_rt::test]
async fn get_and_set_network() {
    let server = Server::new().await;

    let (response, code) = server.set_features(json!({"network": true})).await;
    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response["network"]), @r#"true"#);

    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
    {
      "self": null,
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "00000000-0000-0000-0000-000000000000"
    }
    "###);

    // adding self
    let (response, code) = server.set_network(json!({"self": "myself"})).await;
    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // adding remotes
    let (response, code) = server
        .set_network(json!({"remotes": {
            "myself": {
                "url": "http://localhost:7700"
            },
            "thy": {
                "url": "http://localhost:7701",
                "searchApiKey": "foo"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {
        "myself": {
          "url": "http://localhost:7700",
          "searchApiKey": null,
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "foo",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // partially updating one remote
    let (response, code) = server
        .set_network(json!({"remotes": {
            "thy": {
                "searchApiKey": "bar"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {
        "myself": {
          "url": "http://localhost:7700",
          "searchApiKey": null,
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // adding one remote
    let (response, code) = server
        .set_network(json!({"remotes": {
            "them": {
                "url": "http://localhost:7702",
                "searchApiKey": "baz"
            }
        }}))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {
        "myself": {
          "url": "http://localhost:7700",
          "searchApiKey": null,
          "writeApiKey": null,
          "status": "available"
        },
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // deleting one remote
    let (response, code) = server
        .set_network(json!({"remotes": {
            "myself": Null,
        }}))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "myself",
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // removing self
    let (response, code) = server.set_network(json!({"self": Null})).await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": null,
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // setting self again
    let (response, code) = server.set_network(json!({"self": "thy"})).await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "thy",
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // doing nothing
    let (response, code) = server.set_network(json!({})).await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "thy",
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // still doing nothing
    let (response, code) = server.set_network(json!({"remotes": {}})).await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "thy",
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // good time to check GET
    let (response, code) = server.get_network().await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "thy",
      "remotes": {
        "them": {
          "url": "http://localhost:7702",
          "searchApiKey": "baz",
          "writeApiKey": null,
          "status": "available"
        },
        "thy": {
          "url": "http://localhost:7701",
          "searchApiKey": "bar",
          "writeApiKey": null,
          "status": "available"
        }
      },
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);

    // deleting everything
    let (response, code) = server
        .set_network(json!({
            "remotes": Null,
        }))
        .await;

    meili_snap::snapshot!(code, @"200 OK");
    meili_snap::snapshot!(meili_snap::json_string!(response, {".version" => "[version]"}), @r###"
    {
      "self": "thy",
      "remotes": {},
      "shards": {},
      "leader": null,
      "version": "[version]"
    }
    "###);
}
