//! Property-based tests for tsafe vault operations.
//!
//! Uses proptest to generate arbitrary inputs and verify invariants hold
//! for all possible values — catches edge cases that hand-written tests miss.
//!
//! **Performance:** each [`Vault`] case runs **full production Argon2id** (64 MiB).
//! The proptest default is **256 cases per test**, so vault-heavy tests used to run
//! for many minutes and looked hung. Vault properties use a low default case count;
//! cheap tests use more. Override any time: `PROPTEST_CASES=64 cargo test -p tsafe-core --test proptest_vault`.
//!
//! To run mutation testing (validates test quality):
//!   cargo install cargo-mutants
//!   cargo mutants --package tsafe-core -- --test proptest_vault

use proptest::prelude::*;
use proptest::test_runner::{contextualize_config, Config};
use std::collections::HashMap;
use tempfile::tempdir;
use tsafe_core::vault::Vault;

const TEST_PW: &[u8] = b"proptest-password-42";

/// Full Argon2 vault ops: keep cases low so `cargo test` stays interactive.
const VAULT_PROPCASES: u32 = 12;

/// Parsing / small-KDF props: can afford more cases.
const FAST_PROPCASES: u32 = 64;

// ── Strategies ───────────────────────────────────────────────────────────────

/// Valid vault key names: start with letter/underscore, then alnum/underscore/dot/dash/slash.
fn key_strategy() -> impl Strategy<Value = String> {
    // First char: letter or underscore.  Rest: [A-Za-z0-9_.-/] with no consecutive separators.
    "[A-Za-z_][A-Za-z0-9_]{0,30}".prop_map(|s| s)
}

/// Arbitrary secret values — any valid UTF-8 string, including empty, newlines, unicode.
fn value_strategy() -> impl Strategy<Value = String> {
    "[ -~\\n\\r\\t]{0,500}" // printable ASCII + whitespace, up to 500 chars
}

/// Arbitrary .env file content: KEY=VALUE lines with comments and blanks.
fn dotenv_strategy() -> impl Strategy<Value = String> {
    prop::collection::vec(
        prop_oneof![
            // Comment line
            "# [a-zA-Z ]{0,40}".prop_map(|s| s),
            // Blank line
            Just(String::new()),
            // KEY=VALUE line
            ("[A-Z_]{1,20}", "[ -~]{0,50}").prop_map(|(k, v)| format!("{k}={v}")),
            // Quoted value
            ("[A-Z_]{1,20}", "[a-zA-Z0-9 ]{0,30}").prop_map(|(k, v)| format!("{k}=\"{v}\"")),
        ],
        0..20,
    )
    .prop_map(|lines| lines.join("\n"))
}

// ── Vault-backed property tests (expensive: production KDF + disk) ───────────

proptest! {
    #![proptest_config(contextualize_config(Config::with_cases(VAULT_PROPCASES)))]
    /// Any valid key + arbitrary UTF-8 value survives a vault set → get roundtrip.
    #[test]
    fn prop_set_get_roundtrip(key in key_strategy(), value in value_strategy()) {
        let dir = tempdir().unwrap();
        let path = dir.path().join("prop.vault");
        let mut vault = Vault::create(&path, TEST_PW).unwrap();
        vault.set(&key, &value, HashMap::new()).unwrap();
        let got = vault.get(&key).unwrap();
        prop_assert_eq!(&*got, &value);
    }

    /// Multiple keys can coexist and each retrieves the correct value.
    #[test]
    fn prop_multi_key_roundtrip(
        entries in prop::collection::vec(
            (key_strategy(), value_strategy()),
            1..10
        )
    ) {
        let dir = tempdir().unwrap();
        let path = dir.path().join("multi.vault");
        let mut vault = Vault::create(&path, TEST_PW).unwrap();

        // Deduplicate keys (last wins, matching real behavior).
        let mut expected = HashMap::new();
        for (k, v) in &entries {
            vault.set(k, v, HashMap::new()).unwrap();
            expected.insert(k.clone(), v.clone());
        }

        for (k, v) in &expected {
            let got = vault.get(k).unwrap();
            prop_assert_eq!(&*got, v);
        }
    }

    /// A vault survives create → save → reopen → get for any value.
    #[test]
    fn prop_persist_reopen_roundtrip(key in key_strategy(), value in value_strategy()) {
        let dir = tempdir().unwrap();
        let path = dir.path().join("reopen.vault");
        {
            let mut vault = Vault::create(&path, TEST_PW).unwrap();
            vault.set(&key, &value, HashMap::new()).unwrap();
        } // vault dropped, lock released
        let vault2 = Vault::open(&path, TEST_PW).unwrap();
        let got = vault2.get(&key).unwrap();
        prop_assert_eq!(&*got, &value, "persist→reopen roundtrip failed");
    }
}

// ── Cheap property tests (no 64 MiB vault KDF) ────────────────────────────────

proptest! {
    #![proptest_config(contextualize_config(Config::with_cases(FAST_PROPCASES)))]
    /// .env parsing never panics on arbitrary input.
    #[test]
    fn prop_dotenv_parse_no_panic(content in dotenv_strategy()) {
        let dir = tempdir().unwrap();
        let path = dir.path().join("test.env");
        std::fs::write(&path, &content).unwrap();
        // Should never panic, even on weird input.
        let _ = tsafe_core::env::parse_dotenv(&path);
    }

    /// KDF with params in valid range never panics.
    /// Uses 8–16 MiB Argon2id only (see strategy), not the vault’s 64 MiB profile.
    #[test]
    fn prop_kdf_valid_range_no_panic(
        m_cost in 8192u32..=16384,   // 8–16 MiB
        t_cost in 1u32..=2,
        p_cost in 1u32..=2,
    ) {
        let salt = tsafe_core::crypto::random_salt();
        let result = tsafe_core::crypto::derive_key(
            b"test-password",
            &salt,
            m_cost,
            t_cost,
            p_cost,
        );
        prop_assert!(result.is_ok());
    }
}
