/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
 */

use crate::utils::{server::TestServer, webdav::GenerateTestDavResource};

use ahash::AHashSet;
use dav_proto::Depth;
use groupware::DavResourceName;
use hyper::StatusCode;

pub async fn test(test: &TestServer) {
    let client = test.account("john@example.com").webdav_client();

    for resource_type in [
        DavResourceName::File,
        DavResourceName::Cal,
        DavResourceName::Card,
    ] {
        println!(
            "Running REPORT sync-collection tests ({})...",
            resource_type.base_path()
        );
        let user_base_path = format!("{}/john%40example.com/", resource_type.base_path());

        // Test 1: Initial sync
        let response = client
            .sync_collection(&user_base_path, "", Depth::Infinity, None, ["D:getetag"])
            .await;
        assert_eq!(
            response.hrefs().len(),
            if resource_type == DavResourceName::File {
                1
            } else {
                2
            },
            "{:?}",
            response.hrefs()
        );
        let sync_token_1 = response.sync_token().to_string();

        // Test 2: No changes since last sync
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token_1,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(response.hrefs(), Vec::<String>::new());

        // Test 3: Create a collection and make sure it is synced
        let new_collection = format!("{}new-collection/", user_base_path);
        client
            .mkcol("MKCOL", &new_collection, [], [])
            .await
            .with_status(StatusCode::CREATED);
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token_1,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(response.hrefs(), vec![new_collection.clone()]);
        let sync_token_2 = response.sync_token().to_string();

        // Test 4: Create a file and make sure it is synced
        let new_file = format!("{new_collection}new-file");
        let contents = resource_type.generate();
        client
            .request("PUT", &new_file, &contents)
            .await
            .with_status(StatusCode::CREATED);
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token_1,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(
            response.hrefs(),
            vec![new_collection.clone(), new_file.clone()]
        );
        let sync_token_3 = response.sync_token().to_string();
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token_2,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(response.hrefs(), vec![new_file.clone()]);

        // Test 5: sync-token with Depth 1
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token_1,
                Depth::One,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(response.hrefs(), vec![new_collection.clone()]);

        // Test 6: sync-token with Depth 0
        let response = client
            .sync_collection(
                &new_collection,
                &sync_token_1,
                Depth::Zero,
                None,
                ["D:getetag"],
            )
            .await;
        assert_eq!(response.hrefs(), vec![new_collection.clone()]);

        // Test 7: Outdated sync-token in If header should fail
        let new_file2 = format!("{new_collection}new-file2");
        let contents = resource_type.generate();
        let condition = format!("(<{sync_token_2}>)");
        client
            .request_with_headers(
                "PUT",
                &new_file2,
                [("if", condition.as_str())],
                contents.as_str(),
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED)
            .with_empty_body();

        // Test 8: Correct sync-token in If header should work
        let condition = format!("(<{sync_token_3}>)");
        client
            .request_with_headers(
                "PUT",
                &new_file2,
                [("if", condition.as_str())],
                contents.as_str(),
            )
            .await
            .with_status(StatusCode::CREATED)
            .with_empty_body();

        // Test 9: Limit
        let mut sync_token = client
            .sync_collection(
                &new_collection,
                &sync_token_3,
                Depth::Zero,
                None,
                ["D:getetag"],
            )
            .await
            .sync_token()
            .to_string();
        let (folder_name, files) = client
            .create_hierarchy(user_base_path.trim_end_matches('/'), 1, 0, 10)
            .await;
        let mut expected_changes = files
            .iter()
            .map(|x| x.0.as_str())
            .chain([folder_name.as_str()])
            .collect::<AHashSet<_>>();
        for _ in 0..10 {
            let response = client
                .sync_collection(
                    &user_base_path,
                    &sync_token,
                    Depth::Infinity,
                    2.into(),
                    ["D:getetag"],
                )
                .await;
            sync_token = response.sync_token().to_string();
            let hrefs = response.hrefs();
            if hrefs.is_empty() {
                break;
            }
            let mut has_user_base_path = false;
            let mut item_count = 0;
            for href in hrefs {
                if href == user_base_path {
                    has_user_base_path = true;
                } else if expected_changes.remove(href) {
                    item_count += 1;
                } else {
                    panic!("Unexpected href: {href}");
                }
            }
            if has_user_base_path {
                assert_eq!(item_count, 2);
                response
                    .with_value(
                        "D:multistatus.D:response.D:status",
                        "HTTP/1.1 507 Insufficient Storage",
                    )
                    .with_value(
                        "D:multistatus.D:response.D:error.D:number-of-matches-within-limits",
                        "",
                    )
                    .with_value(
                        "D:multistatus.D:response.D:responsedescription",
                        "The number of matches exceeds the limit of 2",
                    );
            } else {
                assert!(item_count <= 2);
                break;
            }
        }
        assert!(expected_changes.is_empty(), "{:?}", expected_changes);

        // Test 10: Expect changes after deletion
        client
            .request("DELETE", &new_file, "")
            .await
            .with_status(StatusCode::NO_CONTENT);
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        sync_token = response.sync_token().to_string();
        response
            .with_href_count(1)
            .with_value("D:multistatus.D:response.D:href", &new_file)
            .with_value(
                "D:multistatus.D:response.D:status",
                "HTTP/1.1 404 Not Found",
            );
        client
            .request("DELETE", &new_collection, "")
            .await
            .with_status(StatusCode::NO_CONTENT);
        let response = client
            .sync_collection(
                &user_base_path,
                &sync_token,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await;
        sync_token = response.sync_token().to_string();
        response
            .with_href_count(1)
            .with_value("D:multistatus.D:response.D:href", &new_collection)
            .with_value(
                "D:multistatus.D:response.D:status",
                "HTTP/1.1 404 Not Found",
            );
        client
            .request("DELETE", &folder_name, "")
            .await
            .with_status(StatusCode::NO_CONTENT);
        client
            .sync_collection(
                &user_base_path,
                &sync_token,
                Depth::Infinity,
                None,
                ["D:getetag"],
            )
            .await
            .with_href_count(1)
            .with_value("D:multistatus.D:response.D:href", &folder_name)
            .with_value(
                "D:multistatus.D:response.D:status",
                "HTTP/1.1 404 Not Found",
            );
    }

    client.delete_default_containers().await;
    test.assert_is_empty().await;
}
