use backtrace::Frame;
use std::ptr;
use std::thread;

fn get_actual_fn_pointer(fp: usize) -> usize {
    // On AIX, the function name references a function descriptor.
    // A function descriptor consists of (See https://reviews.llvm.org/D62532)
    // * The address of the entry point of the function.
    // * The TOC base address for the function.
    // * The environment pointer.
    // Deref `fp` directly so that we can get the address of `fp`'s
    // entry point in text section.
    //
    // For TOC, one can find more information in
    // https://www.ibm.com/docs/en/aix/7.2?topic=program-understanding-programming-toc
    if cfg!(target_os = "aix") {
        unsafe {
            let actual_fn_entry = *(fp as *const usize);
            actual_fn_entry
        }
    } else {
        fp
    }
}

#[test]
// FIXME: shouldn't ignore this test on i686-msvc, unsure why it's failing
#[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)]
#[rustfmt::skip] // we care about line numbers here
fn smoke_test_frames() {
    frame_1(line!());
    #[inline(never)] fn frame_1(start_line: u32) { frame_2(start_line) }
    #[inline(never)] fn frame_2(start_line: u32) { frame_3(start_line) }
    #[inline(never)] fn frame_3(start_line: u32) { frame_4(start_line) }
    #[inline(never)] fn frame_4(start_line: u32) {
        let mut v = Vec::new();
        backtrace::trace(|cx| {
            v.push(cx.clone());
            true
        });

        // Various platforms have various bits of weirdness about their
        // backtraces. To find a good starting spot let's search through the
        // frames
        let target = get_actual_fn_pointer(frame_4 as usize);
        let offset = v
            .iter()
            .map(|frame| frame.symbol_address() as usize)
            .enumerate()
            .filter_map(|(i, sym)| {
                if sym >= target {
                    Some((sym, i))
                } else {
                    None
                }
            })
            .min()
            .unwrap()
            .1;
        let mut frames = v[offset..].iter();

        assert_frame(
            frames.next().unwrap(),
            get_actual_fn_pointer(frame_4 as usize),
            "frame_4",
            "tests/smoke.rs",
            start_line + 6,
            9,
        );
        assert_frame(
            frames.next().unwrap(),
            get_actual_fn_pointer(frame_3 as usize),
            "frame_3",
            "tests/smoke.rs",
            start_line + 3,
            52,
        );
        assert_frame(
            frames.next().unwrap(),
            get_actual_fn_pointer(frame_2 as usize),
            "frame_2",
            "tests/smoke.rs",
            start_line + 2,
            52,
        );
        assert_frame(
            frames.next().unwrap(),
            get_actual_fn_pointer(frame_1 as usize),
            "frame_1",
            "tests/smoke.rs",
            start_line + 1,
            52,
        );
        assert_frame(
            frames.next().unwrap(),
            get_actual_fn_pointer(smoke_test_frames as usize),
            "smoke_test_frames",
            "",
            0,
            0,
        );
    }

    fn assert_frame(
        frame: &Frame,
        actual_fn_pointer: usize,
        expected_name: &str,
        expected_file: &str,
        expected_line: u32,
        expected_col: u32,
    ) {
        backtrace::resolve_frame(frame, |sym| {
            print!("symbol  ip:{:?} address:{:?} ", frame.ip(), frame.symbol_address());
            if let Some(name) = sym.name() {
                print!("name:{name} ");
            }
            if let Some(file) = sym.filename() {
                print!("file:{} ", file.display());
            }
            if let Some(lineno) = sym.lineno() {
                print!("lineno:{lineno} ");
            }
            if let Some(colno) = sym.colno() {
                print!("colno:{colno} ");
            }
            println!();
        });

        let ip = frame.ip() as usize;
        let sym = frame.symbol_address() as usize;
        assert!(ip >= sym);
        assert!(
            sym >= actual_fn_pointer,
            "{:?} < {:?} ({} {}:{}:{})",
            sym as *const usize,
            actual_fn_pointer as *const usize,
            expected_name,
            expected_file,
            expected_line,
            expected_col,
        );

        // windows dbghelp is *quite* liberal (and wrong) in many of its reports
        // right now...
        //
        // This assertion can also fail for release builds, so skip it there
        if cfg!(debug_assertions) {
            assert!(sym - actual_fn_pointer < 1024);
        }

        let mut resolved = 0;

        let mut name = None;
        let mut addr = None;
        let mut col  = None;
        let mut line = None;
        let mut file = None;
        backtrace::resolve_frame(frame, |sym| {
            resolved += 1;
            name = sym.name().map(|v| v.to_string());
            addr = sym.addr();
            col  = sym.colno();
            line = sym.lineno();
            file = sym.filename().map(|v| v.to_path_buf());
        });
        assert!(resolved > 0);

        let name = name.expect("didn't find a name");

        // in release mode names get weird as functions can get merged
        // together with `mergefunc`, so only assert this in debug mode
        if cfg!(debug_assertions) {
            assert!(
                name.contains(expected_name),
                "didn't find `{expected_name}` in `{name}`"
            );
        }

        addr.expect("didn't find a symbol");

        if cfg!(debug_assertions) {
            let line = line.expect("didn't find a line number");
            let file = file.expect("didn't find a line number");
            if !expected_file.is_empty() {
                assert!(
                    file.ends_with(expected_file),
                    "{file:?} didn't end with {expected_file:?}"
                );
            }
            if expected_line != 0 {
                assert!(
                    line == expected_line,
                    "bad line number on frame for `{expected_name}`: {line} != {expected_line}"
                );
            }

            // dbghelp on MSVC doesn't support column numbers
            if !cfg!(target_env = "msvc") {
                let col = col.expect("didn't find a column number");
                if expected_col != 0 {
                    assert!(
                        col == expected_col,
                        "bad column number on frame for `{expected_name}`: {col} != {expected_col}",
                    );
                }
            }
        }
    }
}

#[test]
fn many_threads() {
    let threads = (0..16)
        .map(|_| {
            thread::spawn(|| {
                for _ in 0..16 {
                    backtrace::trace(|frame| {
                        backtrace::resolve(frame.ip(), |symbol| {
                            let _s = symbol.name().map(|s| s.to_string());
                        });
                        true
                    });
                }
            })
        })
        .collect::<Vec<_>>();

    for t in threads {
        t.join().unwrap()
    }
}

#[test]
#[cfg(feature = "rustc-serialize")]
fn is_rustc_serialize() {
    extern crate rustc_serialize;

    fn is_encode<T: rustc_serialize::Encodable>() {}
    fn is_decode<T: rustc_serialize::Decodable>() {}

    is_encode::<backtrace::Backtrace>();
    is_decode::<backtrace::Backtrace>();
}

#[test]
#[cfg(feature = "serde")]
fn is_serde() {
    extern crate serde;

    fn is_serialize<T: serde::ser::Serialize>() {}
    fn is_deserialize<T: serde::de::DeserializeOwned>() {}

    is_serialize::<backtrace::Backtrace>();
    is_deserialize::<backtrace::Backtrace>();
}

#[test]
fn sp_smoke_test() {
    let mut refs = vec![];
    recursive_stack_references(&mut refs);
    return;

    #[inline(never)]
    fn recursive_stack_references(refs: &mut Vec<usize>) {
        assert!(refs.len() < 5);

        let x = refs.len();
        refs.push(ptr::addr_of!(x) as usize);

        if refs.len() < 5 {
            recursive_stack_references(refs);
            eprintln!("exiting: {x}");
            return;
        }

        backtrace::trace(make_trace_closure(refs));
        eprintln!("exiting: {x}");
    }

    // NB: the following `make_*` functions are pulled out of line, rather than
    // defining their results as inline closures at their call sites, so that
    // the resulting closures don't have "recursive_stack_references" in their
    // mangled names.

    fn make_trace_closure<'a>(
        refs: &'a mut Vec<usize>,
    ) -> impl FnMut(&backtrace::Frame) -> bool + 'a {
        let mut child_sp = None;
        let mut child_ref = None;
        move |frame| {
            eprintln!("\n=== frame ===================================");

            let mut is_recursive_stack_references = false;
            backtrace::resolve(frame.ip(), |sym| {
                is_recursive_stack_references |=
                    sym.name()
                        .and_then(|name| name.as_str())
                        .map_or(false, |name| {
                            eprintln!("name = {name}");
                            name.contains("recursive_stack_references")
                        })
            });

            let sp = frame.sp() as usize;
            eprintln!("sp  = {:p}", sp as *const u8);
            if sp == 0 {
                // If the SP is null, then we don't have an implementation for
                // getting the SP on this target. Just keep walking the stack,
                // but don't make our assertions about the on-stack pointers and
                // SP values.
                return true;
            }

            // The stack grows down.
            if let Some(child_sp) = child_sp {
                assert!(child_sp <= sp);
            }

            if is_recursive_stack_references {
                let r = refs.pop().unwrap();
                eprintln!("ref = {:p}", r as *const u8);
                if sp != 0 {
                    assert!(r > sp);
                    if let Some(child_ref) = child_ref {
                        assert!(sp >= child_ref);
                    }
                }
                child_ref = Some(r);
            }

            child_sp = Some(sp);
            true
        }
    }
}
