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

use types::collection::Collection;

use crate::utils::server::TestServer;

use crate::utils::webdav::GenerateTestDavResource;
use crate::webdav::*;

pub async fn test(test: &TestServer) {
    println!("Running PUT/GET tests...");
    let client = test.account("john@example.com").webdav_client();

    // Simple PUT
    let mut files = AHashMap::new();
    for (path, ct, content) in [
        (
            "/dav/file/john%40example.com/file1.txt",
            "text/plain",
            TEST_FILE_1,
        ),
        (
            "/dav/file/john%40example.com/file2.txt",
            "text/x-other",
            TEST_FILE_2,
        ),
        (
            "/dav/card/john%40example.com/default/card1.vcf",
            "text/vcard; charset=utf-8",
            TEST_VCARD_1,
        ),
        (
            "/dav/card/john%40example.com/default/card2.vcf",
            "text/vcard; charset=utf-8",
            TEST_VCARD_2,
        ),
        (
            "/dav/cal/john%40example.com/default/event1.ics",
            "text/calendar; charset=utf-8",
            TEST_ICAL_1,
        ),
        (
            "/dav/cal/john%40example.com/default/event2.ics",
            "text/calendar; charset=utf-8",
            TEST_ICAL_2,
        ),
    ] {
        let content = content.replace("\n", "\r\n");
        let etag = client
            .request_with_headers("PUT", path, [("content-type", ct)], &content)
            .await
            .with_status(StatusCode::CREATED)
            .etag()
            .to_string();
        files.insert(path, (content, ct, etag));
    }

    // Test GET
    for (path, (content, ct, etag)) in &files {
        client
            .request("GET", path, "")
            .await
            .with_status(StatusCode::OK)
            .with_header("etag", etag)
            .with_header("content-type", ct)
            .with_body(content);
    }

    // PUT under a non-existing parent should fail
    for (path, contents) in [
        ("/dav/file/john%40example.com/foo/file1.txt", TEST_FILE_1),
        ("/dav/card/john%40example.com/foo/card1.vcf", TEST_VCARD_1),
        ("/dav/cal/john%40example.com/foo/event1.ics", TEST_ICAL_1),
    ] {
        client
            .request("PUT", path, contents)
            .await
            .with_status(StatusCode::CONFLICT);
    }

    // PUT under resources should fail
    for (path, contents) in [
        (
            "/dav/file/john%40example.com/file1.txt/other-file.txt",
            TEST_FILE_1,
        ),
        (
            "/dav/card/john%40example.com/default/card1.vcf/other-file.vcf",
            TEST_VCARD_1,
        ),
        (
            "/dav/cal/john%40example.com/default/event1.ics/other-file.ical",
            TEST_ICAL_1,
        ),
    ] {
        client
            .request("PUT", path, contents)
            .await
            .with_status(StatusCode::METHOD_NOT_ALLOWED);
    }

    // PUT a non-vCard/iCalendar file should fail
    for (path, ct, content, precondition) in [
        (
            "/dav/card/john%40example.com/card3.vcf",
            "text/vcard; charset=utf-8",
            TEST_FILE_1,
            "B:supported-address-data",
        ),
        (
            "/dav/cal/john%40example.com/event3.ics",
            "text/calendar; charset=utf-8",
            TEST_FILE_2,
            "A:supported-calendar-data",
        ),
    ] {
        client
            .request_with_headers("PUT", path, [("content-type", ct)], content)
            .await
            .with_status(StatusCode::PRECONDITION_FAILED)
            .with_failed_precondition(precondition, "");
    }

    // Exceeding the configured file limits should fail
    let conf = &test.server.core.groupware;
    for (path, contents, max_size, expect) in [
        (
            "/dav/file/john%40example.com/chunky-file1.txt",
            TEST_FILE_1,
            conf.max_file_size,
            None,
        ),
        (
            "/dav/card/john%40example.com/chunky-card1.vcf",
            TEST_VCARD_1,
            conf.max_vcard_size,
            Some("B:max-resource-size"),
        ),
        (
            "/dav/cal/john%40example.com/chunky-event1.ics",
            TEST_ICAL_1,
            conf.max_ical_size,
            Some("A:max-resource-size"),
        ),
    ] {
        let mut chunky_contents = String::with_capacity(max_size + contents.len());
        while chunky_contents.len() < max_size {
            chunky_contents.push_str(contents);
        }
        let response = client
            .request("PUT", path, chunky_contents)
            .await
            .with_status(
                expect
                    .map(|_| StatusCode::PRECONDITION_FAILED)
                    .unwrap_or(StatusCode::PAYLOAD_TOO_LARGE),
            );
        if let Some(expect) = expect {
            response.with_failed_precondition(expect, &max_size.to_string());
        }
    }

    // PUT requests cannot exceed quota
    let mike_noquota = test.account("mike@example.com").webdav_client();
    for resource_type in [
        DavResourceName::File,
        DavResourceName::Card,
        DavResourceName::Cal,
    ] {
        let path = format!(
            "{}/mike%40example.com/quota-test/",
            resource_type.base_path()
        );
        mike_noquota
            .mkcol("MKCOL", &path, [], [])
            .await
            .with_status(StatusCode::CREATED);
        let mut num_success = 0;
        let mut did_fail = false;

        for i in 0..100 {
            let content = resource_type.generate();
            let available = mike_noquota.available_quota(&path).await;

            let response = mike_noquota
                .request_with_headers("PUT", &format!("{path}file{i}"), [], &content)
                .await;
            if available > content.len() as u64 {
                num_success += 1;
                response.with_status(StatusCode::CREATED);
            } else {
                response
                    .with_status(StatusCode::PRECONDITION_FAILED)
                    .with_failed_precondition("D:quota-not-exceeded", "");
                did_fail = true;
                break;
            }
        }
        if !did_fail {
            panic!("Quota test failed: {} files created", num_success);
        }
        if num_success == 0 {
            panic!("Quota test failed: no files created");
        }

        mike_noquota
            .request("DELETE", &path, "")
            .await
            .with_status(StatusCode::NO_CONTENT);
    }

    // PUT precondition enforcement
    let modseq = [
        test.resources("john@example.com", Collection::FileNode)
            .await
            .highest_change_id,
        test.resources("john@example.com", Collection::Calendar)
            .await
            .highest_change_id,
        test.resources("john@example.com", Collection::AddressBook)
            .await
            .highest_change_id,
    ];
    for (path, ct, content) in [
        (
            "/dav/file/john%40example.com/file1.txt",
            "text/plain",
            TEST_FILE_1,
        ),
        (
            "/dav/card/john%40example.com/default/card1.vcf",
            "text/vcard; charset=utf-8",
            TEST_VCARD_1,
        ),
        (
            "/dav/cal/john%40example.com/default/event1.ics",
            "text/calendar; charset=utf-8",
            TEST_ICAL_1,
        ),
    ] {
        let content = content.replace("\n", "\r\n");
        client
            .request_with_headers(
                "PUT",
                path,
                [("content-type", ct), ("if-none-match", "*")],
                &content,
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED);

        client
            .request_with_headers(
                "PUT",
                path,
                [("content-type", ct), ("overwrite", "F")],
                &content,
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED);

        client
            .request_with_headers(
                "PUT",
                path,
                [("content-type", ct), ("if", "([\"3827\"])")],
                &content,
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED);

        client
            .request_with_headers(
                "PUT",
                path,
                [
                    ("content-type", ct),
                    ("if", "([\"3827\"])"),
                    ("prefer", "return=representation"),
                ],
                &content,
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED)
            .with_header("preference-applied", "return=representation")
            .with_body(&content);
    }
    assert_eq!(
        [
            test.resources("john@example.com", Collection::FileNode)
                .await
                .highest_change_id,
            test.resources("john@example.com", Collection::Calendar)
                .await
                .highest_change_id,
            test.resources("john@example.com", Collection::AddressBook)
                .await
                .highest_change_id,
        ],
        modseq
    );

    // Update files using etags
    for (path, (content, ct, etag)) in &mut files {
        let condition = format!("([{}])", etag);
        *content = content.replace("X-TEST:SEQ1", "X-TEST:SEQ2");
        *etag = client
            .request_with_headers(
                "PUT",
                path,
                [("content-type", &**ct), ("if", condition.as_str())],
                content.as_str(),
            )
            .await
            .with_status(StatusCode::NO_CONTENT)
            .etag()
            .to_string();
    }

    // Test GET
    for (path, (content, ct, etag)) in &files {
        client
            .request("GET", path, "")
            .await
            .with_status(StatusCode::OK)
            .with_header("etag", etag)
            .with_header("content-type", ct)
            .with_body(content);
    }

    // PUT requests require unique UIDs
    for (path, ct, content, precond_key, precond_value) in [
        (
            "/dav/card/john%40example.com/default/card5.vcf",
            "text/vcard; charset=utf-8",
            TEST_VCARD_1,
            "B:no-uid-conflict.D:href",
            "/dav/card/john%40example.com/default/card1.vcf",
        ),
        (
            "/dav/cal/john%40example.com/default/event5.ics",
            "text/calendar; charset=utf-8",
            TEST_ICAL_1,
            "A:no-uid-conflict.D:href",
            "/dav/cal/john%40example.com/default/event1.ics",
        ),
    ] {
        client
            .request_with_headers(
                "PUT",
                path,
                [("content-type", ct), ("if-none-match", "*")],
                content,
            )
            .await
            .with_status(StatusCode::PRECONDITION_FAILED)
            .with_failed_precondition(precond_key, precond_value);
    }

    // iCal containing different component types should fail
    client
        .request_with_headers(
            "PUT",
            "/dav/cal/john%40example.com/default/invalid.ics",
            [
                ("content-type", "text/calendar; charset=utf-8"),
                ("if-none-match", "*"),
            ],
            r#"BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:1234567890
SUMMARY:Test Event
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
BEGIN:VTODO
UID:1234567890
SUMMARY:Test Task
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VTODO
END:VCALENDAR
"#,
        )
        .await
        .with_status(StatusCode::PRECONDITION_FAILED)
        .with_failed_precondition("A:valid-calendar-object-resource", "");

    // iCal referencing more than one UID should fail
    client
        .request_with_headers(
            "PUT",
            "/dav/cal/john%40example.com/default/invalid.ics",
            [
                ("content-type", "text/calendar; charset=utf-8"),
                ("if-none-match", "*"),
            ],
            r#"BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:1234567890
SUMMARY:Test Event 1
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
BEGIN:VEVENT
UID:1234567891
SUMMARY:Test Event 2
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
END:VCALENDAR
"#,
        )
        .await
        .with_status(StatusCode::PRECONDITION_FAILED)
        .with_failed_precondition("A:valid-calendar-object-resource", "");

    // Deleting unknown/invalid destinations should fail
    for (path, expect) in [
        (
            "/dav/file/john%40example.com/unknown.txt",
            StatusCode::NOT_FOUND,
        ),
        (
            "/dav/card/john%40example.com/default/unknown.txt",
            StatusCode::NOT_FOUND,
        ),
        (
            "/dav/cal/john%40example.com/default/unknown.txt",
            StatusCode::NOT_FOUND,
        ),
        ("/dav/file/john%40example.com", StatusCode::FORBIDDEN),
        ("/dav/cal/john%40example.com", StatusCode::FORBIDDEN),
        ("/dav/card/john%40example.com", StatusCode::FORBIDDEN),
        (
            "/dav/pal/john%40example.com",
            StatusCode::METHOD_NOT_ALLOWED,
        ),
        ("/dav/file", StatusCode::FORBIDDEN),
        ("/dav/cal", StatusCode::FORBIDDEN),
        ("/dav/card", StatusCode::FORBIDDEN),
        ("/dav/pal", StatusCode::METHOD_NOT_ALLOWED),
    ] {
        client.request("DELETE", path, "").await.with_status(expect);
    }

    // Delete files
    for (path, (_, _, etag)) in &files {
        client
            .request_with_headers("DELETE", path, [("if", "([\"3827\"])")], "")
            .await
            .with_status(StatusCode::PRECONDITION_FAILED);

        let condition = format!("([{}])", etag);
        client
            .request_with_headers("DELETE", path, [("if", condition.as_str())], "")
            .await
            .with_status(StatusCode::NO_CONTENT);

        client
            .request("DELETE", path, "")
            .await
            .with_status(StatusCode::NOT_FOUND);
    }

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