use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Barrier, Mutex};

use tempfile::tempdir;
use tsafe_core::audit::{AuditEntry, AuditLog};
use tsafe_core::errors::SafeError;
use tsafe_core::vault::Vault;

const TEST_PW: &[u8] = b"concurrency-stress-password";

#[test]
fn concurrent_audit_appends_remain_line_atomic_and_complete() {
    const THREADS: usize = 12;
    const ENTRIES_PER_THREAD: usize = 40;
    const EXPECTED: usize = THREADS * ENTRIES_PER_THREAD;

    let dir = tempdir().unwrap();
    let log_path = Arc::new(dir.path().join("audit.jsonl"));
    let barrier = Arc::new(Barrier::new(THREADS + 1));

    let handles = (0..THREADS)
        .map(|thread_idx| {
            let log_path = Arc::clone(&log_path);
            let barrier = Arc::clone(&barrier);
            std::thread::spawn(move || {
                let log = AuditLog::new(&log_path);
                barrier.wait();
                for entry_idx in 0..ENTRIES_PER_THREAD {
                    let key = format!("T{thread_idx:02}_K{entry_idx:02}");
                    log.append(&AuditEntry::success("stress", "set", Some(&key)))
                        .unwrap();
                }
            })
        })
        .collect::<Vec<_>>();

    barrier.wait();
    for handle in handles {
        handle.join().expect("audit append thread panicked");
    }

    let raw = std::fs::read_to_string(&*log_path).unwrap();
    assert!(
        raw.ends_with('\n'),
        "audit log should end at a JSONL record boundary"
    );

    let lines = raw.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), EXPECTED, "lost or extra audit records");

    let mut seen_keys = HashSet::new();
    for line in lines {
        let entry: AuditEntry =
            serde_json::from_str(line).expect("each audit line must be valid JSON");
        assert_eq!(entry.profile, "stress");
        assert_eq!(entry.operation, "set");
        let key = entry.key.expect("stress entries carry unique keys");
        assert!(seen_keys.insert(key), "duplicate audit key observed");
    }
    assert_eq!(seen_keys.len(), EXPECTED);
}

#[test]
fn vault_write_owner_blocks_concurrent_open_write_attempts() {
    const CONTENDERS: usize = 10;

    let dir = tempdir().unwrap();
    let path = Arc::new(dir.path().join("stress.vault"));
    let mut owner = Vault::create(&path, TEST_PW).unwrap();
    owner
        .set("OWNER_BEFORE", "created-before-contenders", HashMap::new())
        .unwrap();

    let barrier = Arc::new(Barrier::new(CONTENDERS + 1));
    let handles = (0..CONTENDERS)
        .map(|idx| {
            let path = Arc::clone(&path);
            let barrier = Arc::clone(&barrier);
            std::thread::spawn(move || {
                barrier.wait();
                match Vault::open(&path, TEST_PW) {
                    Err(SafeError::InvalidVault { reason }) => {
                        assert!(
                            reason.contains("vault is locked by another process"),
                            "unexpected lock error: {reason}"
                        );
                    }
                    Ok(mut vault) => {
                        let key = format!("CONTENDER_{idx}");
                        vault
                            .set(&key, "unexpected-write", HashMap::new())
                            .expect("contender write unexpectedly reached save path");
                        panic!("contender acquired the lock and wrote {key}");
                    }
                    Err(other) => panic!("unexpected vault open error: {other:?}"),
                }
            })
        })
        .collect::<Vec<_>>();

    barrier.wait();
    owner
        .set(
            "OWNER_DURING",
            "written-while-contenders-race",
            HashMap::new(),
        )
        .unwrap();

    for handle in handles {
        handle.join().expect("vault contention thread panicked");
    }

    drop(owner);

    let reopened = Vault::open(&path, TEST_PW).unwrap();
    assert_eq!(
        &*reopened.get("OWNER_BEFORE").unwrap(),
        "created-before-contenders"
    );
    assert_eq!(
        &*reopened.get("OWNER_DURING").unwrap(),
        "written-while-contenders-race"
    );
    for idx in 0..CONTENDERS {
        assert!(matches!(
            reopened.get(&format!("CONTENDER_{idx}")),
            Err(SafeError::SecretNotFound { .. })
        ));
    }
}

/// Stress test: 4 threads concurrently attempt vault opens, writes, and audit
/// reads against a shared vault and audit log.  Only the thread that acquires
/// the vault lock can write; the others must receive a clear lock-error.
/// Audit reads are lock-free and must succeed for all threads at all times.
///
/// This documents the accepted v1 concurrency contract:
/// - vault writes are serialised by the advisory lock
/// - audit reads are always safe (file is opened read-only per call)
/// - no data is lost or corrupted on the vault side
#[test]
fn concurrent_vault_open_write_and_audit_read_stress() {
    const THREADS: usize = 4;
    const AUDIT_READS_PER_THREAD: usize = 5;

    let dir = tempdir().unwrap();
    let vault_path = Arc::new(dir.path().join("stress_rw.vault"));
    let audit_path = Arc::new(dir.path().join("stress_rw.audit.jsonl"));

    // Seed the vault with an owner key before spawning contenders.
    {
        let mut owner = Vault::create(&vault_path, TEST_PW).unwrap();
        owner.set("SEED_KEY", "seed-value", HashMap::new()).unwrap();
    }

    // Seed one audit entry so reads never see an empty log.
    {
        let log = AuditLog::new(&audit_path);
        log.append(&AuditEntry::success("stress_rw", "init", Some("SEED_KEY")))
            .unwrap();
    }

    let write_successes = Arc::new(Mutex::new(Vec::<String>::new()));
    let barrier = Arc::new(Barrier::new(THREADS + 1));

    let handles: Vec<_> = (0..THREADS)
        .map(|idx| {
            let vault_path = Arc::clone(&vault_path);
            let audit_path = Arc::clone(&audit_path);
            let barrier = Arc::clone(&barrier);
            let write_successes = Arc::clone(&write_successes);

            std::thread::spawn(move || {
                barrier.wait(); // synchronised start

                // Each thread: attempt a vault open + write, then do several
                // audit reads regardless of whether the write succeeded.
                let secret_key = format!("THREAD_{idx}_SECRET");
                let write_outcome = match Vault::open(&vault_path, TEST_PW) {
                    Ok(mut vault) => {
                        // We hold the lock — write should succeed.
                        vault
                            .set(&secret_key, &format!("value-{idx}"), HashMap::new())
                            .ok();
                        Some(secret_key.clone())
                    }
                    Err(SafeError::InvalidVault { reason })
                        if reason.contains("vault is locked by another process") =>
                    {
                        None // expected: another thread holds the lock
                    }
                    Err(other) => panic!("unexpected vault open error: {other:?}"),
                };

                if let Some(key) = write_outcome {
                    write_successes.lock().unwrap().push(key);
                }

                // Audit reads must always succeed, regardless of vault lock.
                let log = AuditLog::new(&audit_path);
                for _ in 0..AUDIT_READS_PER_THREAD {
                    let entries = log.read(None).expect("audit read must never fail");
                    assert!(
                        !entries.is_empty(),
                        "audit log should always contain at least the seed entry"
                    );
                }
            })
        })
        .collect();

    barrier.wait(); // release all threads simultaneously

    for handle in handles {
        handle.join().expect("stress thread panicked");
    }

    // At least one thread must have successfully written (the one that won
    // the advisory lock race).  All written keys must be visible on reopen.
    let written = write_successes.lock().unwrap().clone();
    assert!(
        !written.is_empty(),
        "at least one thread must have acquired the vault lock and written"
    );

    let final_vault = Vault::open(&vault_path, TEST_PW).unwrap();
    assert_eq!(
        &*final_vault.get("SEED_KEY").unwrap(),
        "seed-value",
        "seed key must survive concurrent stress"
    );
    for key in &written {
        assert!(
            final_vault.get(key).is_ok(),
            "written key {key} must be present after stress"
        );
    }
}
