/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 */

mod integration;

use std::collections::HashSet;

use integration::Account;
use integration::baikal::{AccountSeed, Baikal};
use integration::validate::{
    all_contact_uids, all_event_uids, assert_contact_round_trip, assert_event_round_trip,
    calendar_event_count, cleanup, collection_id_by_name, collection_names, common,
    contact_card_count, count, open_archive, tmp_archive,
};

use rusqlite::Connection;
use serde_json::Value;
use vandelay::error::Error;
use vandelay::sync::import_dav;
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};

fn dav_config(kind: DavKindArg, account: &Account, dav_root: &str) -> DavImportConfig {
    let path = match kind {
        DavKindArg::Caldav => format!("{dav_root}/calendars/{}/", account.username),
        DavKindArg::Carddav => format!("{dav_root}/addressbooks/{}/", account.username),
        DavKindArg::Webdav => format!("{dav_root}/"),
    };
    DavImportConfig {
        kind,
        url: path,
        auth: DavAuth::Basic {
            user: account.username.clone(),
            password: account.password.clone(),
        },
        allow_cleartext: true,
        dav_connections: 2,
        multiget_batch: 25,
        allow_source_change: false,
    }
}

fn dav_config_via_discovery(
    kind: DavKindArg,
    account: &Account,
    dav_root: &str,
) -> DavImportConfig {
    DavImportConfig {
        kind,
        url: format!("{dav_root}/"),
        auth: DavAuth::Basic {
            user: account.username.clone(),
            password: account.password.clone(),
        },
        allow_cleartext: true,
        dav_connections: 2,
        multiget_batch: 25,
        allow_source_change: false,
    }
}

#[test]
#[ignore = "requires Docker"]
fn baikal_starts_seeds_and_imports() {
    let b = Baikal::start().expect("baikal start");
    let per_account = b.seed_all().expect("baikal seed");
    assert_eq!(
        per_account.len(),
        b.accounts.len(),
        "seed should return stats for every account"
    );
    b.verify_seed(&per_account).expect("baikal verify");

    let dav_root = b.dav_root();

    for seed in &per_account {
        let account = b
            .accounts
            .iter()
            .find(|a| a.username == seed.username)
            .expect("account lookup");
        assert!(
            !seed.calendars.is_empty(),
            "{}: expected calendars seeded",
            seed.username
        );
        assert!(
            !seed.address_books.is_empty(),
            "{}: expected address books seeded",
            seed.username
        );
        assert!(
            seed.total_events() > 0,
            "{}: expected events seeded",
            seed.username
        );
        assert!(
            seed.total_contacts() > 0,
            "{}: expected contacts seeded",
            seed.username
        );

        let cal_archive = tmp_archive(&format!("baikal-cal-{}", seed.username));
        let summary = import_dav::run(
            common(&cal_archive),
            dav_config(DavKindArg::Caldav, account, &dav_root),
        )
        .expect("caldav import");
        assert!(
            !summary.any_failed(),
            "{}: first caldav import had failures: {summary:?}",
            seed.username
        );
        let conn = open_archive(&cal_archive);
        assert_event_collection_exact(&conn, seed);
        assert_event_uids_round_trip(&conn, seed);
        assert_event_data_well_formed(&conn);
        for plan in &seed.calendars {
            for item in &plan.items {
                assert_event_round_trip(
                    &conn,
                    &item.source,
                    &item.uid,
                    &format!("{}/{}", seed.username, plan.display_name),
                );
            }
        }
        let cal_count = count(&conn, "calendars") as usize;
        let ev_count = count(&conn, "calendar_events") as usize;
        let blob_count = count(&conn, "blobs") as usize;
        drop(conn);

        let summary2 = import_dav::run(
            common(&cal_archive),
            dav_config(DavKindArg::Caldav, account, &dav_root),
        )
        .expect("idempotent caldav re-import");
        assert!(
            !summary2.any_failed(),
            "{}: idempotent caldav re-import had failures",
            seed.username
        );
        let conn = open_archive(&cal_archive);
        assert_eq!(
            count(&conn, "calendars") as usize,
            cal_count,
            "{}: idempotent re-import changed calendar count",
            seed.username
        );
        assert_eq!(
            count(&conn, "calendar_events") as usize,
            ev_count,
            "{}: idempotent re-import changed event count",
            seed.username
        );
        assert_eq!(
            count(&conn, "blobs") as usize,
            blob_count,
            "{}: idempotent re-import changed blob count",
            seed.username
        );
        drop(conn);

        let card_archive = tmp_archive(&format!("baikal-card-{}", seed.username));
        let summary = import_dav::run(
            common(&card_archive),
            dav_config(DavKindArg::Carddav, account, &dav_root),
        )
        .expect("carddav import");
        assert!(
            !summary.any_failed(),
            "{}: first carddav import had failures: {summary:?}",
            seed.username
        );
        let conn = open_archive(&card_archive);
        assert_contact_collection_exact(&conn, seed);
        assert_contact_uids_round_trip(&conn, seed);
        assert_contact_data_well_formed(&conn);
        for plan in &seed.address_books {
            for item in &plan.items {
                assert_contact_round_trip(
                    &conn,
                    &item.source,
                    &item.uid,
                    &format!("{}/{}", seed.username, plan.display_name),
                );
            }
        }
        let book_count = count(&conn, "address_books") as usize;
        let card_count = count(&conn, "contact_cards") as usize;
        drop(conn);

        let summary2 = import_dav::run(
            common(&card_archive),
            dav_config(DavKindArg::Carddav, account, &dav_root),
        )
        .expect("idempotent carddav re-import");
        assert!(
            !summary2.any_failed(),
            "{}: idempotent carddav re-import had failures",
            seed.username
        );
        let conn = open_archive(&card_archive);
        assert_eq!(
            count(&conn, "address_books") as usize,
            book_count,
            "{}: idempotent re-import changed address book count",
            seed.username
        );
        assert_eq!(
            count(&conn, "contact_cards") as usize,
            card_count,
            "{}: idempotent re-import changed contact count",
            seed.username
        );
        drop(conn);

        let vanish_href = seed.calendars[0]
            .items
            .first()
            .map(|i| i.href.clone())
            .expect("at least one event seeded");
        let vanish_uid = seed.calendars[0].items.first().unwrap().uid.clone();
        b.delete_item(account, &vanish_href)
            .expect("baikal delete vanished item");
        let summary_after_vanish = import_dav::run(
            common(&cal_archive),
            dav_config(DavKindArg::Caldav, account, &dav_root),
        )
        .expect("caldav re-import after vanish");
        assert!(
            !summary_after_vanish.any_failed(),
            "{}: re-import after vanish had failures",
            seed.username
        );
        let conn = open_archive(&cal_archive);
        assert_eq!(
            count(&conn, "calendar_events") as usize,
            ev_count - 1,
            "{}: vanished event was not pruned",
            seed.username
        );
        let uids_now = all_event_uids(&conn);
        assert!(
            !uids_now.contains(&vanish_uid),
            "{}: vanished event uid {vanish_uid} still in archive",
            seed.username
        );
        drop(conn);

        let icals = integration::data::load_icals().expect("load icals");
        let (_added_event_href, added_event_uid) = b
            .add_event(account, 0, &format!("bk-{}", seed.username), &icals)
            .expect("baikal add event");
        let summary_after_add_event = import_dav::run(
            common(&cal_archive),
            dav_config(DavKindArg::Caldav, account, &dav_root),
        )
        .expect("caldav re-import after add");
        assert!(
            !summary_after_add_event.any_failed(),
            "{}: re-import after add-event had failures",
            seed.username
        );
        let conn = open_archive(&cal_archive);
        let after_add_events = count(&conn, "calendar_events") as usize;
        assert_eq!(
            after_add_events, ev_count,
            "{}: add-then-import should restore count to baseline",
            seed.username
        );
        let uids_now = all_event_uids(&conn);
        assert!(
            uids_now.contains(&added_event_uid),
            "{}: added event uid {added_event_uid} not found in archive",
            seed.username
        );
        drop(conn);

        let vcards = integration::data::load_vcards().expect("load vcards");
        let (_added_card_href, added_card_uid) = b
            .add_contact(account, 0, &format!("bk-{}", seed.username), &vcards)
            .expect("baikal add contact");
        let summary_after_add_contact = import_dav::run(
            common(&card_archive),
            dav_config(DavKindArg::Carddav, account, &dav_root),
        )
        .expect("carddav re-import after add");
        assert!(
            !summary_after_add_contact.any_failed(),
            "{}: re-import after add-contact had failures",
            seed.username
        );
        let conn = open_archive(&card_archive);
        let after_add_contacts = count(&conn, "contact_cards") as usize;
        assert_eq!(
            after_add_contacts,
            card_count + 1,
            "{}: add-then-import should grow contact count by 1",
            seed.username
        );
        let imported_contact_uids = all_contact_uids(&conn);
        assert!(
            imported_contact_uids.contains(&added_card_uid),
            "{}: added contact uid {added_card_uid} not found",
            seed.username
        );
        drop(conn);

        let discovery_archive = tmp_archive(&format!("baikal-cal-discovery-{}", seed.username));
        let discovery_summary = import_dav::run(
            common(&discovery_archive),
            dav_config_via_discovery(DavKindArg::Caldav, account, &dav_root),
        )
        .expect("caldav import via discovery from dav root");
        assert!(
            !discovery_summary.any_failed(),
            "{}: caldav import via discovery had failures: {discovery_summary:?}",
            seed.username
        );
        let conn = open_archive(&discovery_archive);
        let discovered_events = count(&conn, "calendar_events") as usize;
        assert_eq!(
            discovered_events, ev_count,
            "{}: discovery-based import event count mismatch (server holds {ev_count} after vanish+add)",
            seed.username
        );
        drop(conn);
        cleanup(&discovery_archive);

        cleanup(&cal_archive);
        cleanup(&card_archive);
    }

    let primary = &b.accounts[0];
    let other = &b.accounts[1];
    let shared_archive = tmp_archive("baikal-source-change");
    import_dav::run(
        common(&shared_archive),
        dav_config(DavKindArg::Caldav, primary, &dav_root),
    )
    .expect("seed archive with primary user");
    let err = import_dav::run(
        common(&shared_archive),
        dav_config(DavKindArg::Caldav, other, &dav_root),
    )
    .expect_err("expected source-change abort");
    assert!(
        matches!(err, Error::SourceChange(_)),
        "expected SourceChange, got {err:?}"
    );
    cleanup(&shared_archive);

    b.stop().expect("baikal stop");
}

fn assert_event_collection_exact(conn: &Connection, seed: &AccountSeed) {
    let names = collection_names(conn, "calendars");
    let expected: HashSet<String> = seed
        .calendars
        .iter()
        .map(|c| c.display_name.clone())
        .collect();
    assert_eq!(
        names, expected,
        "{}: calendar displayname set mismatch",
        seed.username
    );
    for plan in &seed.calendars {
        let cal_id = collection_id_by_name(conn, "calendars", &plan.display_name)
            .unwrap_or_else(|| panic!("calendar {} missing", plan.display_name));
        let got = calendar_event_count(conn, cal_id);
        assert_eq!(
            got as usize,
            plan.items.len(),
            "{}: calendar {} event count {got} != seeded {}",
            seed.username,
            plan.display_name,
            plan.items.len()
        );
    }
}

fn assert_contact_collection_exact(conn: &Connection, seed: &AccountSeed) {
    let names = collection_names(conn, "address_books");
    let expected: HashSet<String> = seed
        .address_books
        .iter()
        .map(|c| c.display_name.clone())
        .collect();
    assert_eq!(
        names, expected,
        "{}: address book displayname set mismatch",
        seed.username
    );
    for plan in &seed.address_books {
        let book_id = collection_id_by_name(conn, "address_books", &plan.display_name)
            .unwrap_or_else(|| panic!("address book {} missing", plan.display_name));
        let got = contact_card_count(conn, book_id);
        assert_eq!(
            got as usize,
            plan.items.len(),
            "{}: book {} contact count {got} != seeded {}",
            seed.username,
            plan.display_name,
            plan.items.len()
        );
    }
}

fn assert_event_uids_round_trip(conn: &Connection, seed: &AccountSeed) {
    let imported = all_event_uids(conn);
    let expected = seed.event_uids();
    let missing: Vec<_> = expected.difference(&imported).collect();
    assert!(
        missing.is_empty(),
        "{}: event uids missing from archive: {missing:?}",
        seed.username
    );
}

fn assert_contact_uids_round_trip(conn: &Connection, seed: &AccountSeed) {
    let imported = all_contact_uids(conn);
    let expected = seed.contact_uids();
    let missing: Vec<_> = expected.difference(&imported).collect();
    assert!(
        missing.is_empty(),
        "{}: contact uids missing from archive: {missing:?}",
        seed.username
    );
}

fn assert_event_data_well_formed(conn: &Connection) {
    let mut stmt = conn
        .prepare("SELECT data, data_type FROM calendar_events")
        .expect("prepare");
    let rows: Vec<(String, String)> = stmt
        .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
        .expect("query")
        .filter_map(|r| r.ok())
        .collect();
    assert!(!rows.is_empty(), "no calendar_events rows");
    for (raw, data_type) in rows {
        assert_eq!(
            data_type, "Event",
            "all VEVENT seeds must land as Event, got data_type={data_type}, raw={raw}"
        );
        let v: Value = serde_json::from_str(&raw)
            .unwrap_or_else(|e| panic!("calendar_events.data invalid JSON: {e}; raw={raw}"));
        let obj = v.as_object().expect("event data is object");
        let kind = obj
            .get("@type")
            .or_else(|| obj.get("type"))
            .and_then(Value::as_str)
            .unwrap_or("");
        assert_eq!(kind, "Event", "JSCalendar @type must be Event, got {kind}");
        assert!(obj.contains_key("uid"), "event JSON missing uid: {obj:?}");
        assert!(
            obj.contains_key("start"),
            "event JSON missing start: {obj:?}"
        );
    }
}

fn assert_contact_data_well_formed(conn: &Connection) {
    let mut stmt = conn
        .prepare("SELECT uid, data FROM contact_cards")
        .expect("prepare");
    let rows: Vec<(String, String)> = stmt
        .query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))
        .expect("query")
        .filter_map(|r| r.ok())
        .collect();
    assert!(!rows.is_empty(), "no contact_cards rows");
    for (uid, raw) in rows {
        assert!(!uid.is_empty(), "contact uid empty");
        let v: Value = serde_json::from_str(&raw)
            .unwrap_or_else(|e| panic!("contact_cards.data invalid JSON: {e}; raw={raw}"));
        let obj = v.as_object().expect("contact data is object");
        let kind = obj
            .get("@type")
            .or_else(|| obj.get("type"))
            .and_then(Value::as_str)
            .unwrap_or("");
        assert_eq!(kind, "Card", "JSContact @type must be Card, got {kind}");
    }
}
