//! Integration tests against Zoho Mail IMAP.
//!
//! These tests run against `imap.zoho.com` (or the host in `DAAKI_ZOHO_HOST`)
//! using an app-specific password.
//!
//! # Setup
//!
//! Provide credentials via `credentials.json` at the workspace root or the
//! `DAAKI_PROVIDER_CREDENTIALS` env var. See `helpers/mod.rs` for the schema.
//!
//! ```sh
//! cargo test --test zoho -- --ignored --test-threads=1
//! ```

#![allow(clippy::unwrap_used, clippy::expect_used)]

mod helpers;

use std::time::Duration;

use daaki_imap::types::{FetchAttr, SequenceSet};
use daaki_imap::{IdleEvent, ImapConnection, SessionState, TlsMode};
use helpers::{skip_unless, IMAPS_PORT, TIMEOUT};
use tokio_util::sync::CancellationToken;

/// Zoho IMAP host — override with `DAAKI_ZOHO_HOST` for regional servers
/// (e.g. `imap.zoho.eu`, `imap.zoho.in`).
fn zoho_host() -> String {
    std::env::var("DAAKI_ZOHO_HOST").unwrap_or_else(|_| "imap.zoho.eu".to_owned())
}

/// Connect to Zoho IMAP with implicit TLS and LOGIN.
async fn connect_zoho() -> ImapConnection {
    let creds = helpers::load_credentials();
    let zoho = creds.zoho.expect("Zoho credentials required");
    let host = zoho_host();
    helpers::connect_provider(&host, &zoho).await
}

// ==========================================================================
// Connection & Authentication
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_connect_and_login() {
    let creds = helpers::load_credentials();
    let zoho = skip_unless!(creds.zoho, "Zoho");
    let host = zoho_host();

    let conn = ImapConnection::connect(&host, IMAPS_PORT, TlsMode::Implicit, TIMEOUT)
        .await
        .unwrap();

    assert_eq!(conn.session_state(), SessionState::NotAuthenticated);

    conn.login(&zoho.username, &zoho.password, TIMEOUT)
        .await
        .unwrap();

    assert_eq!(conn.session_state(), SessionState::Authenticated);

    conn.logout().await.unwrap();
}

// ==========================================================================
// Capabilities
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_capabilities() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let caps = conn.capabilities();

    // Zoho must advertise IMAP4rev1.
    assert!(
        caps.iter()
            .any(|c| matches!(c, daaki_imap::types::Capability::Imap4Rev1)),
        "expected IMAP4rev1 capability, got: {caps:?}"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// LIST mailboxes
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_list_mailboxes() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let mailboxes = conn.list("", "*", TIMEOUT).await.unwrap();

    assert!(
        mailboxes
            .iter()
            .any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
        "expected INBOX in LIST response: {mailboxes:?}"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// SELECT INBOX
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_select_inbox() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
    // UIDVALIDITY must be non-zero per RFC 3501 Section 2.3.1.1.
    assert!(
        selected.uid_validity.unwrap_or(0) > 0,
        "UIDVALIDITY must be > 0"
    );
    assert_eq!(conn.session_state(), SessionState::Selected);

    conn.logout().await.unwrap();
}

// ==========================================================================
// FETCH recent message
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_fetch_recent_message() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let selected = conn.select("INBOX", TIMEOUT).await.unwrap();

    if selected.exists == 0 {
        eprintln!("skipping fetch test: INBOX is empty");
        conn.logout().await.unwrap();
        return;
    }

    let fetches = conn
        .fetch(
            &SequenceSet::new("1").unwrap(),
            &[
                FetchAttr::Uid,
                FetchAttr::Flags,
                FetchAttr::Envelope,
                FetchAttr::Rfc822Size,
                FetchAttr::InternalDate,
            ],
            TIMEOUT,
        )
        .await
        .unwrap();

    assert!(!fetches.is_empty(), "FETCH should return at least one item");

    let msg = &fetches[0];
    assert!(msg.uid.is_some(), "UID should be present");
    assert!(msg.envelope.is_some(), "ENVELOPE should be present");

    conn.logout().await.unwrap();
}

// ==========================================================================
// SEARCH
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_search_all() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    let result = conn.search("ALL", TIMEOUT).await.unwrap();
    // Result may be empty if INBOX has no messages — that's valid.
    let _ = result;

    conn.logout().await.unwrap();
}

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_uid_search() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    let result = conn.uid_search("ALL", TIMEOUT).await.unwrap();
    let _ = result;

    conn.logout().await.unwrap();
}

// ==========================================================================
// NOOP (RFC 3501 Section 6.1.2)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_noop() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.noop(TIMEOUT).await.unwrap();
    conn.logout().await.unwrap();
}

// ==========================================================================
// EXAMINE — read-only SELECT (RFC 3501 Section 6.3.2)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_examine_inbox() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let selected = conn.examine("INBOX", TIMEOUT).await.unwrap();
    assert!(
        selected.uid_validity.unwrap_or(0) > 0,
        "UIDVALIDITY must be > 0"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// STATUS (RFC 3501 Section 6.3.10)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_status_inbox() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let items = conn
        .status("INBOX", "(MESSAGES UNSEEN UIDNEXT UIDVALIDITY)", TIMEOUT)
        .await
        .unwrap()
        .items;

    assert!(!items.is_empty(), "STATUS should return at least one item");

    conn.logout().await.unwrap();
}

// ==========================================================================
// IDLE (RFC 2177)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_idle_and_cancel() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    // Check if IDLE is supported before attempting.
    let has_idle = conn
        .capabilities()
        .iter()
        .any(|c| matches!(c, daaki_imap::types::Capability::Idle));

    if !has_idle {
        eprintln!("skipping: Zoho server does not advertise IDLE capability");
        conn.logout().await.unwrap();
        return;
    }

    conn.select("INBOX", TIMEOUT).await.unwrap();

    let cancel = CancellationToken::new();
    let cancel_clone = cancel.clone();

    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_millis(500)).await;
        cancel_clone.cancel();
    });

    let event = conn.idle(TIMEOUT, cancel).await.unwrap();
    assert_eq!(event, IdleEvent::Cancelled);

    conn.logout().await.unwrap();
}

// ==========================================================================
// NAMESPACE (RFC 2342)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_namespace() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    // Check if NAMESPACE is supported before attempting.
    let has_namespace = conn
        .capabilities()
        .iter()
        .any(|c| matches!(c, daaki_imap::types::Capability::Namespace));

    if !has_namespace {
        eprintln!("skipping: Zoho server does not advertise NAMESPACE capability");
        conn.logout().await.unwrap();
        return;
    }

    let ns = conn.namespace(TIMEOUT).await.unwrap();

    assert!(
        !ns.personal.is_empty(),
        "expected at least one personal namespace"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// UID FETCH (RFC 3501 Section 6.4.8)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_uid_fetch() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let selected = conn.select("INBOX", TIMEOUT).await.unwrap();

    if selected.exists == 0 {
        eprintln!("skipping uid_fetch test: INBOX is empty");
        conn.logout().await.unwrap();
        return;
    }

    // Get a real UID via search, then fetch it — avoids `*` interpretation quirks.
    let uids = conn.uid_search("ALL", TIMEOUT).await.unwrap();
    if uids.ids.is_empty() {
        eprintln!("skipping uid_fetch test: no UIDs from search");
        conn.logout().await.unwrap();
        return;
    }
    let uid = uids.ids[0];
    let uid_set = uid.to_string();

    let fetches = conn
        .uid_fetch(
            &SequenceSet::new(&uid_set).unwrap(),
            &[FetchAttr::Uid, FetchAttr::Flags, FetchAttr::Envelope],
            TIMEOUT,
        )
        .await
        .unwrap();

    assert!(
        !fetches.is_empty(),
        "UID FETCH should return at least one item"
    );
    for msg in &fetches {
        assert!(msg.uid.is_some(), "every UID FETCH result must have a UID");
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// FETCH BODYSTRUCTURE (RFC 3501 Section 6.4.5)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_fetch_bodystructure() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let selected = conn.select("INBOX", TIMEOUT).await.unwrap();

    if selected.exists == 0 {
        eprintln!("skipping bodystructure test: INBOX is empty");
        conn.logout().await.unwrap();
        return;
    }

    let fetches = conn
        .fetch(
            &SequenceSet::new("1").unwrap(),
            &[FetchAttr::Uid, FetchAttr::BodyStructure],
            TIMEOUT,
        )
        .await
        .unwrap();

    assert!(!fetches.is_empty());
    let msg = &fetches[0];
    assert!(
        msg.body_structure.is_some(),
        "BODYSTRUCTURE should be present"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// ID (RFC 2971)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_id() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    match conn
        .id(
            &[("name", Some("daaki-test")), ("version", Some("0.1.0"))],
            TIMEOUT,
        )
        .await
    {
        Ok(server_id) => {
            assert!(
                !server_id.is_empty(),
                "expected server to return ID information"
            );
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise ID capability");
        }
        Err(e) => panic!("unexpected error from ID: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// ENABLE (RFC 5161)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_enable_condstore() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    match conn.enable(&["CONDSTORE"], TIMEOUT).await {
        Ok(enabled) => {
            assert!(
                enabled.iter().any(|s| s.eq_ignore_ascii_case("CONDSTORE")),
                "expected CONDSTORE to be enabled: {enabled:?}"
            );
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise ENABLE capability");
        }
        Err(e) => panic!("unexpected error from ENABLE: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// LSUB (RFC 3501 Section 6.3.9)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_lsub() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let subscribed = conn.lsub("", "*", TIMEOUT).await.unwrap();
    assert!(
        subscribed
            .iter()
            .any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
        "expected INBOX in LSUB response: {:?}",
        subscribed.iter().map(|m| &m.name).collect::<Vec<_>>()
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// UNSELECT (RFC 3691)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_unselect() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();
    assert_eq!(conn.session_state(), SessionState::Selected);

    match conn.unselect(TIMEOUT).await {
        Ok(()) => {
            assert_eq!(conn.session_state(), SessionState::Authenticated);
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise UNSELECT capability");
        }
        Err(e) => panic!("unexpected error from UNSELECT: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// SORT (RFC 5256)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_sort_by_date() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    match conn.sort("DATE", "UTF-8", "ALL", TIMEOUT).await {
        Ok(result) => {
            let _ = result;
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise SORT capability");
        }
        Err(e) => panic!("unexpected error from SORT: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// THREAD (RFC 5256)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_thread_references() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    match conn.thread("REFERENCES", "UTF-8", "ALL", TIMEOUT).await {
        Ok(threads) => {
            let _ = threads;
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise THREAD=REFERENCES capability");
        }
        Err(e) => panic!("unexpected error from THREAD: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// SELECT CONDSTORE (RFC 7162)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_select_condstore() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    match conn
        .select_with(
            "INBOX",
            &daaki_imap::types::SelectOptions::condstore(),
            TIMEOUT,
        )
        .await
    {
        Ok(selected) => {
            assert!(
                selected.uid_validity.unwrap_or(0) > 0,
                "UIDVALIDITY must be > 0"
            );
            assert!(
                selected.highest_mod_seq.is_some(),
                "expected HIGHESTMODSEQ in CONDSTORE SELECT response"
            );
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise CONDSTORE capability");
        }
        Err(e) => panic!("unexpected error from SELECT CONDSTORE: {e:?}"),
    }

    conn.logout().await.unwrap();
}

// ==========================================================================
// SEARCH with criteria (RFC 3501 Section 6.4.4)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_search_unseen() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    let result = conn.search("UNSEEN", TIMEOUT).await.unwrap();
    let _ = result;

    conn.logout().await.unwrap();
}

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_search_since_date() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    let result = conn.search("SINCE 01-Jan-2020", TIMEOUT).await.unwrap();
    let _ = result;

    conn.logout().await.unwrap();
}

// ==========================================================================
// FETCH BODY section (RFC 3501 Section 6.4.5)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_fetch_body_header() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    let selected = conn.select("INBOX", TIMEOUT).await.unwrap();

    if selected.exists == 0 {
        eprintln!("skipping fetch body header test: INBOX is empty");
        conn.logout().await.unwrap();
        return;
    }

    let fetches = conn
        .fetch(
            &SequenceSet::new("1").unwrap(),
            &[FetchAttr::BodySection {
                peek: true,
                section: Some("HEADER".into()),
                partial: None,
            }],
            TIMEOUT,
        )
        .await
        .unwrap();

    assert!(!fetches.is_empty());
    let msg = &fetches[0];
    assert!(
        !msg.body_sections.is_empty(),
        "expected at least one body section"
    );

    conn.logout().await.unwrap();
}

// ==========================================================================
// IDLE timeout (RFC 2177)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_idle_timeout() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let has_idle = conn
        .capabilities()
        .iter()
        .any(|c| matches!(c, daaki_imap::types::Capability::Idle));

    if !has_idle {
        eprintln!("skipping: Zoho server does not advertise IDLE capability");
        conn.logout().await.unwrap();
        return;
    }

    conn.select("INBOX", TIMEOUT).await.unwrap();

    let cancel = CancellationToken::new();
    let event = conn.idle(Duration::from_secs(2), cancel).await.unwrap();

    assert_eq!(event, IdleEvent::Timeout);

    conn.logout().await.unwrap();
}

// ==========================================================================
// CHECK (RFC 3501 Section 6.4.1)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_check() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;
    conn.select("INBOX", TIMEOUT).await.unwrap();

    conn.check(TIMEOUT).await.unwrap();

    conn.logout().await.unwrap();
}

// ==========================================================================
// LIST with pattern (RFC 3501 Section 6.3.8)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_list_inbox_only() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    let mailboxes = conn.list("", "INBOX", TIMEOUT).await.unwrap();
    assert_eq!(
        mailboxes.len(),
        1,
        "LIST for 'INBOX' should return exactly one result: {mailboxes:?}"
    );
    assert!(mailboxes[0].name.as_str().eq_ignore_ascii_case("INBOX"));

    conn.logout().await.unwrap();
}

// ==========================================================================
// COMPRESS=DEFLATE (RFC 4978)
// ==========================================================================

#[tokio::test]
#[ignore = "requires Zoho credentials"]
async fn zoho_compress_deflate() {
    let creds = helpers::load_credentials();
    let _ = skip_unless!(creds.zoho, "Zoho");

    let conn = connect_zoho().await;

    match conn.compress(TIMEOUT).await {
        Ok(()) => {
            let mailboxes = conn.list("", "*", TIMEOUT).await.unwrap();
            assert!(
                mailboxes
                    .iter()
                    .any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
                "LIST over compressed connection should still find INBOX"
            );

            conn.noop(TIMEOUT).await.unwrap();
        }
        Err(daaki_imap::Error::MissingCapability(_)) => {
            eprintln!("skipping: Zoho does not advertise COMPRESS=DEFLATE");
        }
        Err(e) => panic!("unexpected error from COMPRESS: {e:?}"),
    }

    conn.logout().await.unwrap();
}
