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

mod integration;
mod seeder;

use std::path::{Path, PathBuf};

use integration::stalwart::shared as shared_stalwart;
use rusqlite::Connection;

fn base_url() -> &'static str {
    shared_stalwart().base_url()
}

fn sieve_url() -> String {
    let s = shared_stalwart();
    format!("sieve://{}:{}", s.host, s.sieve_port)
}

fn sieves_url() -> String {
    let s = shared_stalwart();
    format!("sieves://{}:{}", s.host, s.sieve_port)
}
use vandelay::jmap::account::AccountSelector;
use vandelay::jmap::http::Auth;
use vandelay::logging::Logger;
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
use vandelay::sync::{self, CommonConfig, ConnectConfig, ImportConfig};

fn tmp_archive(tag: &str) -> PathBuf {
    let mut p = std::env::temp_dir();
    p.push(format!(
        "vandelay-managesieve-{tag}-{}-{}.sqlite",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    let _ = std::fs::remove_file(&p);
    p
}

fn common(archive: &Path) -> CommonConfig {
    CommonConfig {
        archive: archive.to_path_buf(),
        threads: 1,
        dry_run: false,
        max_retries: 3,
        allow_invalid_certs: true,
        logger: Logger::from_flags(false, 0),
    }
}

fn basic_config(localpart: &str) -> ManageSieveImportConfig {
    ManageSieveImportConfig {
        url: sieve_url(),
        auth: ManageSieveAuth::Basic {
            user: format!("{localpart}@{}", seeder::DOMAIN),
            password: seeder::USER_PASSWORD.to_owned(),
        },
        allow_cleartext: false,
        allow_source_change: false,
    }
}

fn count(conn: &Connection, table: &str) -> i64 {
    conn.query_row(&format!("SELECT count(*) FROM {table}"), [], |r| r.get(0))
        .unwrap()
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_import_test1_lands_active_sieve_script() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test1").expect("test1");
    let archive = tmp_archive("test1");

    assert_eq!(fx.domain, seeder::DOMAIN);
    assert!(!fx.domain_id.is_empty(), "domain id resolved");
    assert_eq!(
        fx.admin_login,
        (
            seeder::ADMIN_USER.to_owned(),
            seeder::ADMIN_PASSWORD.to_owned()
        )
    );
    assert!(!fx.base_url.is_empty(), "base url set");
    assert!(!acc.account_id.is_empty(), "JMAP account id resolved");
    assert!(!acc.email.is_empty());
    assert!(!acc.admin_role, "test1 is a regular user");
    if let Some(seeded) = &acc.seeded {
        assert_eq!(
            seeded.sieve_active,
            Some(true),
            "test1 layout activates a sieve"
        );
        assert!(seeded.emails > 0);
        assert!(seeded.contacts > 0);
        assert!(seeded.events > 0);
        assert!(seeded.file_nodes > 0);
        assert!(seeded.address_books > 0);
        assert!(seeded.calendars > 0);
        assert!(seeded.identity);
        assert!(seeded.mailboxes_created > 0);
    }
    assert_eq!(acc.password, seeder::USER_PASSWORD);

    let summary = sync::import_managesieve::run(common(&archive), basic_config(&acc.localpart))
        .expect("import");
    assert!(!summary.any_failed(), "import had failures: {summary:?}");

    let conn = Connection::open(&archive).unwrap();
    let scripts = count(&conn, "sieve_scripts");
    assert!(scripts >= 2, "test1 layout seeds two scripts");

    let active: i64 = conn
        .query_row(
            "SELECT count(*) FROM sieve_scripts WHERE is_active = 1",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(active, 1, "exactly one of test1's two scripts is active");

    let blob_count = count(&conn, "blobs");
    assert!(blob_count >= 1, "blob interned for the script body");

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_import_test2_lands_inactive_sieve_script() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test2").expect("test2");
    let archive = tmp_archive("test2");

    let summary = sync::import_managesieve::run(common(&archive), basic_config(&acc.localpart))
        .expect("import");
    assert!(!summary.any_failed(), "import had failures: {summary:?}");

    let conn = Connection::open(&archive).unwrap();
    let scripts = count(&conn, "sieve_scripts");
    assert!(scripts >= 1, "expected at least one sieve script for test2");
    let active: i64 = conn
        .query_row(
            "SELECT count(*) FROM sieve_scripts WHERE is_active = 1",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(active, 0, "test2's seeded script should be inactive");

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_import_test3_has_no_scripts() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test3").expect("test3");
    let archive = tmp_archive("test3");

    let summary = sync::import_managesieve::run(common(&archive), basic_config(&acc.localpart))
        .expect("import");
    assert!(!summary.any_failed(), "import had failures: {summary:?}");

    let conn = Connection::open(&archive).unwrap();
    assert_eq!(count(&conn, "sieve_scripts"), 0);

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_second_run_is_convergent() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test1").expect("test1");
    let archive = tmp_archive("converge");

    let first = sync::import_managesieve::run(common(&archive), basic_config(&acc.localpart))
        .expect("first import");
    let (_, c1) = &first.per_type[0];
    assert!(
        c1.created >= 2,
        "test1 seeds two scripts, both new on first run"
    );

    let summary = sync::import_managesieve::run(common(&archive), basic_config(&acc.localpart))
        .expect("second import");
    let (_, c) = &summary.per_type[0];
    assert_eq!(c.created, 0, "convergent run should create nothing");
    assert_eq!(c.deleted, 0, "convergent run should delete nothing");
    assert_eq!(c.fetched, 0, "convergent run should not rewrite blobs");
    assert!(c.skipped >= 2, "both present scripts should be skipped");

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_and_jmap_imports_share_blob_bytes() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test1").expect("test1");
    let msieve_archive = tmp_archive("parity-msieve");
    let jmap_archive = tmp_archive("parity-jmap");

    sync::import_managesieve::run(common(&msieve_archive), basic_config(&acc.localpart))
        .expect("managesieve import");

    let jmap_cfg = ImportConfig {
        connect: ConnectConfig {
            url: fx.base_url.clone(),
            auth: Auth::Basic {
                user: acc.email.clone(),
                password: seeder::USER_PASSWORD.to_owned(),
            },
            account: AccountSelector::Id(acc.account_id.clone()),
        },
        objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
        allow_source_change: false,
    };
    let jmap_common = CommonConfig {
        archive: jmap_archive.clone(),
        threads: 4,
        dry_run: false,
        max_retries: 5,
        allow_invalid_certs: true,
        logger: Logger::from_flags(false, 0),
    };
    sync::import_jmap::run(jmap_common, jmap_cfg).expect("jmap import");

    let msieve_conn = Connection::open(&msieve_archive).unwrap();
    let jmap_conn = Connection::open(&jmap_archive).unwrap();

    fn name_hash_active(conn: &Connection) -> Vec<(String, Vec<u8>, bool)> {
        let mut stmt = conn
            .prepare(
                "SELECT s.name, b.hash, s.is_active
                 FROM sieve_scripts s JOIN blobs b ON b.id = s.blob_id
                 ORDER BY s.name",
            )
            .unwrap();
        let rows = stmt
            .query_map([], |r| {
                let name: String = r.get(0)?;
                let hash: Vec<u8> = r.get(1)?;
                let active: i64 = r.get(2)?;
                Ok((name, hash, active != 0))
            })
            .unwrap();
        rows.filter_map(|r| r.ok()).collect()
    }

    let msieve = name_hash_active(&msieve_conn);
    let jmap = name_hash_active(&jmap_conn);
    assert!(
        !msieve.is_empty(),
        "managesieve archive must have at least one script row"
    );
    assert_eq!(
        msieve, jmap,
        "every script's (name, blake3, is_active) must match across protocols"
    );

    let _ = std::fs::remove_file(&msieve_archive);
    let _ = std::fs::remove_file(&jmap_archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_dry_run_reports_diff_without_writing() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test1").expect("test1");
    let archive = tmp_archive("dryrun");

    let mut common_cfg = common(&archive);
    common_cfg.dry_run = true;
    let summary =
        sync::import_managesieve::run(common_cfg, basic_config(&acc.localpart)).expect("dryrun");
    let (_, c) = &summary.per_type[0];
    assert!(
        c.created >= 1,
        "expected at least one 'new' in dry-run plan"
    );

    let conn = Connection::open(&archive).unwrap();
    assert_eq!(
        count(&conn, "sieve_scripts"),
        0,
        "dry-run must not write rows"
    );
    assert_eq!(count(&conn, "blobs"), 0, "dry-run must not intern blobs");

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_source_change_protection_refuses_second_account() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc1 = fx.account("test1").expect("test1");
    let acc2 = fx.account("test2").expect("test2");
    let archive = tmp_archive("source_change");

    sync::import_managesieve::run(common(&archive), basic_config(&acc1.localpart))
        .expect("first import");

    let err = sync::import_managesieve::run(common(&archive), basic_config(&acc2.localpart))
        .expect_err("should refuse the second source without override");
    assert!(matches!(err, vandelay::error::Error::SourceChange(_)));

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_implicit_tls_path_succeeds_when_offered() {
    let fx = seeder::provision(base_url()).expect("provision");
    let acc = fx.account("test1").expect("test1");
    let archive = tmp_archive("implicit_tls");

    let mut cfg = basic_config(&acc.localpart);
    cfg.url = sieves_url();
    let result = sync::import_managesieve::run(common(&archive), cfg);

    if let Err(vandelay::error::Error::Connection(msg)) = &result {
        eprintln!("(expected on cleartext-only deployments) {msg}");
    }

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}

#[test]
#[ignore = "requires Docker"]
fn managesieve_round_trip_via_jmap_export_converges() {
    let fx = seeder::provision(base_url()).expect("provision");
    let src = fx.account("test1").expect("test1");
    let dst = fx.account("test4").expect("test4");
    let archive = tmp_archive("roundtrip");

    sync::import_managesieve::run(common(&archive), basic_config(&src.localpart))
        .expect("managesieve import");

    let import_conn = Connection::open(&archive).unwrap();
    let imported_scripts = count(&import_conn, "sieve_scripts");
    assert!(
        imported_scripts >= 1,
        "expected at least one script in the source archive"
    );
    let imported_active: i64 = import_conn
        .query_row(
            "SELECT count(*) FROM sieve_scripts WHERE is_active = 1",
            [],
            |r| r.get(0),
        )
        .unwrap();
    assert_eq!(
        imported_active, 1,
        "test1 should have exactly one active script"
    );
    drop(import_conn);

    let export_common = CommonConfig {
        archive: archive.clone(),
        threads: 4,
        dry_run: false,
        max_retries: 5,
        allow_invalid_certs: true,
        logger: Logger::from_flags(false, 0),
    };
    let export_cfg = vandelay::sync::ExportConfig {
        connect: ConnectConfig {
            url: fx.base_url.clone(),
            auth: Auth::Basic {
                user: dst.email.clone(),
                password: seeder::USER_PASSWORD.to_owned(),
            },
            account: AccountSelector::Id(dst.account_id.clone()),
        },
        objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
        prune: false,
        yes: false,
    };
    let first = sync::export::run(export_common, export_cfg).expect("first export");
    assert!(!first.any_failed(), "first export had failures: {first:?}");
    let created_first: u64 = first
        .per_type
        .iter()
        .filter(|(k, _)| *k == "SieveScript")
        .map(|(_, c)| c.created)
        .sum();
    assert_eq!(
        created_first as i64, imported_scripts,
        "first export should create every imported script on the target ({first:?})"
    );

    let export_common2 = CommonConfig {
        archive: archive.clone(),
        threads: 4,
        dry_run: false,
        max_retries: 5,
        allow_invalid_certs: true,
        logger: Logger::from_flags(false, 0),
    };
    let export_cfg2 = vandelay::sync::ExportConfig {
        connect: ConnectConfig {
            url: fx.base_url.clone(),
            auth: Auth::Basic {
                user: dst.email.clone(),
                password: seeder::USER_PASSWORD.to_owned(),
            },
            account: AccountSelector::Id(dst.account_id.clone()),
        },
        objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
        prune: false,
        yes: false,
    };
    let second = sync::export::run(export_common2, export_cfg2).expect("second export");
    for (name, counts) in &second.per_type {
        assert_eq!(
            counts.created, 0,
            "{name}: round-trip second export should create nothing"
        );
        assert_eq!(
            counts.deleted, 0,
            "{name}: round-trip second export should delete nothing"
        );
    }

    let _ = std::fs::remove_file(&archive);
    seeder::teardown(base_url()).expect("teardown");
}
