use std::sync::Arc;

use crate::{
    assert_status,
    common::{
        api_common::{ApiProject, ApiVersion, AppendsOptionalPat},
        api_v2::{request_data::get_public_project_creation_data_json, ApiV2},
        database::{
            generate_random_name, ADMIN_USER_PAT, FRIEND_USER_ID,
            FRIEND_USER_PAT, USER_USER_PAT,
        },
        dummy_data::TestFile,
        environment::{with_test_environment, TestEnvironment},
        permissions::{PermissionsTest, PermissionsTestContext},
    },
};
use actix_http::StatusCode;
use actix_web::test;
use ariadne::ids::base62_impl::parse_base62;
use futures::StreamExt;
use itertools::Itertools;
use labrinth::{
    database::models::project_item::PROJECTS_SLUGS_NAMESPACE,
    models::{projects::ProjectId, teams::ProjectPermissions},
    util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData},
};
use serde_json::json;

#[actix_rt::test]
async fn test_project_type_sanity() {
    with_test_environment(
        None,
        |test_env: TestEnvironment<ApiV2>| async move {
            let api = &test_env.api;

            // Perform all other patch tests on both 'mod' and 'modpack'
            for (mod_or_modpack, slug, file) in [
                ("mod", "test-mod", TestFile::build_random_jar()),
                ("modpack", "test-modpack", TestFile::build_random_mrpack()),
            ] {
                // Create a modpack or mod
                // both are 'fabric' (but modpack is actually 'mrpack' behind the scenes, through v3,with fabric as a 'mrpack_loader')
                let (test_project, test_version) = api
                    .add_public_project(slug, Some(file), None, USER_USER_PAT)
                    .await;
                let test_project_slug = test_project.slug.as_ref().unwrap();

                // Check that the loader displays correctly as fabric from the version creation
                assert_eq!(test_project.loaders, vec!["fabric"]);
                assert_eq!(test_version[0].loaders, vec!["fabric"]);

                // Check that the project type is correct when getting the project
                let project = api
                    .get_project_deserialized(test_project_slug, USER_USER_PAT)
                    .await;
                assert_eq!(test_project.loaders, vec!["fabric"]);
                assert_eq!(project.project_type, mod_or_modpack);

                // Check that the project type is correct when getting the version
                let version = api
                    .get_version_deserialized(
                        &test_version[0].id.to_string(),
                        USER_USER_PAT,
                    )
                    .await;
                assert_eq!(
                    version.loaders.iter().map(|x| &x.0).collect_vec(),
                    vec!["fabric"]
                );

                // Edit the version loader to change it to 'forge'
                let resp = api
                    .edit_version(
                        &test_version[0].id.to_string(),
                        json!({
                            "loaders": ["forge"],
                        }),
                        USER_USER_PAT,
                    )
                    .await;
                assert_status!(&resp, StatusCode::NO_CONTENT);

                // Check that the project type is still correct when getting the project
                let project = api
                    .get_project_deserialized(test_project_slug, USER_USER_PAT)
                    .await;
                assert_eq!(project.project_type, mod_or_modpack);
                assert_eq!(project.loaders, vec!["forge"]);

                // Check that the project type is still correct when getting the version
                let version = api
                    .get_version_deserialized(
                        &test_version[0].id.to_string(),
                        USER_USER_PAT,
                    )
                    .await;
                assert_eq!(
                    version.loaders.iter().map(|x| &x.0).collect_vec(),
                    vec!["forge"]
                );
            }

            // As we get more complicated strucures with as v3 continues to expand, and alpha/beta get more complicated, we should add more tests here,
            // to ensure that projects created with v3 routes are still valid and work with v3 routes.
        },
    )
    .await;
}

#[actix_rt::test]
async fn test_add_remove_project() {
    // Test setup and dummy data
    with_test_environment(
        None,
        |test_env: TestEnvironment<ApiV2>| async move {
            let api = &test_env.api;

            // Generate test project data.
            let mut json_data = get_public_project_creation_data_json(
                "demo",
                Some(&TestFile::BasicMod),
            );

            // Basic json
            let json_segment = MultipartSegment {
                name: "data".to_string(),
                filename: None,
                content_type: Some("application/json".to_string()),
                data: MultipartSegmentData::Text(
                    serde_json::to_string(&json_data).unwrap(),
                ),
            };

            // Basic json, with a different file
            json_data["initial_versions"][0]["file_parts"][0] =
                json!("basic-mod-different.jar");
            let json_diff_file_segment = MultipartSegment {
                data: MultipartSegmentData::Text(
                    serde_json::to_string(&json_data).unwrap(),
                ),
                ..json_segment.clone()
            };

            // Basic json, with a different file, and a different slug
            json_data["slug"] = json!("new_demo");
            json_data["initial_versions"][0]["file_parts"][0] =
                json!("basic-mod-different.jar");
            let json_diff_slug_file_segment = MultipartSegment {
                data: MultipartSegmentData::Text(
                    serde_json::to_string(&json_data).unwrap(),
                ),
                ..json_segment.clone()
            };

            let basic_mod_file = TestFile::BasicMod;
            let basic_mod_different_file = TestFile::BasicModDifferent;

            // Basic file
            let file_segment = MultipartSegment {
                // 'Basic'
                name: basic_mod_file.filename(),
                filename: Some(basic_mod_file.filename()),
                content_type: basic_mod_file.content_type(),
                data: MultipartSegmentData::Binary(basic_mod_file.bytes()),
            };

            // Differently named file, with the SAME content (for hash testing)
            let file_diff_name_segment = MultipartSegment {
                // 'Different'
                name: basic_mod_different_file.filename(),
                filename: Some(basic_mod_different_file.filename()),
                content_type: basic_mod_different_file.content_type(),
                // 'Basic'
                data: MultipartSegmentData::Binary(basic_mod_file.bytes()),
            };

            // Differently named file, with different content
            let file_diff_name_content_segment = MultipartSegment {
                // 'Different'
                name: basic_mod_different_file.filename(),
                filename: Some(basic_mod_different_file.filename()),
                content_type: basic_mod_different_file.content_type(),
                data: MultipartSegmentData::Binary(
                    basic_mod_different_file.bytes(),
                ),
            };

            // Add a project- simple, should work.
            let req = test::TestRequest::post()
                .uri("/v2/project")
                .append_pat(USER_USER_PAT)
                .set_multipart(vec![json_segment.clone(), file_segment.clone()])
                .to_request();
            let resp: actix_web::dev::ServiceResponse =
                test_env.call(req).await;
            assert_status!(&resp, StatusCode::OK);

            // Get the project we just made, and confirm that it's correct
            let project =
                api.get_project_deserialized("demo", USER_USER_PAT).await;
            assert!(project.versions.len() == 1);
            let uploaded_version_id = project.versions[0];

            // Checks files to ensure they were uploaded and correctly identify the file
            let hash = sha1::Sha1::from(basic_mod_file.bytes())
                .digest()
                .to_string();
            let version = api
                .get_version_from_hash_deserialized(
                    &hash,
                    "sha1",
                    USER_USER_PAT,
                )
                .await;
            assert_eq!(version.id, uploaded_version_id);

            // Reusing with a different slug and the same file should fail
            // Even if that file is named differently
            let req = test::TestRequest::post()
                .uri("/v2/project")
                .append_pat(USER_USER_PAT)
                .set_multipart(vec![
                    json_diff_slug_file_segment.clone(), // Different slug, different file name
                    file_diff_name_segment.clone(), // Different file name, same content
                ])
                .to_request();

            let resp = test_env.call(req).await;
            assert_status!(&resp, StatusCode::BAD_REQUEST);

            // Reusing with the same slug and a different file should fail
            let req = test::TestRequest::post()
                .uri("/v2/project")
                .append_pat(USER_USER_PAT)
                .set_multipart(vec![
                    json_diff_file_segment.clone(), // Same slug, different file name
                    file_diff_name_content_segment.clone(), // Different file name, different content
                ])
                .to_request();

            let resp = test_env.call(req).await;
            assert_status!(&resp, StatusCode::BAD_REQUEST);

            // Different slug, different file should succeed
            let req = test::TestRequest::post()
                .uri("/v2/project")
                .append_pat(USER_USER_PAT)
                .set_multipart(vec![
                    json_diff_slug_file_segment.clone(), // Different slug, different file name
                    file_diff_name_content_segment.clone(), // Different file name, same content
                ])
                .to_request();

            let resp = test_env.call(req).await;
            assert_status!(&resp, StatusCode::OK);

            // Get
            let project =
                api.get_project_deserialized("demo", USER_USER_PAT).await;
            let id = project.id.to_string();

            // Remove the project
            let resp = test_env.api.remove_project("demo", USER_USER_PAT).await;
            assert_status!(&resp, StatusCode::NO_CONTENT);

            // Confirm that the project is gone from the cache
            let mut redis_conn =
                test_env.db.redis_pool.connect().await.unwrap();
            assert_eq!(
                redis_conn
                    .get(PROJECTS_SLUGS_NAMESPACE, "demo")
                    .await
                    .unwrap()
                    .map(|x| x.parse::<i64>().unwrap()),
                None
            );
            assert_eq!(
                redis_conn
                    .get(PROJECTS_SLUGS_NAMESPACE, &id)
                    .await
                    .unwrap()
                    .map(|x| x.parse::<i64>().unwrap()),
                None
            );

            // Old slug no longer works
            let resp = api.get_project("demo", USER_USER_PAT).await;
            assert_status!(&resp, StatusCode::NOT_FOUND);
        },
    )
    .await;
}

#[actix_rt::test]
async fn permissions_upload_version() {
    with_test_environment(
        None,
        |test_env: TestEnvironment<ApiV2>| async move {
            let alpha_project_id = &test_env.dummy.project_alpha.project_id;
            let alpha_version_id = &test_env.dummy.project_alpha.version_id;
            let alpha_team_id = &test_env.dummy.project_alpha.team_id;
            let alpha_file_hash = &test_env.dummy.project_alpha.file_hash;

            let api = &test_env.api;
            let basic_mod_different_file = TestFile::BasicModDifferent;
            let upload_version = ProjectPermissions::UPLOAD_VERSION;

            let req_gen = |ctx: PermissionsTestContext| async move {
                let project_id = ctx.project_id.unwrap();
                let project_id = ProjectId(parse_base62(&project_id).unwrap());
                api.add_public_version(
                    project_id,
                    "1.0.0",
                    TestFile::BasicMod,
                    None,
                    None,
                    ctx.test_pat.as_deref(),
                )
                .await
            };

            PermissionsTest::new(&test_env)
                .simple_project_permissions_test(upload_version, req_gen)
                .await
                .unwrap();

            // Upload file to existing version
            // Uses alpha project, as it has an existing version
            let file_ref = Arc::new(basic_mod_different_file);
            let req_gen = |ctx: PermissionsTestContext| {
                let file_ref = file_ref.clone();
                async move {
                    api.upload_file_to_version(
                        alpha_version_id,
                        &file_ref,
                        ctx.test_pat.as_deref(),
                    )
                    .await
                }
            };
            PermissionsTest::new(&test_env)
                .with_existing_project(alpha_project_id, alpha_team_id)
                .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
                .simple_project_permissions_test(upload_version, req_gen)
                .await
                .unwrap();

            // Patch version
            // Uses alpha project, as it has an existing version
            let req_gen = |ctx: PermissionsTestContext| async move {
                api.edit_version(
                    alpha_version_id,
                    json!({
                        "name": "Basic Mod",
                    }),
                    ctx.test_pat.as_deref(),
                )
                .await
            };

            PermissionsTest::new(&test_env)
                .with_existing_project(alpha_project_id, alpha_team_id)
                .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
                .simple_project_permissions_test(upload_version, req_gen)
                .await
                .unwrap();

            // Delete version file
            // Uses alpha project, as it has an existing version
            let delete_version = ProjectPermissions::DELETE_VERSION;
            let req_gen = |ctx: PermissionsTestContext| async move {
                api.remove_version_file(
                    alpha_file_hash,
                    ctx.test_pat.as_deref(),
                )
                .await
            };

            PermissionsTest::new(&test_env)
                .with_existing_project(alpha_project_id, alpha_team_id)
                .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
                .simple_project_permissions_test(delete_version, req_gen)
                .await
                .unwrap();

            // Delete version
            // Uses alpha project, as it has an existing version
            let req_gen = |ctx: PermissionsTestContext| async move {
                api.remove_version(alpha_version_id, ctx.test_pat.as_deref())
                    .await
            };
            PermissionsTest::new(&test_env)
                .with_existing_project(alpha_project_id, alpha_team_id)
                .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
                .simple_project_permissions_test(delete_version, req_gen)
                .await
                .unwrap();
        },
    )
    .await;
}

#[actix_rt::test]
pub async fn test_patch_v2() {
    // Hits V3-specific patchable fields
    // Other fields are tested in test_patch_project (the v2 version of that test)
    with_test_environment(
        None,
        |test_env: TestEnvironment<ApiV2>| async move {
            let api = &test_env.api;

            let alpha_project_slug = &test_env.dummy.project_alpha.project_slug;

            // Sucessful request to patch many fields.
            let resp = api
                .edit_project(
                    alpha_project_slug,
                    json!({
                        "client_side": "unsupported",
                        "server_side": "required",
                    }),
                    USER_USER_PAT,
                )
                .await;
            assert_status!(&resp, StatusCode::NO_CONTENT);

            let project = api
                .get_project_deserialized(alpha_project_slug, USER_USER_PAT)
                .await;

            // Note: the original V2 value of this was "optional",
            // but Required/Optional is no longer a carried combination in v3, as the changes made were lossy.
            // Now, the test Required/Unsupported combination is tested instead.
            // Setting Required/Optional in v2 will not work, this is known and accepteed.
            assert_eq!(project.client_side.as_str(), "unsupported");
            assert_eq!(project.server_side.as_str(), "required");
        },
    )
    .await;
}

#[actix_rt::test]
async fn permissions_patch_project_v2() {
    with_test_environment(
        Some(8),
        |test_env: TestEnvironment<ApiV2>| async move {
            let api = &test_env.api;

            // For each permission covered by EDIT_DETAILS, ensure the permission is required
            let edit_details = ProjectPermissions::EDIT_DETAILS;
            let test_pairs = [
                ("description", json!("description")),
                ("issues_url", json!("https://issues.com")),
                ("source_url", json!("https://source.com")),
                ("wiki_url", json!("https://wiki.com")),
                (
                    "donation_urls",
                    json!([{
                        "id": "paypal",
                        "platform": "Paypal",
                        "url": "https://paypal.com"
                    }]),
                ),
                ("discord_url", json!("https://discord.com")),
            ];

            futures::stream::iter(test_pairs)
                .map(|(key, value)| {
                    let test_env = test_env.clone();
                    async move {
                        let req_gen = |ctx: PermissionsTestContext| async {
                            api.edit_project(
                            &ctx.project_id.unwrap(),
                            json!({
                                key: if key == "slug" {
                                    json!(generate_random_name("randomslug"))
                                } else {
                                    value.clone()
                                },
                            }),
                            ctx.test_pat.as_deref(),
                        )
                        .await
                        };
                        PermissionsTest::new(&test_env)
                            .simple_project_permissions_test(
                                edit_details,
                                req_gen,
                            )
                            .await
                            .into_iter();
                    }
                })
                .buffer_unordered(4)
                .collect::<Vec<_>>()
                .await;

            // Edit body
            // Cannot bulk edit body
            let edit_body = ProjectPermissions::EDIT_BODY;
            let req_gen = |ctx: PermissionsTestContext| async move {
                api.edit_project(
                    &ctx.project_id.unwrap(),
                    json!({
                        "body": "new body!", // new body
                    }),
                    ctx.test_pat.as_deref(),
                )
                .await
            };
            PermissionsTest::new(&test_env)
                .simple_project_permissions_test(edit_body, req_gen)
                .await
                .unwrap();
        },
    )
    .await;
}

#[actix_rt::test]
pub async fn test_bulk_edit_links() {
    with_test_environment(
        None,
        |test_env: TestEnvironment<ApiV2>| async move {
            let api = &test_env.api;
            let alpha_project_id: &str =
                &test_env.dummy.project_alpha.project_id;
            let beta_project_id: &str = &test_env.dummy.project_beta.project_id;

            let resp = api
                .edit_project_bulk(
                    &[alpha_project_id, beta_project_id],
                    json!({
                        "issues_url": "https://github.com",
                        "donation_urls": [
                            {
                                "id": "patreon",
                                "platform": "Patreon",
                                "url": "https://www.patreon.com/my_user"
                            }
                        ],
                    }),
                    ADMIN_USER_PAT,
                )
                .await;
            assert_status!(&resp, StatusCode::NO_CONTENT);

            let alpha_body = api
                .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = alpha_body.donation_urls.unwrap();
            assert_eq!(donation_urls.len(), 1);
            assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
            assert_eq!(
                alpha_body.issues_url,
                Some("https://github.com".to_string())
            );
            assert_eq!(alpha_body.discord_url, None);

            let beta_body = api
                .get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = beta_body.donation_urls.unwrap();
            assert_eq!(donation_urls.len(), 1);
            assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
            assert_eq!(
                beta_body.issues_url,
                Some("https://github.com".to_string())
            );
            assert_eq!(beta_body.discord_url, None);

            let resp = api
                .edit_project_bulk(
                    &[alpha_project_id, beta_project_id],
                    json!({
                        "discord_url": "https://discord.gg",
                        "issues_url": null,
                        "add_donation_urls": [
                            {
                                "id": "bmac",
                                "platform": "Buy Me a Coffee",
                                "url": "https://www.buymeacoffee.com/my_user"
                            }
                        ],
                    }),
                    ADMIN_USER_PAT,
                )
                .await;
            assert_status!(&resp, StatusCode::NO_CONTENT);

            let alpha_body = api
                .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = alpha_body
                .donation_urls
                .unwrap()
                .into_iter()
                .sorted_by_key(|x| x.id.clone())
                .collect_vec();
            assert_eq!(donation_urls.len(), 2);
            assert_eq!(
                donation_urls[0].url,
                "https://www.buymeacoffee.com/my_user"
            );
            assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user");
            assert_eq!(alpha_body.issues_url, None);
            assert_eq!(
                alpha_body.discord_url,
                Some("https://discord.gg".to_string())
            );

            let beta_body = api
                .get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = beta_body
                .donation_urls
                .unwrap()
                .into_iter()
                .sorted_by_key(|x| x.id.clone())
                .collect_vec();
            assert_eq!(donation_urls.len(), 2);
            assert_eq!(
                donation_urls[0].url,
                "https://www.buymeacoffee.com/my_user"
            );
            assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user");
            assert_eq!(alpha_body.issues_url, None);
            assert_eq!(
                alpha_body.discord_url,
                Some("https://discord.gg".to_string())
            );

            let resp = api
                .edit_project_bulk(
                    &[alpha_project_id, beta_project_id],
                    json!({
                        "donation_urls": [
                            {
                                "id": "patreon",
                                "platform": "Patreon",
                                "url": "https://www.patreon.com/my_user"
                            },
                            {
                                "id": "ko-fi",
                                "platform": "Ko-fi",
                                "url": "https://www.ko-fi.com/my_user"
                            }
                        ],
                        "add_donation_urls": [
                            {
                                "id": "paypal",
                                "platform": "PayPal",
                                "url": "https://www.paypal.com/my_user"
                            }
                        ],
                        "remove_donation_urls": [
                            {
                                "id": "ko-fi",
                                "platform": "Ko-fi",
                                "url": "https://www.ko-fi.com/my_user"
                            }
                        ],
                    }),
                    ADMIN_USER_PAT,
                )
                .await;
            assert_status!(&resp, StatusCode::NO_CONTENT);

            let alpha_body = api
                .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = alpha_body
                .donation_urls
                .unwrap()
                .into_iter()
                .sorted_by_key(|x| x.id.clone())
                .collect_vec();
            assert_eq!(donation_urls.len(), 2);
            assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
            assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user");

            let beta_body = api
                .get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
                .await;
            let donation_urls = beta_body
                .donation_urls
                .unwrap()
                .into_iter()
                .sorted_by_key(|x| x.id.clone())
                .collect_vec();
            assert_eq!(donation_urls.len(), 2);
            assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
            assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user");
        },
    )
    .await;
}
