#![forbid(unsafe_code)]

mod support;

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

use ironstorage::{
    authentication::{
        AuthenticationClock, AuthenticationError, AuthenticationSession, AuthenticationTimeout,
        MAX_AUTHENTICATION_TIMEOUT,
    },
    crypto::{KeyInfo, KeyStore, SecretProvider as _},
    read::{ShowResult, VaultReader},
    repository::{Repository, SecretBytes},
    secret_store::{
        SecretCachePolicy, SecretLocator, SecretProtection, SecretProtectionPolicy,
        SecretReference, SecretStore, SecretStoreBackend, SecretStoreError,
    },
};
use support::compatibility::FixtureSet;

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

#[derive(Default)]
struct BackendState {
    values: BTreeMap<SecretLocator, SecretBytes>,
    fault: Option<SecretStoreError>,
    retrieves: usize,
    locks: usize,
    unlocks: usize,
    last_protection: Option<SecretProtection>,
}

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

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

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

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

    fn last_protection(&self) -> Option<SecretProtection> {
        self.0.lock().expect("test mutex").last_protection
    }

    fn invalidate_enrollment(&self) {
        self.0.lock().expect("test mutex").values.clear();
    }

    fn take_fault(state: &mut BackendState) -> 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)?;
        state.last_protection = Some(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.last_protection = Some(protection);
        state.retrieves += 1;
        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.last_protection = Some(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.last_protection = Some(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)?;
        state.locks += 1;
        Ok(())
    }

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

#[derive(Clone, Default)]
struct ManualClock(Arc<Mutex<Duration>>);

impl ManualClock {
    fn advance(&self, duration: Duration) {
        let mut now = self.0.lock().expect("test clock");
        *now += duration;
    }
}

impl AuthenticationClock for ManualClock {
    fn now(&self) -> Duration {
        *self.0.lock().expect("test clock")
    }
}

fn fixture_key(fixture: &FixtureSet, keys: &KeyStore, name: &str) -> TestResult<KeyInfo> {
    let fingerprint = &fixture.key(name)?.primary_fingerprint;
    keys.infos()
        .find(|key| key.fingerprint().as_str() == fingerprint)
        .ok_or_else(|| format!("missing imported fixture key {name}").into())
}

fn provision_passphrase(backend: MemoryBackend, key: &KeyInfo, passphrase: &[u8]) -> TestResult {
    let store = SecretStore::new(
        backend,
        SecretCachePolicy::Disabled,
        SecretProtectionPolicy::device_unlocked(),
    );
    store.unlock()?;
    store.create(
        &SecretReference::openpgp_passphrase(key.fingerprint().as_str())?,
        SecretBytes::new(passphrase.to_vec()),
    )?;
    store.lock()?;
    Ok(())
}

fn session(
    backend: MemoryBackend,
    clock: ManualClock,
    seconds: u64,
) -> Result<AuthenticationSession<MemoryBackend, ManualClock>, AuthenticationError> {
    Ok(AuthenticationSession::with_clock(
        backend,
        SecretProtectionPolicy::device_unlocked(),
        AuthenticationTimeout::new(Duration::from_secs(seconds))?,
        clock,
    ))
}

#[test]
fn user_activity_extends_the_lease_but_secret_access_and_timer_polling_do_not() -> TestResult {
    let fixture = FixtureSet::load()?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let alice = fixture_key(&fixture, &keys, "alice")?;
    let backend = MemoryBackend::default();
    provision_passphrase(
        backend.clone(),
        &alice,
        fixture.key("alice")?.passphrase.as_bytes(),
    )?;
    let baseline_locks = backend.locks();
    let clock = ManualClock::default();
    let session = session(backend.clone(), clock.clone(), 120)?;
    let mut handle = session.authenticate(&alice)?;

    assert_eq!(handle.remaining_time()?, Duration::from_secs(120));
    assert_eq!(backend.retrieves(), 1);
    clock.advance(Duration::from_secs(100));
    assert_eq!(session.remaining_time()?, Some(Duration::from_secs(20)));
    assert_eq!(
        handle
            .secret_for(&alice)
            .expect("active lease supplies cached passphrase")
            .expose(),
        b"fixture-alice-passphrase"
    );
    assert_eq!(
        backend.retrieves(),
        1,
        "the lease owns the only memory cache"
    );
    clock.advance(Duration::from_secs(20));
    assert!(session.expire()?);
    assert_eq!(handle.ensure_active(), Err(AuthenticationError::Expired));
    assert_eq!(session.remaining_time()?, None);
    assert_eq!(backend.locks(), baseline_locks + 1);

    let handle = session.authenticate(&alice)?;
    clock.advance(Duration::from_secs(100));
    handle.touch_user_activity()?;
    clock.advance(Duration::from_secs(119));
    assert_eq!(handle.remaining_time()?, Duration::from_secs(1));
    clock.advance(Duration::from_secs(1));
    assert_eq!(handle.ensure_active(), Err(AuthenticationError::Expired));
    Ok(())
}

#[test]
fn manual_lock_cancellation_and_expiry_revoke_all_existing_handles() -> TestResult {
    let fixture = FixtureSet::load()?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let alice = fixture_key(&fixture, &keys, "alice")?;
    let backend = MemoryBackend::default();
    provision_passphrase(
        backend.clone(),
        &alice,
        fixture.key("alice")?.passphrase.as_bytes(),
    )?;
    let clock = ManualClock::default();
    let session = session(backend.clone(), clock.clone(), 10)?;

    let first = session.authenticate(&alice)?;
    let first_clone = first.clone();
    session.manual_lock()?;
    assert_eq!(first.ensure_active(), Err(AuthenticationError::Revoked));
    assert_eq!(
        first_clone.ensure_active(),
        Err(AuthenticationError::Revoked)
    );

    let second = session.authenticate(&alice)?;
    session.cancel()?;
    assert_eq!(second.ensure_active(), Err(AuthenticationError::Cancelled));

    backend.fail_next(SecretStoreError::Cancelled);
    assert!(matches!(
        session.authenticate(&alice),
        Err(AuthenticationError::Cancelled)
    ));
    assert_eq!(session.remaining_time()?, None);
    Ok(())
}

#[test]
fn relock_clears_unlock_material_and_old_handles_cannot_open_entries() -> 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 alice = fixture_key(&fixture, &keys, "alice")?;
    let backend = MemoryBackend::default();
    provision_passphrase(
        backend.clone(),
        &alice,
        fixture.key("alice")?.passphrase.as_bytes(),
    )?;
    let clock = ManualClock::default();
    let session = session(backend.clone(), clock.clone(), 5)?;
    let reader = VaultReader::new(&repository, &keys);
    let mut old_handle = session.authenticate(&alice)?;

    match reader.show(Some("email/personal"), &mut old_handle)? {
        ShowResult::Entry(contents) => assert_eq!(
            contents.expose(),
            fixture.read("expected/basic/email/personal.txt")?
        ),
        ShowResult::Directory(_) => panic!("fixture entry resolved as a directory"),
    }
    assert_eq!(backend.retrieves(), 1);
    session.manual_lock()?;
    assert!(
        reader
            .show(Some("email/personal"), &mut old_handle)
            .is_err()
    );

    let mut new_handle = session.authenticate(&alice)?;
    assert_eq!(
        backend.retrieves(),
        2,
        "relock discarded cached passphrase bytes"
    );
    assert!(matches!(
        reader.show(Some("email/personal"), &mut new_handle)?,
        ShowResult::Entry(_)
    ));
    let debug = format!("{session:?} {new_handle:?}");
    assert!(!debug.contains("fixture-alice-passphrase"));
    Ok(())
}

#[test]
fn exact_deadline_races_are_serialized_and_timeout_validation_is_bounded() -> TestResult {
    assert_eq!(
        AuthenticationTimeout::new(Duration::ZERO),
        Err(AuthenticationError::InvalidTimeout)
    );
    assert_eq!(
        AuthenticationTimeout::new(MAX_AUTHENTICATION_TIMEOUT + Duration::from_secs(1)),
        Err(AuthenticationError::InvalidTimeout)
    );

    let fixture = FixtureSet::load()?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let alice = fixture_key(&fixture, &keys, "alice")?;
    let backend = MemoryBackend::default();
    provision_passphrase(
        backend.clone(),
        &alice,
        fixture.key("alice")?.passphrase.as_bytes(),
    )?;
    let clock = ManualClock::default();
    let session = session(backend.clone(), clock.clone(), 1)?;
    let handle = session.authenticate(&alice)?;
    let baseline_locks = backend.locks();
    clock.advance(Duration::from_secs(1));

    let expiry_session = session.clone();
    let expiry = std::thread::spawn(move || expiry_session.expire());
    let access = std::thread::spawn(move || handle.ensure_active());
    let expiry_result = expiry.join().expect("expiry thread")?;
    let access_result = access.join().expect("access thread");
    assert!(matches!(access_result, Err(AuthenticationError::Expired)));
    assert!(expiry_result || session.remaining_time()?.is_none());
    assert_eq!(backend.locks(), baseline_locks + 1, "expiry relocks once");
    Ok(())
}

#[test]
fn verified_manual_recovery_enrolls_current_biometry_and_reestablishes_the_lease() -> 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 alice = fixture_key(&fixture, &keys, "alice")?;
    let backend = MemoryBackend::default();
    let clock = ManualClock::default();
    let session = AuthenticationSession::with_clock(
        backend.clone(),
        SecretProtectionPolicy::current_biometry_for_openpgp(),
        AuthenticationTimeout::new(Duration::from_secs(30))?,
        clock.clone(),
    );
    let reader = VaultReader::new(&repository, &keys);

    assert!(matches!(
        session.authenticate(&alice),
        Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
    ));
    let mut manual = session.authenticate_with_passphrase(
        &alice,
        SecretBytes::new(fixture.key("alice")?.passphrase.as_bytes().to_vec()),
    )?;
    assert!(matches!(
        reader.show(Some("email/personal"), &mut manual)?,
        ShowResult::Entry(_)
    ));
    manual.persist_passphrase(&alice)?;
    assert_eq!(
        backend.last_protection(),
        Some(SecretProtection::BiometryCurrentSet)
    );

    session.manual_lock()?;
    let mut biometric = session.authenticate(&alice)?;
    assert!(matches!(
        reader.show(Some("email/personal"), &mut biometric)?,
        ShowResult::Entry(_)
    ));
    clock.advance(Duration::from_secs(30));
    assert!(session.expire()?);
    assert_eq!(biometric.ensure_active(), Err(AuthenticationError::Expired));

    backend.invalidate_enrollment();
    assert!(matches!(
        session.authenticate(&alice),
        Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
    ));
    let mut recovered = session.authenticate_with_passphrase(
        &alice,
        SecretBytes::new(fixture.key("alice")?.passphrase.as_bytes().to_vec()),
    )?;
    assert!(matches!(
        reader.show(Some("email/personal"), &mut recovered)?,
        ShowResult::Entry(_)
    ));
    recovered.persist_passphrase(&alice)?;
    session.delete_key_passphrase(&alice)?;
    session.manual_lock()?;
    assert!(matches!(
        session.authenticate(&alice),
        Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
    ));
    Ok(())
}
