//! Tests verifying that `ctx_read` never hangs indefinitely due to session lock contention.
//! Covers the Windows deadlock scenario: shell-hook holds session write-lock while
//! `ctx_read` waits on session read-lock, causing circular dependency.

use lean_ctx::core::pathutil::safe_canonicalize_bounded;
use std::path::Path;
use std::time::{Duration, Instant};

// ═══════════════════════════════════════════════════════════════════════════════
// 1. safe_canonicalize_bounded — timeout behavior
// ═══════════════════════════════════════════════════════════════════════════════

#[test]
fn canonicalize_existing_path_succeeds_quickly() {
    let start = Instant::now();
    let result = safe_canonicalize_bounded(Path::new("/tmp"), 2000);
    let elapsed = start.elapsed();

    assert!(elapsed < Duration::from_secs(1));
    // On macOS /tmp -> /private/tmp
    assert!(
        result.to_str().unwrap().contains("tmp"),
        "should resolve to a path containing 'tmp', got: {}",
        result.display()
    );
}

#[test]
fn canonicalize_nonexistent_path_returns_original() {
    let fake = Path::new("/nonexistent/path/that/does/not/exist/xyzzy123");
    let start = Instant::now();
    let result = safe_canonicalize_bounded(fake, 2000);
    let elapsed = start.elapsed();

    assert!(elapsed < Duration::from_secs(1));
    assert_eq!(result, fake.to_path_buf());
}

#[test]
fn canonicalize_bounded_respects_timeout() {
    // We can't easily simulate a hanging canonicalize on non-Windows,
    // but we verify the function doesn't panic and completes quickly
    // for various path types
    let paths = &[
        "/tmp",
        "/var",
        "/usr/bin",
        "/this/does/not/exist",
        "relative/path/here",
    ];

    for p in paths {
        let start = Instant::now();
        let _ = safe_canonicalize_bounded(Path::new(p), 500);
        assert!(
            start.elapsed() < Duration::from_millis(600),
            "canonicalize took too long for {p}"
        );
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 2. Session lock timeout — simulated contention
// ═══════════════════════════════════════════════════════════════════════════════

mod session_lock_timeout {
    use std::sync::Arc;
    use std::time::{Duration, Instant};
    use tokio::sync::RwLock;

    #[test]
    fn read_lock_succeeds_when_uncontested() {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let lock = Arc::new(RwLock::new(42u32));

        rt.block_on(async {
            let result = tokio::time::timeout(Duration::from_secs(5), lock.read()).await;
            assert!(result.is_ok());
            assert_eq!(*result.unwrap(), 42);
        });
    }

    /// Upper bound for "the timeout fired promptly, we did not deadlock".
    /// Generous on purpose: loaded CI runners (Windows especially) can stall a
    /// timer by hundreds of ms, and the assertion only needs to distinguish
    /// "timed out" from "waited for the writer / hung forever".
    const PROMPT: Duration = Duration::from_secs(5);

    #[test]
    fn read_lock_times_out_when_write_held() {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let lock = Arc::new(RwLock::new(42u32));
        let lock2 = lock.clone();

        rt.block_on(async {
            // Write lock held for the whole test — a read can never succeed.
            let _guard = lock2.write().await;

            let start = Instant::now();
            let result = tokio::time::timeout(Duration::from_millis(200), lock.read()).await;

            assert!(result.is_err(), "should have timed out");
            assert!(start.elapsed() < PROMPT, "timeout did not fire promptly");
        });
    }

    #[test]
    fn write_lock_times_out_when_read_held() {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let lock = Arc::new(RwLock::new(42u32));
        let lock2 = lock.clone();

        rt.block_on(async {
            let _guard = lock2.read().await;

            let start = Instant::now();
            let result = tokio::time::timeout(Duration::from_millis(200), lock.write()).await;

            assert!(result.is_err(), "should have timed out");
            assert!(start.elapsed() < PROMPT, "timeout did not fire promptly");
        });
    }

    #[test]
    fn block_in_place_timeout_pattern_works() {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .unwrap();
        let lock = Arc::new(RwLock::new(String::from("hello")));
        let lock2 = lock.clone();

        rt.block_on(async {
            // Simulate a held write lock (like shell-hook holding session)
            let _guard = lock2.write().await;

            // This is the exact pattern used in ctx_read.rs
            let result = tokio::task::block_in_place(|| {
                let rt = tokio::runtime::Handle::current();
                rt.block_on(tokio::time::timeout(
                    Duration::from_millis(300),
                    lock.read(),
                ))
            });

            assert!(result.is_err(), "should timeout, not deadlock");
        });
    }

    #[test]
    fn multiple_readers_succeed_concurrently() {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .unwrap();
        let lock = Arc::new(RwLock::new(String::from("data")));

        rt.block_on(async {
            let mut handles = Vec::new();
            for _ in 0..10 {
                let l = lock.clone();
                handles.push(tokio::spawn(async move {
                    let result = tokio::time::timeout(Duration::from_secs(1), l.read()).await;
                    assert!(result.is_ok());
                }));
            }
            for h in handles {
                h.await.unwrap();
            }
        });
    }

    #[test]
    fn graceful_degradation_on_write_timeout() {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .unwrap();
        let lock = Arc::new(RwLock::new(String::from("session_data")));
        let lock2 = lock.clone();

        rt.block_on(async {
            let _guard = lock2.read().await;

            // Simulate the post-read session update (write lock)
            let start = Instant::now();
            let session_guard = tokio::task::block_in_place(|| {
                let rt = tokio::runtime::Handle::current();
                rt.block_on(tokio::time::timeout(
                    Duration::from_millis(200),
                    lock.write(),
                ))
            });

            // Should timeout gracefully, not hang
            assert!(session_guard.is_err());
            assert!(start.elapsed() < PROMPT, "timeout did not fire promptly");
            // In production, we'd fall back to project_root from ctx
        });
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 3. Integration: read_file_lossy doesn't hang on nonexistent/weird paths
// ═══════════════════════════════════════════════════════════════════════════════

mod read_file_safety {
    use lean_ctx::tools::ctx_read::read_file_lossy;
    use std::time::{Duration, Instant};

    #[test]
    fn nonexistent_file_errors_quickly() {
        let start = Instant::now();
        let result = read_file_lossy("/this/path/does/not/exist/file.rs");
        assert!(result.is_err());
        assert!(
            start.elapsed() < Duration::from_secs(3),
            "should fail fast, not hang"
        );
    }

    #[test]
    fn existing_file_reads_within_timeout() {
        let start = Instant::now();
        let result = read_file_lossy("/tmp/.lean-ctx-test-read");
        // May error (file doesn't exist) or succeed, but must not hang
        let _ = result;
        assert!(
            start.elapsed() < Duration::from_secs(3),
            "should complete within timeout"
        );
    }

    #[test]
    fn deeply_nested_nonexistent_path_errors_quickly() {
        let deep = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/file.txt";
        let start = Instant::now();
        let result = read_file_lossy(deep);
        assert!(result.is_err());
        assert!(start.elapsed() < Duration::from_secs(3));
    }

    #[cfg(windows)]
    #[test]
    fn windows_gemini_path_doesnt_hang() {
        // Simulates the exact path pattern from the bug report
        let path = r"C:\Users\USER\.gemini\some_config.json";
        let start = Instant::now();
        let _ = read_file_lossy(path);
        assert!(
            start.elapsed() < Duration::from_secs(5),
            "Windows .gemini path must not hang"
        );
    }
}
