//! Integration tests for kigi-crash-handler.
//!
//! These tests verify that installing the crash handler does not interfere
//! with normal program operation (tokio runtime, signal handling, I/O),
//! and that it correctly captures crash data when a fatal signal fires.
//!
//! Tests that send fatal signals use subprocess isolation: the test process
//! re-executes itself with an env var that selects the crash scenario, so
//! the parent can verify outcomes without dying.

#![cfg(unix)]

use std::path::Path;
use std::process::Command;

/// Re-invoke the current test binary as a subprocess with the given scenario.
/// Returns (exit status, stdout, stderr).
fn run_scenario(scenario: &str, crash_dir: &Path) -> (std::process::ExitStatus, String, String) {
    let exe = std::env::current_exe().expect("current_exe");
    let output = Command::new(exe)
        .env("CRASH_TEST_SCENARIO", scenario)
        .env("CRASH_TEST_DIR", crash_dir.as_os_str())
        .arg("--ignored")
        .arg("--exact")
        .arg("--nocapture")
        .arg("subprocess_entry")
        .output()
        .expect("failed to spawn subprocess");
    (
        output.status,
        String::from_utf8_lossy(&output.stdout).into_owned(),
        String::from_utf8_lossy(&output.stderr).into_owned(),
    )
}

// ── Subprocess entry point ──────────────────────────────────────────────

/// This test is `#[ignore]`d so it only runs when invoked as a subprocess
/// by the parent test via `run_scenario`. The `CRASH_TEST_SCENARIO` env
/// var selects which scenario to execute.
#[test]
#[ignore]
fn subprocess_entry() {
    let scenario = match std::env::var("CRASH_TEST_SCENARIO") {
        Ok(s) => s,
        Err(_) => return, // not a subprocess invocation
    };
    let crash_dir = std::env::var("CRASH_TEST_DIR").expect("CRASH_TEST_DIR");
    let crash_dir = std::path::PathBuf::from(crash_dir);

    // Install the crash handler before anything else.
    let config = kigi_crash_handler::CrashHandlerConfig {
        app_version: "0.0.0-test".to_string(),
        crash_dir,
    };
    kigi_crash_handler::install(config);

    match scenario.as_str() {
        // Scenario 1: install handler, run tokio runtime with concurrent work, exit cleanly.
        "tokio_normal" => {
            let rt = tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .expect("tokio runtime");
            rt.block_on(async {
                // Spawn several concurrent tasks to stress the runtime.
                let mut handles = Vec::new();
                for i in 0..20 {
                    handles.push(tokio::spawn(async move {
                        tokio::time::sleep(std::time::Duration::from_millis(5)).await;
                        i * i
                    }));
                }
                let mut sum = 0u64;
                for h in handles {
                    sum += h.await.unwrap();
                }
                // Also test signal infrastructure coexistence.
                // Register a tokio SIGTERM handler (same as the pager does).
                #[cfg(unix)]
                {
                    use tokio::signal::unix::{SignalKind, signal};
                    let _term = signal(SignalKind::terminate())
                        .expect("tokio SIGTERM handler should work alongside crash handler");
                }
                eprintln!("tokio_normal: sum={sum}, all tasks completed");
            });
        }

        // Scenario 2: install handler, do sync file I/O and computation, exit cleanly.
        "sync_normal" => {
            let tmp = tempfile::tempdir().expect("tempdir");
            for i in 0..50 {
                let path = tmp.path().join(format!("file-{i}.txt"));
                std::fs::write(&path, format!("contents {i}")).expect("write");
                let data = std::fs::read_to_string(&path).expect("read");
                assert!(data.contains(&format!("{i}")));
            }
            eprintln!("sync_normal: 50 files written and read back");
        }

        // Scenario 3: install handler, send ourselves SIGBUS, verify crash file written.
        "sigbus" => {
            // Give the handler a moment to be fully installed, then crash.
            unsafe { libc::raise(libc::SIGBUS) };
        }

        // Scenario 4: install handler, send ourselves SIGSEGV.
        "sigsegv" => {
            unsafe { libc::raise(libc::SIGSEGV) };
        }

        // Scenario 5: tokio runtime + signal coexistence, then clean shutdown.
        "tokio_signals" => {
            let rt = tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .expect("tokio runtime");
            rt.block_on(async {
                use tokio::signal::unix::{SignalKind, signal};
                let mut usr1 = signal(SignalKind::user_defined1()).expect("SIGUSR1 handler");

                // Send ourselves SIGUSR1 and verify tokio receives it
                // (proves our SIGBUS/SIGSEGV handler doesn't clobber other signals).
                unsafe { libc::raise(libc::SIGUSR1) };
                tokio::time::timeout(std::time::Duration::from_secs(2), usr1.recv())
                    .await
                    .expect("SIGUSR1 should arrive within 2s");

                eprintln!("tokio_signals: SIGUSR1 received, signal coexistence OK");
            });
        }

        other => {
            eprintln!("unknown scenario: {other}");
            std::process::exit(99);
        }
    }
}

// ── Parent test cases ───────────────────────────────────────────────────

#[test]
fn handler_does_not_interfere_with_tokio_runtime() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let (status, _stdout, stderr) = run_scenario("tokio_normal", tmp.path());
    assert!(
        status.success(),
        "tokio_normal should exit 0, got {status:?}\nstderr: {stderr}"
    );
    assert!(
        stderr.contains("all tasks completed"),
        "should see completion message\nstderr: {stderr}"
    );
    // No crash file should exist.
    assert!(
        !tmp.path().join("last-crash.bin").exists()
            || std::fs::metadata(tmp.path().join("last-crash.bin"))
                .map(|m| m.len() == 0)
                .unwrap_or(true),
        "crash file should not contain data after clean exit"
    );
}

#[test]
fn handler_does_not_interfere_with_sync_io() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let (status, _stdout, stderr) = run_scenario("sync_normal", tmp.path());
    assert!(
        status.success(),
        "sync_normal should exit 0, got {status:?}\nstderr: {stderr}"
    );
    assert!(
        stderr.contains("50 files written"),
        "should see completion message\nstderr: {stderr}"
    );
}

#[test]
fn handler_does_not_clobber_other_signal_handlers() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let (status, _stdout, stderr) = run_scenario("tokio_signals", tmp.path());
    assert!(
        status.success(),
        "tokio_signals should exit 0, got {status:?}\nstderr: {stderr}"
    );
    assert!(
        stderr.contains("signal coexistence OK"),
        "SIGUSR1 should be delivered through tokio\nstderr: {stderr}"
    );
}

#[test]
fn sigbus_produces_valid_crash_blob() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let (status, _stdout, _stderr) = run_scenario("sigbus", tmp.path());

    // Process should have been killed by a signal.
    // We expect SIGBUS, but the frame-pointer walker may hit unmapped memory
    // and cause a secondary SIGSEGV (SA_RESETHAND ensures it terminates).
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        let sig = status.signal();
        assert!(
            sig == Some(libc::SIGBUS) || sig == Some(libc::SIGSEGV),
            "process should be killed by SIGBUS or SIGSEGV, got signal={sig:?} status={status:?}"
        );
    }

    // The crash file should be parseable.
    let crash_file = tmp.path().join("last-crash.bin");
    assert!(crash_file.exists(), "crash file should exist after SIGBUS");
    let data = std::fs::read(&crash_file).expect("read crash file");
    assert!(
        data.len() > 4,
        "crash file should have data, got {} bytes",
        data.len()
    );

    let blob =
        kigi_crash_handler::format::CrashBlob::parse(&data).expect("crash blob should parse");

    // On macOS SIGBUS=10, on Linux SIGBUS=7, SIGSEGV=11 on both.
    // The frame-pointer walker may cause a secondary SIGSEGV.
    assert!(
        blob.signal == 7 || blob.signal == 10 || blob.signal == 11,
        "signal should be SIGBUS or SIGSEGV, got {}",
        blob.signal
    );
    assert_eq!(blob.app_version, "0.0.0-test");
    assert!(blob.pid > 0, "PID should be nonzero");
    assert!(blob.timestamp > 0, "timestamp should be nonzero");

    // check_previous_crash should produce a report.
    let report = kigi_crash_handler::check_previous_crash(tmp.path())
        .expect("should produce a crash report");
    assert!(report.signal_name.contains("SIGBUS"));
    assert_eq!(report.app_version, "0.0.0-test");
    assert!(report.report_path.exists(), "report file should be written");

    // Crash blob should be consumed (deleted).
    assert!(
        !crash_file.exists(),
        "crash file should be deleted after processing"
    );
}

#[test]
fn sigsegv_produces_valid_crash_blob() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let (status, _stdout, _stderr) = run_scenario("sigsegv", tmp.path());

    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        let sig = status.signal();
        assert_eq!(
            sig,
            Some(libc::SIGSEGV),
            "process should be killed by SIGSEGV, got signal={sig:?} status={status:?}"
        );
    }

    let crash_file = tmp.path().join("last-crash.bin");
    assert!(crash_file.exists(), "crash file should exist after SIGSEGV");
    let data = std::fs::read(&crash_file).expect("read crash file");
    let blob =
        kigi_crash_handler::format::CrashBlob::parse(&data).expect("crash blob should parse");
    assert_eq!(blob.signal, 11, "signal should be SIGSEGV (11)");
    assert_eq!(blob.app_version, "0.0.0-test");
}

#[test]
fn clean_exit_does_not_produce_crash_report() {
    let tmp = tempfile::tempdir().expect("tempdir");
    // Run both scenarios and verify no crash artifacts.
    for scenario in &["tokio_normal", "sync_normal", "tokio_signals"] {
        let (status, _stdout, stderr) = run_scenario(scenario, tmp.path());
        assert!(status.success(), "{scenario} failed: {stderr}");
    }
    // check_previous_crash should return None.
    let report = kigi_crash_handler::check_previous_crash(tmp.path());
    assert!(report.is_none(), "no crash report after clean exits");
}
