#![forbid(unsafe_code)]

mod support;

use std::{
    collections::{BTreeMap, VecDeque},
    error::Error,
    num::NonZeroUsize,
    sync::{Arc, Mutex},
    time::Duration,
};

use ironstorage::{
    config::ConfigLoader,
    crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider as _, SecretProviderError},
    git::{GitCredentialProvider as _, GitError},
    repository::{EncryptedEntry, SecretBytes},
    secret_store::{
        OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError, SecretCachePolicy, SecretLocator,
        SecretProtection, SecretProtectionPolicy, SecretReference, SecretStore, SecretStoreBackend,
        SecretStoreError,
    },
};
use support::compatibility::FixtureSet;

type TestResult = Result<(), Box<dyn Error>>;

#[derive(Default)]
struct MemoryState {
    values: BTreeMap<SecretLocator, SecretBytes>,
    fault: Option<SecretStoreError>,
    create_fault: Option<SecretStoreError>,
    retrieves: usize,
    protections: Vec<SecretProtection>,
}

#[derive(Clone, Default)]
struct MemoryBackend(Arc<Mutex<MemoryState>>);

impl MemoryBackend {
    fn fail_next(&self, error: SecretStoreError) {
        self.0.lock().expect("test mutex").fault = Some(error);
    }

    fn fail_next_create(&self, error: SecretStoreError) {
        self.0.lock().expect("test mutex").create_fault = Some(error);
    }

    fn corrupt_first(&self) {
        let mut state = self.0.lock().expect("test mutex");
        let value = state.values.values_mut().next().expect("stored test value");
        *value = SecretBytes::new(b"not an IronStorage record".to_vec());
    }

    fn retrieves(&self) -> usize {
        self.0.lock().expect("test mutex").retrieves
    }

    fn protections(&self) -> Vec<SecretProtection> {
        self.0.lock().expect("test mutex").protections.clone()
    }

    fn take_fault(state: &mut MemoryState) -> Result<(), SecretStoreError> {
        match state.fault.take() {
            Some(error) => Err(error),
            None => Ok(()),
        }
    }
}

impl SecretStoreBackend for MemoryBackend {
    fn create(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
        value: &[u8],
    ) -> Result<(), SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)?;
        if let Some(error) = state.create_fault.take() {
            return Err(error);
        }
        state.protections.push(protection);
        if state.values.contains_key(locator) {
            return Err(SecretStoreError::AlreadyExists);
        }
        state
            .values
            .insert(locator.clone(), SecretBytes::new(value.to_vec()));
        Ok(())
    }

    fn retrieve(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
    ) -> Result<SecretBytes, SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)?;
        state.retrieves += 1;
        state.protections.push(protection);
        state
            .values
            .get(locator)
            .map(|value| SecretBytes::new(value.expose().to_vec()))
            .ok_or(SecretStoreError::Missing)
    }

    fn replace(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
        value: &[u8],
    ) -> Result<(), SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)?;
        state.protections.push(protection);
        let existing = state
            .values
            .get_mut(locator)
            .ok_or(SecretStoreError::Missing)?;
        *existing = SecretBytes::new(value.to_vec());
        Ok(())
    }

    fn delete(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
    ) -> Result<(), SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)?;
        state.protections.push(protection);
        state
            .values
            .remove(locator)
            .map(drop)
            .ok_or(SecretStoreError::Missing)
    }

    fn lock(&self) -> Result<(), SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)
    }

    fn unlock(&self) -> Result<(), SecretStoreError> {
        let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
        Self::take_fault(&mut state)
    }
}

#[derive(Clone, Default)]
struct MemoryPrompt(Arc<Mutex<MemoryPromptState>>);

#[derive(Default)]
struct MemoryPromptState {
    responses: VecDeque<Result<Vec<u8>, OpenPgpPassphrasePromptError>>,
    requested: Vec<String>,
}

impl MemoryPrompt {
    fn responding(response: Result<&[u8], OpenPgpPassphrasePromptError>) -> Self {
        let prompt = Self::default();
        prompt
            .0
            .lock()
            .expect("test mutex")
            .responses
            .push_back(response.map(<[u8]>::to_vec));
        prompt
    }

    fn requested(&self) -> Vec<String> {
        self.0.lock().expect("test mutex").requested.clone()
    }
}

impl OpenPgpPassphrasePrompt for MemoryPrompt {
    fn request_passphrase(
        &mut self,
        key: &KeyInfo,
    ) -> Result<SecretBytes, OpenPgpPassphrasePromptError> {
        let mut state = self.0.lock().expect("test mutex");
        state.requested.push(key.fingerprint().as_str().to_owned());
        state
            .responses
            .pop_front()
            .unwrap_or(Err(OpenPgpPassphrasePromptError::Unavailable))
            .map(SecretBytes::new)
    }
}

fn store(backend: MemoryBackend) -> SecretStore<MemoryBackend> {
    SecretStore::new(
        backend,
        SecretCachePolicy::Disabled,
        SecretProtectionPolicy::device_unlocked(),
    )
}

fn fingerprint_reference() -> SecretReference {
    SecretReference::openpgp_passphrase("0123456789ABCDEF0123456789ABCDEF01234567")
        .expect("valid fingerprint")
}

#[test]
fn lifecycle_is_explicit_and_create_never_silently_replaces() -> TestResult {
    let backend = MemoryBackend::default();
    let store = store(backend);
    let reference = fingerprint_reference();

    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Locked)
    ));
    store.unlock()?;
    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    store.create(&reference, SecretBytes::new(b"first".to_vec()))?;
    assert_eq!(store.retrieve(&reference)?.expose(), b"first");
    assert_eq!(
        store.create(&reference, SecretBytes::new(b"other".to_vec())),
        Err(SecretStoreError::AlreadyExists)
    );
    store.replace(&reference, SecretBytes::new(b"second".to_vec()))?;
    assert_eq!(store.retrieve(&reference)?.expose(), b"second");
    store.delete(&reference)?;
    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    assert_eq!(store.delete(&reference), Err(SecretStoreError::Missing));
    store.lock()?;
    assert!(store.is_locked());
    Ok(())
}

#[test]
fn denied_cancelled_unavailable_and_corrupted_are_typed_and_redacted() -> TestResult {
    let backend = MemoryBackend::default();
    backend.fail_next(SecretStoreError::Cancelled);
    let store = store(backend.clone());
    assert_eq!(store.unlock(), Err(SecretStoreError::Cancelled));
    assert!(store.is_locked());
    store.unlock()?;

    let reference = SecretReference::https_git_credential(
        "fixture-server",
        "fixture-app",
        "secret-account-name",
    )?;
    backend.fail_next(SecretStoreError::Denied);
    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Denied)
    ));
    backend.fail_next(SecretStoreError::Unavailable);
    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Unavailable)
    ));
    store.create(&reference, SecretBytes::new(b"token".to_vec()))?;
    backend.corrupt_first();
    assert!(matches!(
        store.retrieve(&reference),
        Err(SecretStoreError::Corrupted)
    ));

    let rendered = format!("{reference:?} {store:?} {}", SecretStoreError::Corrupted);
    assert!(!rendered.contains("secret-account-name"));
    assert!(!rendered.contains("not an IronStorage record"));
    Ok(())
}

#[test]
fn bounded_cache_is_cleared_by_lock_and_never_aliases_git_accounts() -> TestResult {
    let backend = MemoryBackend::default();
    let policy = SecretCachePolicy::timed(
        Duration::from_secs(60),
        NonZeroUsize::new(1).expect("non-zero"),
    )?;
    let store = SecretStore::new(
        backend.clone(),
        policy,
        SecretProtectionPolicy::device_unlocked(),
    );
    store.unlock()?;
    let alice = SecretReference::https_git_credential("server", "application", "alice")?;
    store.create(&alice, SecretBytes::new(b"token".to_vec()))?;
    assert_eq!(store.retrieve(&alice)?.expose(), b"token");
    assert_eq!(
        backend.retrieves(),
        0,
        "create populated the explicit cache"
    );

    let bob = SecretReference::https_git_credential("server", "application", "bob")?;
    assert!(matches!(
        store.retrieve(&bob),
        Err(SecretStoreError::Missing)
    ));
    assert_eq!(
        store.replace(&bob, SecretBytes::new(b"other".to_vec())),
        Err(SecretStoreError::Missing)
    );
    assert_eq!(store.delete(&bob), Err(SecretStoreError::Missing));
    assert_eq!(store.retrieve(&alice)?.expose(), b"token");
    let passphrase = fingerprint_reference();
    store.create(&passphrase, SecretBytes::new(b"passphrase".to_vec()))?;
    assert_eq!(store.retrieve(&alice)?.expose(), b"token");
    assert_eq!(backend.retrieves(), 3, "capacity evicted the older record");
    store.lock()?;
    store.unlock()?;
    assert_eq!(store.retrieve(&alice)?.expose(), b"token");
    assert_eq!(backend.retrieves(), 4, "lock discarded cached bytes");
    assert!(
        SecretCachePolicy::timed(
            Duration::from_secs(16 * 60),
            NonZeroUsize::new(1).expect("non-zero")
        )
        .is_err()
    );
    Ok(())
}

#[test]
fn one_unlocked_provider_supplies_openpgp_and_https_git_secrets() -> TestResult {
    let fixture = FixtureSet::load()?;
    let key = fixture.key("alice")?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let backend = MemoryBackend::default();
    let mut store = SecretStore::new(
        backend.clone(),
        SecretCachePolicy::Disabled,
        SecretProtectionPolicy::user_presence_for_openpgp(),
    );
    store.unlock()?;
    let passphrase = SecretReference::openpgp_passphrase(&key.primary_fingerprint)?;
    store.create(
        &passphrase,
        SecretBytes::new(key.passphrase.as_bytes().to_vec()),
    )?;
    let entry = fixture
        .generated
        .entries
        .iter()
        .find(|entry| entry.store == "basic" && entry.path == "email/personal.gpg")
        .expect("compatibility entry exists");
    assert_eq!(
        keys.decrypt(
            &EncryptedEntry::new(fixture.read(format!("stores/basic/{}", entry.path))?),
            &mut store,
        )?
        .expose(),
        fixture.read("expected/basic/email/personal.txt")?
    );

    let temporary = tempfile::tempdir()?;
    fs_config(&temporary)?;
    let config = ConfigLoader::new(temporary.path().to_owned(), temporary.path().join("native"))
        .load(Some(&temporary.path().join("config.toml")))?;
    let remote = &config.git_remotes()[0];
    backend.fail_next(SecretStoreError::Cancelled);
    assert!(matches!(
        store.credential(remote.server_id(), remote.application_id()),
        Err(GitError::CredentialCancelled)
    ));
    backend.fail_next(SecretStoreError::Denied);
    assert!(matches!(
        store.credential(remote.server_id(), remote.application_id()),
        Err(GitError::CredentialAccessDenied)
    ));
    let git = SecretReference::https_git_credential(
        remote.server_id().as_str(),
        remote.application_id().as_str(),
        "alice",
    )?;
    store.create(&git, SecretBytes::new(b"https-token".to_vec()))?;
    let credential = store.credential(remote.server_id(), remote.application_id())?;
    assert_eq!(credential.username(), "alice");
    assert_eq!(credential.password(), b"https-token");
    store.store_https_git_credential(
        remote.server_id(),
        remote.application_id(),
        "bob",
        SecretBytes::new(b"replacement-token".to_vec()),
    )?;
    assert!(matches!(
        store.retrieve(&git),
        Err(SecretStoreError::Missing)
    ));
    let credential = store.credential(remote.server_id(), remote.application_id())?;
    assert_eq!(credential.username(), "bob");
    assert_eq!(credential.password(), b"replacement-token");
    assert!(
        backend
            .protections()
            .contains(&SecretProtection::RequireUserPresence)
    );
    Ok(())
}

#[test]
fn missing_openpgp_passphrases_are_prompted_verified_and_only_then_persisted() -> TestResult {
    let fixture = FixtureSet::load()?;
    let key = fixture.key("alice")?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let entry = fixture
        .generated
        .entries
        .iter()
        .find(|entry| entry.store == "basic" && entry.path == "email/personal.gpg")
        .expect("compatibility entry exists");
    let ciphertext = EncryptedEntry::new(fixture.read(format!("stores/basic/{}", entry.path))?);
    let expected = fixture.read("expected/basic/email/personal.txt")?;
    let reference = SecretReference::openpgp_passphrase(&key.primary_fingerprint)?;
    let key_info = keys
        .infos()
        .find(|info| info.fingerprint().as_str() == key.primary_fingerprint)
        .expect("fixture key info");

    let mut unprompted = store(MemoryBackend::default());
    unprompted.unlock()?;
    assert!(matches!(
        unprompted.secret_for(&key_info),
        Err(SecretProviderError::Missing)
    ));

    let backend = MemoryBackend::default();
    let prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
    let mut provisioned = store(backend).with_openpgp_passphrase_prompt(prompt.clone());
    provisioned.unlock()?;
    assert_eq!(
        keys.decrypt(&ciphertext, &mut provisioned)?.expose(),
        expected
    );
    assert_eq!(
        provisioned.retrieve(&reference)?.expose(),
        key.passphrase.as_bytes()
    );
    assert_eq!(
        keys.decrypt(&ciphertext, &mut provisioned)?.expose(),
        expected
    );
    assert_eq!(
        prompt.requested(),
        std::slice::from_ref(&key.primary_fingerprint)
    );

    let wrong_backend = MemoryBackend::default();
    let wrong_prompt = MemoryPrompt::responding(Ok(b"incorrect passphrase"));
    let mut wrong = store(wrong_backend).with_openpgp_passphrase_prompt(wrong_prompt.clone());
    wrong.unlock()?;
    let wrong_error = keys
        .decrypt(&ciphertext, &mut wrong)
        .expect_err("incorrect prompted passphrase must fail");
    assert!(matches!(&wrong_error, CryptoError::DecryptionFailed));
    assert!(!format!("{wrong_error:?} {wrong_error}").contains("incorrect passphrase"));
    assert!(matches!(
        wrong.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    assert_eq!(
        wrong_prompt.requested(),
        std::slice::from_ref(&key.primary_fingerprint)
    );

    let cancelled_prompt = MemoryPrompt::responding(Err(OpenPgpPassphrasePromptError::Cancelled));
    let mut cancelled =
        store(MemoryBackend::default()).with_openpgp_passphrase_prompt(cancelled_prompt);
    cancelled.unlock()?;
    assert!(matches!(
        keys.decrypt(&ciphertext, &mut cancelled),
        Err(CryptoError::SecretProvider {
            reason: SecretProviderError::Cancelled,
            ..
        })
    ));
    assert!(matches!(
        cancelled.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));

    let denied_backend = MemoryBackend::default();
    let denied_prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
    let mut denied =
        store(denied_backend.clone()).with_openpgp_passphrase_prompt(denied_prompt.clone());
    denied.unlock()?;
    denied_backend.fail_next(SecretStoreError::Denied);
    assert!(matches!(
        keys.decrypt(&ciphertext, &mut denied),
        Err(CryptoError::SecretProvider {
            reason: SecretProviderError::Denied,
            ..
        })
    ));
    assert!(denied_prompt.requested().is_empty());

    let failed_backend = MemoryBackend::default();
    let failed_prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
    let mut failed = store(failed_backend.clone()).with_openpgp_passphrase_prompt(failed_prompt);
    failed.unlock()?;
    failed_backend.fail_next_create(SecretStoreError::AlreadyExists);
    assert!(matches!(
        keys.decrypt(&ciphertext, &mut failed),
        Err(CryptoError::SecretProvider {
            reason: SecretProviderError::PersistenceFailed,
            ..
        })
    ));
    assert!(matches!(
        failed.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    Ok(())
}

#[test]
fn gnupg_aead_passphrases_are_persisted_only_after_authenticated_decryption() -> TestResult {
    let fixture = FixtureSet::load()?;
    let key = fixture.key("alice")?;
    let keys = KeyStore::load(fixture.path(&key.secret_armor))?;
    let ciphertext = fixture.read(&fixture.gnupg_aead.entry)?;
    let expected = fixture.read(&fixture.gnupg_aead.plaintext)?;
    let reference = SecretReference::openpgp_passphrase(&key.primary_fingerprint)?;

    let prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
    let mut provisioned =
        store(MemoryBackend::default()).with_openpgp_passphrase_prompt(prompt.clone());
    provisioned.unlock()?;
    assert_eq!(
        keys.decrypt(&EncryptedEntry::new(ciphertext.clone()), &mut provisioned)?
            .expose(),
        expected
    );
    assert_eq!(
        provisioned.retrieve(&reference)?.expose(),
        key.passphrase.as_bytes()
    );
    assert_eq!(
        prompt.requested(),
        std::slice::from_ref(&key.primary_fingerprint)
    );

    let wrong_prompt = MemoryPrompt::responding(Ok(b"incorrect AEAD passphrase"));
    let mut wrong =
        store(MemoryBackend::default()).with_openpgp_passphrase_prompt(wrong_prompt.clone());
    wrong.unlock()?;
    assert!(matches!(
        keys.decrypt(&EncryptedEntry::new(ciphertext.clone()), &mut wrong),
        Err(CryptoError::DecryptionFailed)
    ));
    assert!(matches!(
        wrong.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    assert_eq!(
        wrong_prompt.requested(),
        std::slice::from_ref(&key.primary_fingerprint)
    );

    let mut tampered_ciphertext = ciphertext;
    *tampered_ciphertext
        .last_mut()
        .expect("non-empty AEAD fixture") ^= 0x01;
    let tampered_prompt = MemoryPrompt::responding(Ok(key.passphrase.as_bytes()));
    let mut tampered =
        store(MemoryBackend::default()).with_openpgp_passphrase_prompt(tampered_prompt);
    tampered.unlock()?;
    assert!(matches!(
        keys.decrypt(&EncryptedEntry::new(tampered_ciphertext), &mut tampered),
        Err(CryptoError::DecryptionFailed)
    ));
    assert!(matches!(
        tampered.retrieve(&reference),
        Err(SecretStoreError::Missing)
    ));
    Ok(())
}

fn fs_config(temporary: &tempfile::TempDir) -> TestResult {
    std::fs::create_dir_all(temporary.path().join("keys"))?;
    std::fs::create_dir_all(temporary.path().join("vault"))?;
    std::fs::create_dir_all(temporary.path().join("native"))?;
    std::fs::write(
        temporary.path().join("config.toml"),
        "vault = 'vault'\ndefault_key = 'alice'\nkey_material = 'keys'\n[[git.remotes]]\nname = 'origin'\nurl = 'https://example.test/store.git'\nserver_id = 'server'\napplication_id = 'application'\n",
    )?;
    Ok(())
}
