#![cfg(target_os = "macos")]

mod common;
use common::start_child_and_return;

use minidump::{
    CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpMiscInfo,
    MinidumpModuleList, MinidumpSystemInfo, MinidumpThreadList,
};
use minidump_writer::minidump_writer::MinidumpWriter;

fn get_crash_reason<'a, T: std::ops::Deref<Target = [u8]> + 'a>(
    md: &Minidump<'a, T>,
) -> CrashReason {
    let exc: minidump::MinidumpException<'_> =
        md.get_stream().expect("unable to find exception stream");

    exc.get_crash_reason(
        minidump::system_info::Os::MacOs,
        if cfg!(target_arch = "x86_64") {
            minidump::system_info::Cpu::X86_64
        } else if cfg!(target_arch = "aarch64") {
            minidump::system_info::Cpu::Arm64
        } else {
            unimplemented!()
        },
    )
}

struct Captured<'md> {
    #[allow(dead_code)]
    task: u32,
    thread: u32,
    minidump: Minidump<'md, memmap2::Mmap>,
}

fn capture_minidump(name: &str, exception_kind: u32) -> Captured<'_> {
    // Create a mach port server to retrieve the crash details from the child
    let mut server = crash_context::ipc::Server::create(&std::ffi::CString::new(name).unwrap())
        .expect("failed to create mach port service");

    let mut child = start_child_and_return(&[name, &exception_kind.to_string()]);

    // Wait for the child to spinup and report a crash context to us
    let mut rcc = server
        .try_recv_crash_context(Some(std::time::Duration::from_secs(5)))
        .expect("failed to receive context")
        .expect("receive timed out");

    let mut tmpfile = tempfile::Builder::new().prefix(name).tempfile().unwrap();

    let task = rcc.crash_context.task;
    let thread = rcc.crash_context.thread;

    let mut dumper = MinidumpWriter::with_crash_context(rcc.crash_context);

    dumper
        .dump(tmpfile.as_file_mut())
        .expect("failed to write minidump");

    // Signal the child that we've received and processed the crash context
    rcc.acker
        .send_ack(1, Some(std::time::Duration::from_secs(2)))
        .expect("failed to send ack");

    child.kill().expect("failed to kill child");

    let minidump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump");

    Captured {
        task,
        thread,
        minidump,
    }
}

#[test]
fn dump_external_process() {
    if std::env::var("CI").is_ok() {
        println!("test disabled, consistently times out because of potato runners");
        return;
    }

    let approximate_proc_start_time = std::time::SystemTime::now()
        .duration_since(std::time::SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let md = capture_minidump(
        "dump_external_process",
        mach2::exception_types::EXC_BREAKPOINT,
    )
    .minidump;

    let crash_reason = get_crash_reason(&md);

    assert!(matches!(
        crash_reason,
        CrashReason::MacGeneral(minidump_common::errors::ExceptionCodeMac::EXC_BREAKPOINT, _)
    ));

    let _: MinidumpModuleList = md.get_stream().expect("Couldn't find MinidumpModuleList");
    let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList");
    let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList");
    let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo");
    let _: MinidumpBreakpadInfo = md.get_stream().expect("Couldn't find MinidumpBreakpadInfo");

    let misc_info: MinidumpMiscInfo = md.get_stream().expect("Couldn't find MinidumpMiscInfo");

    if let minidump::RawMiscInfo::MiscInfo2(mi) = &misc_info.raw {
        // Unfortunately the minidump format only has 32-bit precision for the
        // process start time
        let process_create_time = mi.process_create_time as u64;

        assert!(
            process_create_time >= approximate_proc_start_time
                && process_create_time <= approximate_proc_start_time + 2
        );

        // I've tried busy looping to spend CPU time to get this up, but
        // MACH_TASK_BASIC_INFO which should give terminated thread times only ever
        // reports 0, and TASK_THREAD_TIMES_INFO which should show active thread
        // times I've only been able to get upt to a few thousand microseconds
        // even when busy looping for well over a second, and those get truncated
        // to whole seconds. And it seems that crashpad doesn't have tests around
        // this, though that's hard to say given how tedious it is finding stuff
        // in that bloated codebase
        // assert!(mi.process_user_time > 0);
        // assert!(mi.process_kernel_time > 0);

        // These aren't currently available on aarch64, or if they are, they
        // are not via the same sysctlbyname mechanism. Would be nice if Apple
        // documented...anything
        if cfg!(target_arch = "x86_64") {
            assert!(mi.processor_max_mhz > 0);
            assert!(mi.processor_current_mhz > 0);
        }
    } else {
        panic!("unexpected misc info type {:?}", misc_info);
    }
}

/// Validates we can actually walk the stack for each thread in the minidump,
/// this is using minidump-processor, which (currently) depends on breakpad
/// symbols, however https://github.com/mozilla/dump_syms is not available as
/// a library https://github.com/mozilla/dump_syms/issues/253, so we just require
/// that it already be installed, hence the ignore
#[test]
fn stackwalks() {
    if std::env::var("CI").is_ok() {
        println!("test disabled, consistently times out because of potato runners");
        return;
    }

    println!("generating minidump...");
    let md = capture_minidump("stackwalks", mach2::exception_types::EXC_BREAKPOINT);

    // Generate the breakpad symbols
    println!("generating symbols...");
    dump_syms::dumper::single_file(
        &dump_syms::dumper::Config {
            output: dump_syms::dumper::Output::Store(".test-symbols".into()),
            symbol_server: None,
            debug_id: None,
            code_id: None,
            arch: if cfg!(target_arch = "aarch64") {
                "arm64"
            } else if cfg!(target_arch = "x86_64") {
                "x86_64"
            } else {
                panic!("invalid MacOS target architecture")
            },
            num_jobs: 2, // default this
            check_cfi: false,
            emit_inlines: false,
            mapping_var: None,
            mapping_src: None,
            mapping_dest: None,
            mapping_file: None,
        },
        "target/debug/test",
    )
    .expect("failed to dump symbols");

    let provider =
        minidump_processor::Symbolizer::new(minidump_processor::simple_symbol_supplier(vec![
            ".test-symbols".into(),
        ]));

    let state = futures::executor::block_on(async {
        minidump_processor::process_minidump(&md.minidump, &provider).await
    })
    .unwrap();

    //state.print(&mut std::io::stdout()).map_err(|_| ()).unwrap();

    // We expect at least 2 threads, one of which is the fake crashing thread
    let fake_crash_thread = state
        .threads
        .iter()
        .find(|cs| cs.thread_id == md.thread)
        .expect("failed to find crash thread");

    assert_eq!(
        fake_crash_thread.thread_name.as_deref(),
        Some("test-thread")
    );

    assert!(
        fake_crash_thread.frames.iter().any(|sf| {
            sf.function_name
                .as_ref()
                .map_or(false, |fname| fname.ends_with("wait_until_killed"))
        }),
        "unable to locate expected function"
    );

    let mod_list: MinidumpModuleList = md
        .minidump
        .get_stream()
        .expect("Couldn't find MinidumpModuleList");

    // Ensure we found dyld
    assert!(mod_list
        .iter()
        .any(|module| &module.name == "/usr/lib/dyld"));
}
