use std::env;
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;

use backtrace::Backtrace;
use findshlibs::{IterationControl, SharedLibrary, TargetSharedLibrary};
use libtest_mimic::{Arguments, Failed, Trial};

#[inline(never)]
fn make_trace() -> Vec<String> {
    fn foo() -> Backtrace {
        bar()
    }
    #[inline(never)]
    fn bar() -> Backtrace {
        baz()
    }
    #[inline(always)]
    fn baz() -> Backtrace {
        Backtrace::new_unresolved()
    }

    let mut base_addr = None;
    TargetSharedLibrary::each(|lib| {
        base_addr = Some(lib.virtual_memory_bias().0 as isize);
        IterationControl::Break
    });
    let addrfix = -base_addr.unwrap();

    let trace = foo();
    trace
        .frames()
        .iter()
        .take(5)
        .map(|x| format!("{:p}", (x.ip() as *const u8).wrapping_offset(addrfix)))
        .collect()
}

fn run_cmd<P: AsRef<OsStr>>(exe: P, me: &Path, flags: Option<&str>, trace: &str) -> String {
    let mut cmd = Command::new(exe);
    cmd.env("LC_ALL", "C"); // GNU addr2line is localized, we aren't
    cmd.env("RUST_BACKTRACE", "1"); // if a child crashes, we want to know why

    if let Some(flags) = flags {
        cmd.arg(flags);
    }
    cmd.arg("--exe").arg(me).arg(trace);

    let output = cmd.output().unwrap();

    assert!(output.status.success());
    String::from_utf8(output.stdout).unwrap()
}

fn run_test(flags: Option<&str>) -> Result<(), Failed> {
    let me = env::current_exe().unwrap();
    let mut exe = me.clone();
    assert!(exe.pop());
    if exe.file_name().unwrap().to_str().unwrap() == "deps" {
        assert!(exe.pop());
    }
    exe.push("examples");
    exe.push("addr2line");

    assert!(exe.is_file());

    let trace = make_trace();

    // HACK: GNU addr2line has a bug where looking up multiple addresses can cause the second
    // lookup to fail. Workaround by doing one address at a time.
    for addr in &trace {
        let theirs = run_cmd("addr2line", &me, flags, addr);
        let ours = run_cmd(&exe, &me, flags, addr);

        // HACK: GNU addr2line does not tidy up paths properly, causing double slashes to be printed.
        // We consider our behavior to be correct, so we fix their output to match ours.
        let theirs = theirs.replace("//", "/");

        assert!(
            theirs == ours,
            "Output not equivalent:

$ addr2line {0} --exe {1} {2}
{4}
$ {3} {0} --exe {1} {2}
{5}


",
            flags.unwrap_or(""),
            me.display(),
            trace.join(" "),
            exe.display(),
            theirs,
            ours
        );
    }
    Ok(())
}

static FLAGS: &str = "aipsf";

fn make_tests() -> Vec<Trial> {
    (0..(1 << FLAGS.len()))
        .map(|bits| {
            if bits == 0 {
                None
            } else {
                let mut param = String::new();
                param.push('-');
                for (i, flag) in FLAGS.chars().enumerate() {
                    if (bits & (1 << i)) != 0 {
                        param.push(flag);
                    }
                }
                Some(param)
            }
        })
        .map(|param| {
            Trial::test(
                format!("addr2line {}", param.as_ref().map_or("", String::as_str)),
                move || run_test(param.as_ref().map(String::as_str)),
            )
        })
        .collect()
}

fn main() {
    if !cfg!(target_os = "linux") {
        return;
    }
    let args = Arguments::from_args();
    libtest_mimic::run(&args, make_tests()).exit();
}
