use crate::common::{TestHooks, TestInfo};
use crate::variables::assert_scalar;
use crate::{CALCULATIONS_APP, prepare_debugee_process};
use crate::{VARS_APP, assert_no_proc};
use BreakCondition::DataWrites;
use BreakSize::Bytes8;
use bugstalker::debugger::address::RelocatedAddress;
use bugstalker::debugger::register::debug::{BreakCondition, BreakSize};
use bugstalker::debugger::variable::dqe::{Dqe, Selector};
use bugstalker::debugger::variable::value::{PointerValue, SupportedScalar, Value};
use bugstalker::debugger::{Debugger, DebuggerBuilder};
use serial_test::serial;

fn assert_old_new_inner(
    info: &TestInfo,
    source_dqe: Option<&str>,
    r#type: &str,
    old: SupportedScalar,
    mb_new: Option<SupportedScalar>,
) {
    let old_val = &info.old_value.take().unwrap();
    assert_eq!(info.wp_dqe_string.take().as_deref(), source_dqe);
    assert_scalar(old_val, r#type, Some(old));
    match mb_new {
        None => {
            assert!(&info.new_value.take().is_none())
        }
        Some(new) => {
            let new_val = &info.new_value.take().unwrap();
            assert_scalar(new_val, r#type, Some(new));
        }
    }
}

fn assert_old_new(
    info: &TestInfo,
    source_dqe: &str,
    r#type: &str,
    old: SupportedScalar,
    mb_new: Option<SupportedScalar>,
) {
    assert_old_new_inner(info, Some(source_dqe), r#type, old, mb_new)
}

fn assert_old_new_from_addr(
    info: &TestInfo,
    r#type: &str,
    old: SupportedScalar,
    mb_new: Option<SupportedScalar>,
) {
    assert_old_new_inner(info, None, r#type, old, mb_new)
}

#[test]
#[serial]
fn test_watchpoint_works() {
    let process = prepare_debugee_process(VARS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut debugger = builder.build(process).unwrap();
    debugger.set_breakpoint_at_line("vars.rs", 7).unwrap();

    debugger.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(7));
    let wp_dqe = Dqe::Variable(Selector::by_name("int8", true));
    debugger
        .set_watchpoint_on_expr("int8", wp_dqe, DataWrites)
        .unwrap();

    debugger.continue_debugee().unwrap();
    assert_eq!(info.line.take(), Some(8));
    let var_val = &info.new_value.take().unwrap();
    assert_scalar(var_val, "i8", Some(SupportedScalar::I8(1)));
    assert!(info.file.take().unwrap().contains("vars.rs"));

    debugger.continue_debugee().unwrap();
    assert_eq!(info.line.take(), Some(31));
    assert!(info.new_value.take().is_none());

    assert!(debugger.watchpoint_list().is_empty());

    debugger.continue_debugee().unwrap();
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_works_2() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_line("calculations.rs", 8).unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(8));
    let wp_dqe = Dqe::Variable(Selector::by_name("int8", true));
    dbg.set_watchpoint_on_expr("int8", wp_dqe, DataWrites)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I8(1), Some(SupportedScalar::I8(2)));
    assert_old_new(&info, "int8", "i8", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I8(2), Some(SupportedScalar::I8(0)));
    assert_old_new(&info, "int8", "i8", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I8(0), Some(SupportedScalar::I8(-5)));
    assert_old_new(&info, "int8", "i8", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I8(-5), Some(SupportedScalar::I8(6)));
    assert_old_new(&info, "int8", "i8", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I8(6), None);
    assert_old_new(&info, "int8", "i8", old, new);

    assert!(dbg.watchpoint_list().is_empty());
    dbg.continue_debugee().unwrap();
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_global_var() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_fn("main").unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(109));
    let wp_dqe = Dqe::Variable(Selector::by_name("GLOBAL_1", false));
    dbg.set_watchpoint_on_expr("GLOBAL_1", wp_dqe, DataWrites)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I64(1), Some(SupportedScalar::I64(0)));
    assert_old_new(&info, "GLOBAL_1", "i64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I64(0), Some(SupportedScalar::I64(3)));
    assert_old_new(&info, "GLOBAL_1", "i64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I64(3), Some(SupportedScalar::I64(1)));
    assert_old_new(&info, "GLOBAL_1", "i64", old, new);
    dbg.continue_debugee().unwrap();

    // watchpoint at global variables never removed automatically
    assert_eq!(dbg.watchpoint_list().len(), 1);
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_max_watchpoint_count() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_line("calculations.rs", 22).unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(22));

    let wp_dqe = Dqe::Variable(Selector::by_name("a", false));
    dbg.set_watchpoint_on_expr("a", wp_dqe, DataWrites).unwrap();
    let wp_dqe = Dqe::Variable(Selector::by_name("b", false));
    dbg.set_watchpoint_on_expr("b", wp_dqe, DataWrites).unwrap();
    let wp_dqe = Dqe::Variable(Selector::by_name("c", false));
    dbg.set_watchpoint_on_expr("c", wp_dqe, DataWrites).unwrap();
    let wp_dqe = Dqe::Variable(Selector::by_name("d", false));
    dbg.set_watchpoint_on_expr("d", wp_dqe, DataWrites).unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(1), Some(SupportedScalar::U64(6)));
    assert_old_new(&info, "a", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(2), Some(SupportedScalar::U64(3)));
    assert_old_new(&info, "b", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(3), Some(SupportedScalar::U64(1)));
    assert_old_new(&info, "c", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(4), Some(SupportedScalar::U64(3)));
    assert_old_new(&info, "d", "u64", old, new);

    dbg.continue_debugee().unwrap();
    assert!(info.new_value.take().is_none());

    assert!(dbg.watchpoint_list().is_empty());
    dbg.continue_debugee().unwrap();
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_remove_and_continue() {
    // 1) set 2 watchpoint with same scope end and same companion breakpoints
    // 2) remove one of watchpoint
    // 3) check that for another watchpoint stop at the end of scope works well - this means
    // that companion not deleted at step 2)
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_line("calculations.rs", 22).unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(22));

    let a_wp_dqe = Dqe::Variable(Selector::by_name("a", false));
    dbg.set_watchpoint_on_expr("a", a_wp_dqe.clone(), DataWrites)
        .unwrap();
    let d_wp_dqe = Dqe::Variable(Selector::by_name("d", false));
    dbg.set_watchpoint_on_expr("d", d_wp_dqe, DataWrites)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(1), Some(SupportedScalar::U64(6)));
    assert_old_new(&info, "a", "u64", old, new);
    dbg.remove_watchpoint_by_expr(a_wp_dqe).unwrap();
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(4), Some(SupportedScalar::U64(3)));
    assert_old_new(&info, "d", "u64", old, new);
    dbg.continue_debugee().unwrap();
    assert!(info.new_value.take().is_none());

    assert!(dbg.watchpoint_list().is_empty());
    dbg.continue_debugee().unwrap();
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_global_var_multithread() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_fn("calculation_global_value_mt")
        .unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(57));

    let wp_dqe = Dqe::Field(
        Dqe::Field(
            Dqe::Variable(Selector::by_name("GLOBAL_2", false)).boxed(),
            "data".to_string(),
        )
        .boxed(),
        "value".to_string(),
    );
    dbg.set_watchpoint_on_expr("GLOBAL_2.data.value", wp_dqe, DataWrites)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(1), Some(SupportedScalar::U64(2)));
    assert_old_new(&info, "GLOBAL_2.data.value", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(2), Some(SupportedScalar::U64(3)));
    assert_old_new(&info, "GLOBAL_2.data.value", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(3), Some(SupportedScalar::U64(4)));
    assert_old_new(&info, "GLOBAL_2.data.value", "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(4), Some(SupportedScalar::U64(5)));
    assert_old_new(&info, "GLOBAL_2.data.value", "u64", old, new);
    dbg.continue_debugee().unwrap();

    // watchpoint at global variables never removed automatically
    assert_eq!(dbg.watchpoint_list().len(), 1);
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_local_var_multithread() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_line("calculations.rs", 68).unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(68));

    let wp_dqe = Dqe::Variable(Selector::by_name("a", false));
    dbg.set_watchpoint_on_expr("a", wp_dqe, DataWrites).unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(1), Some(SupportedScalar::I32(2)));
    assert_old_new(&info, "a", "i32", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(2), Some(SupportedScalar::I32(7)));
    assert_old_new(&info, "a", "i32", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(7), None);
    assert_old_new(&info, "a", "i32", old, new);

    dbg.continue_debugee().unwrap();
    assert!(dbg.watchpoint_list().is_empty());
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_max_watchpoint_count_at_address() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_line("calculations.rs", 22).unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(22));

    fn get_ptr_of(dbg: &Debugger, var: &str) -> RelocatedAddress {
        let addr_dqe = Dqe::Address(Dqe::Variable(Selector::by_name(var, true)).boxed());
        let var = dbg.read_variable(addr_dqe).unwrap();
        let Value::Pointer(PointerValue { value: Some(p), .. }) = var[0].value() else {
            panic!("not a pointer")
        };
        RelocatedAddress::from(*p as usize)
    }

    let ptr_a = get_ptr_of(&dbg, "a");
    let ptr_b = get_ptr_of(&dbg, "b");
    let ptr_c = get_ptr_of(&dbg, "c");
    let ptr_d = get_ptr_of(&dbg, "d");

    let b8 = Bytes8;
    dbg.set_watchpoint_on_memory(ptr_a, b8, DataWrites, false)
        .unwrap();
    dbg.set_watchpoint_on_memory(ptr_b, b8, DataWrites, false)
        .unwrap();
    dbg.set_watchpoint_on_memory(ptr_c, b8, DataWrites, false)
        .unwrap();
    dbg.set_watchpoint_on_memory(ptr_d, b8, DataWrites, false)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(1), Some(SupportedScalar::U64(6)));
    assert_old_new_from_addr(&info, "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(2), Some(SupportedScalar::U64(3)));
    assert_old_new_from_addr(&info, "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(3), Some(SupportedScalar::U64(1)));
    assert_old_new_from_addr(&info, "u64", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::U64(4), Some(SupportedScalar::U64(3)));
    assert_old_new_from_addr(&info, "u64", old, new);

    dbg.remove_watchpoint_by_addr(ptr_a).unwrap();
    dbg.remove_watchpoint_by_addr(ptr_b).unwrap();
    dbg.remove_watchpoint_by_addr(ptr_c).unwrap();
    dbg.remove_watchpoint_by_addr(ptr_d).unwrap();

    dbg.continue_debugee().unwrap();
    assert_no_proc!(debugee_pid);
}

#[test]
#[serial]
fn test_watchpoint_argument() {
    let process = prepare_debugee_process(CALCULATIONS_APP, &[]);
    let debugee_pid = process.pid();
    let info = TestInfo::default();
    let builder = DebuggerBuilder::new().with_hooks(TestHooks::new(info.clone()));
    let mut dbg = builder.build(process).unwrap();
    dbg.set_breakpoint_at_fn("calculate_from_arg").unwrap();

    dbg.start_debugee().unwrap();
    assert_eq!(info.line.take(), Some(102));

    let wp_dqe = Dqe::Variable(Selector::by_name("arg", false));
    dbg.set_watchpoint_on_expr("arg", wp_dqe, DataWrites)
        .unwrap();

    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(1), Some(SupportedScalar::I32(2)));
    assert_old_new(&info, "arg", "i32", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(2), Some(SupportedScalar::I32(4)));
    assert_old_new(&info, "arg", "i32", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(4), Some(SupportedScalar::I32(-1)));
    assert_old_new(&info, "arg", "i32", old, new);
    dbg.continue_debugee().unwrap();
    let (old, new) = (SupportedScalar::I32(-1), None);
    assert_old_new(&info, "arg", "i32", old, new);

    dbg.continue_debugee().unwrap();
    assert!(dbg.watchpoint_list().is_empty());
    assert_no_proc!(debugee_pid);
}
