#![forbid(unsafe_code)]

mod support;

use std::{collections::BTreeMap, fs, num::NonZeroUsize, path::Path};

use ironstorage::{
    command::{EXIT_FAILURE, GrepRequest, Presentation, ShowRequest},
    crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
    read::{PresentationChannel, ReadError, ShowOutput, ShowResult, TreeNodeKind, VaultReader},
    repository::{DirectoryPath, EntryPath, Repository, RepositoryError, SecretBytes},
};
use support::compatibility::{FixtureSet, TestResult};

struct FixtureSecrets {
    values: BTreeMap<String, Vec<u8>>,
}

impl FixtureSecrets {
    fn all(fixture: &FixtureSet) -> Self {
        Self {
            values: fixture
                .generated
                .keys
                .iter()
                .map(|key| {
                    (
                        key.primary_fingerprint.clone(),
                        key.passphrase.as_bytes().to_vec(),
                    )
                })
                .collect(),
        }
    }
}

impl SecretProvider for FixtureSecrets {
    fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
        self.values
            .get(key.fingerprint().as_str())
            .cloned()
            .map(SecretBytes::new)
            .ok_or(SecretProviderError::Unavailable)
    }
}

#[test]
fn deterministic_root_and_subtree_models_render_without_ciphertext_suffixes() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);

    let root = reader.list(&DirectoryPath::root())?;
    assert_eq!(root.title(), "Password Store");
    assert_eq!(
        root.render_plain(),
        concat!(
            "Password Store\n",
            "├── email\n",
            "│   └── personal\n",
            "├── otp\n",
            "│   ├── hotp\n",
            "│   └── totp\n",
            "├── shared\n",
            "│   └── multiple\n",
            "├── team\n",
            "│   └── service\n",
            "└── unicode\n",
            "    └── 咖啡\n",
        )
    );
    assert!(!root.render_plain().contains(".gpg"));
    assert!(!root.render_plain().contains(".gpg-id"));

    let team = reader.list(&DirectoryPath::parse("team")?)?;
    assert_eq!(team.title(), "team");
    assert_eq!(team.render_plain(), "team\n└── service\n");
    assert!(matches!(
        reader.list(&DirectoryPath::parse("missing")?),
        Err(ReadError::Repository(RepositoryError::NotFound { .. }))
    ));
    Ok(())
}

#[test]
fn explicit_and_implicit_show_dispatch_to_entry_or_directory() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);
    let mut provider = FixtureSecrets::all(&fixture);

    let expected = fixture.read("expected/basic/email/personal.txt")?;
    match reader.show(Some("email/personal"), &mut provider)? {
        ShowResult::Entry(secret) => assert_eq!(secret.expose(), expected),
        ShowResult::Directory(_) => panic!("entry dispatched as directory"),
    }
    match reader.show(Some("team"), &mut provider)? {
        ShowResult::Directory(tree) => assert_eq!(tree.render_plain(), "team\n└── service\n"),
        ShowResult::Entry(_) => panic!("directory dispatched as entry"),
    }
    match reader.show(None, &mut provider)? {
        ShowResult::Directory(tree) => assert_eq!(tree.title(), "Password Store"),
        ShowResult::Entry(_) => panic!("default show did not list root"),
    }
    assert!(matches!(
        reader.show(Some("does-not-exist"), &mut provider),
        Err(ReadError::Repository(RepositoryError::NotFound { .. }))
    ));
    Ok(())
}

#[test]
fn typed_clipboard_and_qr_requests_select_lines_without_rendered_state() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);
    let mut provider = FixtureSecrets::all(&fixture);

    let clipboard = ShowRequest {
        entry: Some("email/personal".to_owned()),
        presentation: Presentation::Clipboard {
            line: NonZeroUsize::new(1).expect("nonzero"),
        },
    };
    let ShowOutput::Present(clipboard) = reader.execute_show(&clipboard, &mut provider)? else {
        panic!("clipboard request was not selected")
    };
    assert_eq!(clipboard.channel(), PresentationChannel::Clipboard);
    assert_eq!(clipboard.line(), 1);
    assert_eq!(clipboard.contents().expose(), b"correct horse fixture");
    assert!(!format!("{clipboard:?}").contains("correct horse"));

    let qr = ShowRequest {
        entry: Some("email/personal".to_owned()),
        presentation: Presentation::QrCode {
            line: NonZeroUsize::new(2).expect("nonzero"),
        },
    };
    let ShowOutput::Present(qr) = reader.execute_show(&qr, &mut provider)? else {
        panic!("QR request was not selected")
    };
    assert_eq!(qr.channel(), PresentationChannel::QrCode);
    assert_eq!(qr.contents().expose(), b"login: alice@example.test");

    let missing = reader.select_line(&EntryPath::parse("email/personal")?, 99, &mut provider);
    assert!(matches!(
        &missing,
        Err(ReadError::LineNotFound { line: 99, .. })
    ));
    assert_eq!(missing.unwrap_err().exit_code(), EXIT_FAILURE);
    Ok(())
}

#[test]
fn find_matches_entry_and_directory_names_case_insensitively() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);

    let results = reader.find(&["service".to_owned(), "咖啡".to_owned()])?;
    assert_eq!(
        results
            .matches()
            .iter()
            .map(|matched| (matched.path(), matched.kind()))
            .collect::<Vec<_>>(),
        vec![
            ("team/service", TreeNodeKind::Entry),
            ("unicode/咖啡", TreeNodeKind::Entry),
        ]
    );
    assert_eq!(
        results.tree().render_plain(),
        concat!(
            "Password Store\n",
            "├── team\n",
            "│   └── service\n",
            "└── unicode\n",
            "    └── 咖啡\n",
        )
    );
    assert!(
        results
            .render_plain()
            .starts_with("Search Terms: service,咖啡\nPassword Store\n")
    );
    let personal = reader.find(&["PERSONAL".to_owned()])?;
    assert_eq!(personal.matches()[0].path(), "email/personal");
    assert_eq!(
        personal.matches()[0].id(),
        &ironstorage::read::TreeNodeId::Entry(EntryPath::parse("email/personal")?)
    );
    assert!(matches!(
        reader.find(&[]),
        Err(ReadError::MissingSearchTerms)
    ));
    Ok(())
}

#[test]
fn decrypted_grep_supports_regex_case_inversion_fixed_strings_and_lines() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);
    let mut provider = FixtureSecrets::all(&fixture);

    let mut numbered = grep("alice@example\\.test");
    numbered.line_number = true;
    let basic = reader.grep(&numbered, &mut provider)?;
    assert_eq!(basic.entries().len(), 1);
    assert_eq!(basic.entries()[0].path().to_string(), "email/personal");
    assert_eq!(basic.entries()[0].lines()[0].number(), 2);
    assert_eq!(
        basic.entries()[0].lines()[0].contents().expose(),
        b"login: alice@example.test"
    );
    assert!(!format!("{basic:?}").contains("alice@example"));
    assert_eq!(
        basic.render_plain().expose(),
        b"email/personal:\n2:login: alice@example.test\n"
    );

    let mut insensitive = grep("FIXTURE");
    insensitive.ignore_case = true;
    assert!(reader.grep(&insensitive, &mut provider)?.entries().len() >= 4);

    let mut fixed = grep("alice@example.test");
    fixed.fixed_strings = true;
    assert_eq!(reader.grep(&fixed, &mut provider)?.entries().len(), 1);

    let mut inverted = grep("fixture");
    inverted.invert_match = true;
    let inverted = reader.grep(&inverted, &mut provider)?;
    assert!(inverted.entries().iter().any(|entry| {
        entry.path().to_string() == "email/personal"
            && entry
                .lines()
                .iter()
                .any(|line| line.contents().expose() == b"login: alice@example.test")
    }));

    let invalid = grep("[");
    assert!(matches!(
        reader.grep(&invalid, &mut provider),
        Err(ReadError::InvalidRegex)
    ));
    Ok(())
}

#[test]
fn search_excludes_extensions_git_metadata_and_ciphertext_names() -> TestResult {
    let fixture = FixtureSet::load()?;
    let store = fixture.materialize_store("basic")?;
    fs::create_dir_all(store.path().join(".extensions/nested"))?;
    fs::copy(
        store.path().join("email/personal.gpg"),
        store.path().join(".extensions/nested/leaked-secret.gpg"),
    )?;
    fs::create_dir(store.path().join(".git"))?;
    fs::write(store.path().join(".git/secret.gpg"), b"not a packet")?;
    fs::write(store.path().join("visible-metadata.txt"), b"fixture")?;
    let repository = Repository::open(store.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let reader = VaultReader::new(&repository, &keys);
    let mut provider = FixtureSecrets::all(&fixture);

    let tree = reader.list(&DirectoryPath::root())?.render_plain();
    assert!(!tree.contains("extensions"));
    assert!(!tree.contains(".git"));
    assert!(!tree.contains("metadata"));
    assert!(!tree.contains(".gpg"));
    assert!(reader.find(&["leaked".to_owned()])?.matches().is_empty());
    assert!(reader.find(&["metadata".to_owned()])?.matches().is_empty());
    assert!(
        reader
            .grep(&grep("correct horse"), &mut provider)?
            .entries()
            .iter()
            .all(|entry| !entry.path().as_path().starts_with(Path::new(".extensions")))
    );
    Ok(())
}

fn grep(pattern: &str) -> GrepRequest {
    GrepRequest {
        pattern: pattern.to_owned(),
        ignore_case: false,
        invert_match: false,
        line_number: false,
        fixed_strings: false,
    }
}
