#![forbid(unsafe_code)]

mod support;

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

use ironstorage::{
    command::InitRequest,
    crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
    recipient::{
        NoGitCommitter, PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyError,
        RecipientPolicyManager, SigningPolicy,
    },
    repository::{EntryPath, Repository, SecretBytes},
};
use support::compatibility::{FixtureSet, TestResult};

#[derive(Default)]
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)
    }
}

#[derive(Default)]
struct RecordingCommitter {
    changes: Vec<PolicyCommit>,
    fail: bool,
}

impl PolicyCommitter for RecordingCommitter {
    fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError> {
        self.changes.push(change.clone());
        if self.fail {
            Err(PolicyCommitError::new("simulated embedded Git failure"))
        } else {
            Ok(())
        }
    }
}

#[test]
fn resolves_nearest_root_and_nested_recipient_policies() -> 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 manager = RecipientPolicyManager::new(&repository, &keys);
    let alice = fixture.key("alice")?;
    let bob = fixture.key("bob")?;

    let root = manager.resolve_for_entry(&EntryPath::parse("email/personal")?, None)?;
    assert_eq!(root.policy_directory().as_path(), Path::new(""));
    assert_eq!(
        root.recipients()[0].fingerprint().as_str(),
        alice.primary_fingerprint
    );

    let team = manager.resolve_for_entry(&EntryPath::parse("team/service")?, None)?;
    assert_eq!(team.policy_directory().as_path(), Path::new("team"));
    assert_eq!(
        team.recipients()[0].fingerprint().as_str(),
        bob.primary_fingerprint
    );

    let shared = manager.resolve_for_entry(&EntryPath::parse("shared/multiple")?, None)?;
    assert_eq!(shared.policy_directory().as_path(), Path::new("shared"));
    assert_eq!(shared.recipients().len(), 2);
    Ok(())
}

#[test]
fn root_init_reencrypts_only_entries_not_shielded_by_overrides() -> 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 manager = RecipientPolicyManager::new(&repository, &keys);
    let bob = fixture.key("bob")?;
    let mut provider = FixtureSecrets::all(&fixture);
    let mut committer = RecordingCommitter::default();

    let outcome = manager.apply_init(
        &init(None, &[&bob.primary_fingerprint]),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert_eq!(outcome.reencrypted_entries().len(), 4);
    assert_eq!(committer.changes.len(), 1);
    assert_eq!(
        committer.changes[0].changed_paths(),
        vec![
            PathBuf::from(".gpg-id"),
            PathBuf::from("email/personal.gpg"),
            PathBuf::from("otp/hotp.gpg"),
            PathBuf::from("otp/totp.gpg"),
            PathBuf::from("unicode/咖啡.gpg"),
        ]
    );
    assert_eq!(
        fs::read(store.path().join(".gpg-id"))?,
        format!("{}\n", bob.primary_fingerprint).as_bytes()
    );

    let bob_recipient = [keys.resolve(&bob.primary_fingerprint)?];
    for path in ["email/personal", "otp/hotp", "otp/totp", "unicode/咖啡"] {
        assert!(keys.is_encrypted_for(
            &repository.read_entry(&EntryPath::parse(path)?)?,
            &bob_recipient
        )?);
    }
    let before_commits = committer.changes.len();
    let no_op = manager.apply_init(
        &init(None, &[&bob.primary_fingerprint]),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert!(!no_op.committed());
    assert_eq!(committer.changes.len(), before_commits);
    Ok(())
}

#[test]
fn nested_multiple_recipients_and_removed_override_preserve_plaintext() -> 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 manager = RecipientPolicyManager::new(&repository, &keys);
    let alice = fixture.key("alice")?;
    let bob = fixture.key("bob")?;
    let path = EntryPath::parse("team/service")?;
    let expected = fixture.read("expected/basic/team/service.txt")?;
    let mut provider = FixtureSecrets::all(&fixture);
    let mut committer = RecordingCommitter::default();

    let multiple = manager.apply_init(
        &init(
            Some("team"),
            &[&alice.primary_fingerprint, &bob.primary_fingerprint],
        ),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert_eq!(multiple.reencrypted_entries(), std::slice::from_ref(&path));
    assert_eq!(multiple.recipients().len(), 2);
    assert_eq!(
        fs::read(store.path().join("team/.gpg-id"))?,
        format!(
            "{}\n{}\n",
            alice.primary_fingerprint, bob.primary_fingerprint
        )
        .as_bytes()
    );
    let plaintext = keys.decrypt(&repository.read_entry(&path)?, &mut provider)?;
    assert_eq!(plaintext.expose(), expected);

    let removed = manager.apply_init(
        &init(Some("team"), &[""]),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert_eq!(removed.reencrypted_entries(), std::slice::from_ref(&path));
    assert!(!store.path().join("team/.gpg-id").exists());
    assert!(
        store.path().join("team/.gpg-id.sig").exists(),
        "upstream pass leaves an existing detached signature when an override is removed"
    );
    assert_eq!(
        removed.recipients()[0].fingerprint().as_str(),
        alice.primary_fingerprint
    );
    let plaintext = keys.decrypt(&repository.read_entry(&path)?, &mut provider)?;
    assert_eq!(plaintext.expose(), expected);
    Ok(())
}

#[test]
fn creates_and_authenticates_signed_recipient_policies() -> 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 manager = RecipientPolicyManager::new(&repository, &keys);
    let alice = fixture.key("alice")?;
    let bob = fixture.key("bob")?;
    let alice_handle = keys.resolve(&alice.primary_fingerprint)?;
    let bob_handle = keys.resolve(&bob.primary_fingerprint)?;
    let alice_policy = SigningPolicy::new(alice_handle.clone(), vec![alice_handle.clone()]);

    let resolved =
        manager.resolve_for_entry(&EntryPath::parse("email/personal")?, Some(&alice_policy))?;
    assert_eq!(resolved.policy_directory().as_path(), Path::new(""));

    let before = tree_bytes(store.path())?;
    let wrong_trust = SigningPolicy::new(alice_handle, vec![bob_handle]);
    let mut provider = FixtureSecrets::all(&fixture);
    let error = manager
        .apply_init(
            &init(None, &[&bob.primary_fingerprint]),
            Some(&wrong_trust),
            &mut provider,
            &mut NoGitCommitter,
        )
        .unwrap_err();
    assert!(matches!(
        error,
        RecipientPolicyError::Crypto(CryptoError::InvalidSignature)
    ));
    assert_eq!(tree_bytes(store.path())?, before);

    let alice_handle = keys.resolve(&alice.primary_fingerprint)?;
    let signing = SigningPolicy::new(alice_handle.clone(), vec![alice_handle]);
    manager.apply_init(
        &init(None, &[&bob.primary_fingerprint]),
        Some(&signing),
        &mut provider,
        &mut NoGitCommitter,
    )?;
    assert!(store.path().join(".gpg-id.sig").is_file());
    manager.resolve_for_entry(&EntryPath::parse("email/personal")?, Some(&signing))?;
    Ok(())
}

#[test]
fn invalid_recipients_and_commit_failure_leave_the_tree_byte_exact() -> 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 manager = RecipientPolicyManager::new(&repository, &keys);
    let bob = fixture.key("bob")?;
    let before = tree_bytes(store.path())?;
    let mut provider = FixtureSecrets::all(&fixture);
    let mut committer = RecordingCommitter::default();

    assert!(matches!(
        manager.apply_init(
            &init(None, &["missing@ironstorage.invalid"]),
            None,
            &mut provider,
            &mut committer,
        ),
        Err(RecipientPolicyError::Crypto(
            CryptoError::MissingIdentity { .. }
        ))
    ));
    assert_eq!(tree_bytes(store.path())?, before);
    assert!(committer.changes.is_empty());

    assert!(matches!(
        manager.apply_init(&init(None, &[""]), None, &mut provider, &mut committer,),
        Err(RecipientPolicyError::MissingRecipientPolicy { .. })
    ));
    assert_eq!(tree_bytes(store.path())?, before);

    committer.fail = true;
    assert!(matches!(
        manager.apply_init(
            &init(None, &[&bob.primary_fingerprint]),
            None,
            &mut provider,
            &mut committer,
        ),
        Err(RecipientPolicyError::Commit(_))
    ));
    assert_eq!(tree_bytes(store.path())?, before);
    Ok(())
}

#[test]
fn empty_store_supports_root_and_path_init_and_removal() -> TestResult {
    let fixture = FixtureSet::load()?;
    let temporary = tempfile::tempdir()?;
    let repository = Repository::open(temporary.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let manager = RecipientPolicyManager::new(&repository, &keys);
    let alice = fixture.key("alice")?;
    let bob = fixture.key("bob")?;
    let mut provider = FixtureSecrets::all(&fixture);
    let mut committer = RecordingCommitter::default();

    manager.apply_init(
        &init(
            None,
            &[&alice.primary_fingerprint, &bob.primary_fingerprint],
        ),
        None,
        &mut provider,
        &mut committer,
    )?;
    manager.apply_init(
        &init(Some("clients/acme"), &[&bob.primary_fingerprint]),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert!(temporary.path().join("clients/acme/.gpg-id").is_file());

    manager.apply_init(
        &init(Some("clients/acme"), &[""]),
        None,
        &mut provider,
        &mut committer,
    )?;
    assert!(!temporary.path().join("clients").exists());
    assert!(matches!(
        manager.apply_init(
            &init(Some("clients/acme"), &[""]),
            None,
            &mut provider,
            &mut committer,
        ),
        Err(RecipientPolicyError::PolicyNotFound { .. })
    ));
    Ok(())
}

#[test]
fn failed_commit_removes_new_empty_policy_directories() -> TestResult {
    let fixture = FixtureSet::load()?;
    let temporary = tempfile::tempdir()?;
    let repository = Repository::open(temporary.path())?;
    let keys = KeyStore::load(fixture.path("keys"))?;
    let manager = RecipientPolicyManager::new(&repository, &keys);
    let alice = fixture.key("alice")?;
    let mut provider = FixtureSecrets::all(&fixture);
    let mut committer = RecordingCommitter {
        fail: true,
        ..RecordingCommitter::default()
    };

    assert!(matches!(
        manager.apply_init(
            &init(Some("new/deep"), &[&alice.primary_fingerprint]),
            None,
            &mut provider,
            &mut committer,
        ),
        Err(RecipientPolicyError::Commit(_))
    ));
    assert!(fs::read_dir(temporary.path())?.next().is_none());
    Ok(())
}

fn init(path: Option<&str>, identities: &[&str]) -> InitRequest {
    InitRequest {
        path: path.map(str::to_owned),
        key_identities: identities
            .iter()
            .map(|identity| (*identity).to_owned())
            .collect(),
    }
}

fn tree_bytes(root: &Path) -> TestResult<BTreeMap<PathBuf, Vec<u8>>> {
    fn visit(root: &Path, current: &Path, files: &mut BTreeMap<PathBuf, Vec<u8>>) -> TestResult {
        let mut entries = fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
        entries.sort_by_key(fs::DirEntry::file_name);
        for entry in entries {
            let path = entry.path();
            let kind = entry.file_type()?;
            if kind.is_dir() {
                visit(root, &path, files)?;
            } else if kind.is_file() {
                files.insert(path.strip_prefix(root)?.to_owned(), fs::read(path)?);
            }
        }
        Ok(())
    }

    let mut files = BTreeMap::new();
    visit(root, root, &mut files)?;
    Ok(files)
}
