//! LastPass CSV importer — parser coverage.

use relicario_core::import_lastpass::{parse_lastpass_csv, ImportWarning};
use relicario_core::item_types::{TotpAlgorithm, TotpKind};
use relicario_core::ItemCore;

const HEADER: &str = "url,username,password,totp,extra,name,grouping,fav";

#[test]
fn single_login_row_round_trips() {
    let csv = format!(
        "{HEADER}\n\
         https://github.com/login,alice,hunter2,,,GitHub,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items.len(), 1, "one item expected");
    assert!(warnings.is_empty(), "no warnings expected");

    let item = &items[0];
    assert_eq!(item.title, "GitHub");
    assert!(!item.favorite);
    assert!(item.group.is_none());
    match &item.core {
        ItemCore::Login(l) => {
            assert_eq!(l.username.as_deref(), Some("alice"));
            assert_eq!(l.password.as_deref().map(String::as_str), Some("hunter2"));
            assert_eq!(l.url.as_ref().map(|u| u.as_str()), Some("https://github.com/login"));
            assert!(l.totp.is_none());
        }
        other => panic!("expected Login, got {:?}", other),
    }
}

#[test]
fn item_id_is_freshly_minted() {
    // Decision D12: title collisions don't dedupe; each row gets a fresh ID.
    let csv = format!("{HEADER}\nhttps://x,u,p,,,Same,,\nhttps://x,u,p,,,Same,,");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items.len(), 2);
    assert_ne!(items[0].id, items[1].id, "IDs must be unique even for identical names");
}

// Assertion helper used by later tests.
#[allow(dead_code)]
fn first_warning_message(warnings: &[ImportWarning]) -> String {
    warnings.first().expect("expected at least one warning").message.clone()
}

#[test]
fn grouping_maps_to_item_group() {
    let csv = format!("{HEADER}\nhttps://x,u,p,,,Bank,Finance,");
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty());
    assert_eq!(items[0].group.as_deref(), Some("Finance"));
}

#[test]
fn empty_grouping_yields_none() {
    let csv = format!("{HEADER}\nhttps://x,u,p,,,Bank,,");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(items[0].group.is_none());
}

#[test]
fn fav_one_marks_favorite() {
    let csv = format!("{HEADER}\nhttps://x,u,p,,,Bank,,1");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(items[0].favorite);
}

#[test]
fn fav_zero_or_blank_not_favorite() {
    let csv = format!(
        "{HEADER}\n\
         https://x,u,p,,,Zero,,0\n\
         https://x,u,p,,,Blank,,",
    );
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items.len(), 2);
    assert!(!items[0].favorite);
    assert!(!items[1].favorite);
}

#[test]
fn extra_becomes_notes_for_login() {
    let csv = format!("{HEADER}\nhttps://x,u,p,,a hint,Bank,,");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items[0].notes.as_deref(), Some("a hint"));
}

#[test]
fn multiline_extra_round_trips_via_quoting() {
    // CSV double-quotes escape embedded newlines.
    let csv = format!(
        "{HEADER}\n\
         https://x,u,p,,\"line1\nline2\nline3\",Bank,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty(), "multi-line extra should parse cleanly");
    assert_eq!(items[0].notes.as_deref(), Some("line1\nline2\nline3"));
}

#[test]
fn login_with_valid_totp_secret_attaches_config() {
    // RFC 4648 base32 of b"12345678901234567890" → "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".
    let csv = format!(
        "{HEADER}\n\
         https://github.com/login,alice,hunter2,GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ,,GitHub,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty());
    match &items[0].core {
        ItemCore::Login(l) => {
            let totp = l.totp.as_ref().expect("expected TOTP config");
            assert_eq!(totp.algorithm, TotpAlgorithm::Sha1);
            assert_eq!(totp.digits, 6);
            assert_eq!(totp.period_seconds, 30);
            assert_eq!(totp.kind, TotpKind::Totp);
            assert_eq!(totp.secret.as_slice(), b"12345678901234567890");
        }
        other => panic!("expected Login, got {:?}", other),
    }
}

#[test]
fn login_with_bad_totp_secret_imports_without_totp_and_warns() {
    let csv = format!(
        "{HEADER}\n\
         https://github.com/login,alice,hunter2,!!!!not-base32!!!!,,GitHub,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items.len(), 1, "login should still import");
    match &items[0].core {
        ItemCore::Login(l) => assert!(l.totp.is_none(), "TOTP must be dropped"),
        other => panic!("expected Login, got {:?}", other),
    }
    assert_eq!(warnings.len(), 1);
    let w = &warnings[0];
    assert_eq!(w.title.as_deref(), Some("GitHub"));
    assert!(w.message.contains("TOTP"), "message: {}", w.message);
    assert!(w.message.contains("invalid") || w.message.contains("base32"));
}

#[test]
fn login_with_lowercase_base32_totp_is_accepted() {
    // RFC 4648 is case-insensitive; LastPass exports may use either case.
    let csv = format!(
        "{HEADER}\n\
         https://x,u,p,gezdgnbvgy3tqojqgezdgnbvgy3tqojq,,Acme,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty(), "lowercase base32 must parse");
    match &items[0].core {
        ItemCore::Login(l) => assert!(l.totp.is_some()),
        _ => unreachable!(),
    }
}

#[test]
fn url_http_sn_maps_to_secure_note() {
    let csv = format!(
        "{HEADER}\n\
         http://sn,,,,The body of the note,My Note,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty());
    assert_eq!(items.len(), 1);
    assert_eq!(items[0].title, "My Note");
    match &items[0].core {
        ItemCore::SecureNote(sn) => assert_eq!(sn.body.as_str(), "The body of the note"),
        other => panic!("expected SecureNote, got {:?}", other),
    }
}

#[test]
fn secure_note_does_not_require_password() {
    // SecureNote rows have empty password; that must not trigger the
    // `missing password` skip path (which is Login-only).
    let csv = format!("{HEADER}\nhttp://sn,,,,note text,Title,,");
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty(), "{:?}", warnings);
    assert_eq!(items.len(), 1);
}

#[test]
fn secure_note_passes_through_grouping_and_favorite() {
    let csv = format!("{HEADER}\nhttp://sn,,,,body,Title,Personal,1");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items[0].group.as_deref(), Some("Personal"));
    assert!(items[0].favorite);
}

#[test]
fn secure_note_preserves_structured_extra_verbatim() {
    // LastPass packs structured note data (e.g. credit cards) into `extra`
    // using their own key:value format. We do NOT auto-parse it — verbatim
    // pass-through, per spec D10.
    let csv_body = "NoteType:Credit Card\nNumber:4111111111111111\nCVV:123";
    let csv = format!(
        "{HEADER}\n\
         http://sn,,,,\"{csv_body}\",Visa,,",
        csv_body = csv_body,
    );
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    match &items[0].core {
        ItemCore::SecureNote(sn) => assert_eq!(sn.body.as_str(), csv_body),
        _ => unreachable!(),
    }
}

#[test]
fn login_with_unparseable_url_imports_with_url_none_and_warns() {
    let csv = format!(
        "{HEADER}\n\
         not-a-real-url,alice,hunter2,,,Site,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items.len(), 1);
    match &items[0].core {
        ItemCore::Login(l) => assert!(l.url.is_none()),
        _ => unreachable!(),
    }
    assert_eq!(warnings.len(), 1);
    assert!(warnings[0].message.contains("URL"), "msg: {}", warnings[0].message);
    assert_eq!(warnings[0].title.as_deref(), Some("Site"));
}

#[test]
fn header_with_extra_column_is_rejected() {
    let bad = "url,username,password,totp,extra,name,grouping,fav,EXTRA\nhttps://x,u,p,,,T,,";
    let err = parse_lastpass_csv(bad.as_bytes()).unwrap_err();
    let msg = format!("{err}");
    assert!(msg.contains("LastPass") || msg.contains("expected"), "msg: {msg}");
}

#[test]
fn header_with_wrong_column_order_is_rejected() {
    let swapped = "name,url,username,password,totp,extra,grouping,fav\nT,https://x,u,p,,,,";
    let err = parse_lastpass_csv(swapped.as_bytes()).unwrap_err();
    assert!(format!("{err}").contains("expected"));
}

#[test]
fn quoted_comma_in_extra_parses() {
    let csv = format!(
        "{HEADER}\n\
         https://x,u,p,,\"hint with, a comma\",Site,,",
    );
    let (items, warnings) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert!(warnings.is_empty());
    assert_eq!(items[0].notes.as_deref(), Some("hint with, a comma"));
}

#[test]
fn unicode_title_round_trips() {
    let csv = format!("{HEADER}\nhttps://x,u,p,,,Müllerstraße — café ☕,,");
    let (items, _) = parse_lastpass_csv(csv.as_bytes()).unwrap();
    assert_eq!(items[0].title, "Müllerstraße — café ☕");
}

#[test]
fn empty_csv_after_header_returns_empty_vecs() {
    let (items, warnings) = parse_lastpass_csv(HEADER.as_bytes()).unwrap();
    assert!(items.is_empty());
    assert!(warnings.is_empty());
}

#[test]
fn missing_header_is_rejected() {
    // Empty input — csv reader treats first row as header (which doesn't exist).
    let err = parse_lastpass_csv(b"").unwrap_err();
    let msg = format!("{err}");
    // Either ImportCsvHeader (header didn't match) or ImportCsvFormat (read
    // failed). Both are acceptable; we just need a clear error.
    assert!(msg.contains("LastPass") || msg.contains("CSV"), "msg: {msg}");
}
