use std::{backtrace::Backtrace, panic, process};

use log::error;

pub fn setup_panic_hook() {
    panic::set_hook(Box::new(|info| {
        let backtrace = Backtrace::force_capture();

        let msg = match info.payload().downcast_ref::<&'static str>() {
            Some(s) => *s,
            None => match info.payload().downcast_ref::<String>() {
                Some(s) => &s[..],
                None => "Box<Any>",
            },
        };

        let location = info
            .location()
            .map(|l| format!("{}:{}", l.file(), l.line()))
            .unwrap_or_else(|| "unknown".into());

        eprintln!(
            "CRITICAL: Panic occurred in file '{}' at thread '{}'\nMessage: {}\nStack Trace:\n{}",
            location,
            std::thread::current().name().unwrap_or("<unnamed>"),
            msg,
            backtrace
        );

        // Error log for convenience, but it may fail to log if the logger is not
        // initialized.
        error!(
            "CRITICAL: Panic occurred in file '{}' at thread '{}'\nMessage: {}\nStack Trace:\n{}",
            location,
            std::thread::current().name().unwrap_or("<unnamed>"),
            msg,
            backtrace
        );

        process::abort();
    }));
}

#[cfg(test)]
mod tests {
    use std::process::Command;

    use super::*;

    #[test]
    fn panic_hook_aborts_process() {
        const TEST_NAME: &str = "panic_hook_aborts_process";
        if std::env::var("TICI_PANIC_HOOK_CHILD").is_ok() {
            setup_panic_hook();
            let _ = std::thread::spawn(|| panic!("panic hook abort test")).join();
            std::process::exit(1);
        }

        let exe = std::env::current_exe().expect("failed to locate test binary");
        let status = Command::new(exe)
            .env("TICI_PANIC_HOOK_CHILD", "1")
            .arg(TEST_NAME)
            .status()
            .expect("failed to spawn panic hook child process");

        assert!(
            !status.success(),
            "expected panic hook to abort, {:?}",
            status
        );

        #[cfg(unix)]
        {
            use std::os::unix::process::ExitStatusExt;

            assert_eq!(status.signal(), Some(libc::SIGABRT));
        }
    }
}
