use tb_frontend::{forms::FormCatalog, source::SourceUnit};
use tb_runtime::{host::CaptureHost, value::Value};
use tb_vm::{
    interp::{RunEvent, Vm},
    project::ProjectCompiler,
};
fn vm(source: &str) -> Vm {
    let mut compiler = ProjectCompiler::default();
    let code = compiler
        .compile(
            "TEST",
            &[SourceUnit::new("TEST", "test.bas", source)],
            &FormCatalog::default(),
            &[],
        )
        .unwrap();
    let mut vm = Vm::new(code);
    vm.debug.enabled = true;
    vm.debug.compiler = Some(compiler.debug_compiler());
    vm
}
#[test]
fn break_continue_loop_and_statement_steps() {
    let mut vm = vm("FOR i% = 1 TO 2\nx% = x% + 1: y% = y% + 1\nNEXT\nEND\n");
    let mut host = CaptureHost::default();
    assert!(vm.add_source_breakpoint(0, 0, 2));
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(0))));
    vm.set_step(true);
    assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 2 });
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
    vm.set_step(false);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    assert!(matches!(vm.inspect("y%"), Some(Value::Int(1))));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
}
#[test]
fn watches_immediate_byref_and_errors_preserve_continuation() {
    let mut vm=vm("DIM SHARED x AS INTEGER\nx = 3\nCALL work(x)\nEND\nSUB work(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nn=n+1\nEND SUB\nSUB bump(n AS INTEGER)\nn=n+5\nEND SUB\n");
    let mut host = CaptureHost::default();
    assert!(vm.add_source_breakpoint(0, 0, 8));
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 8 });
    let location = vm.debug_location().unwrap();
    let frame = location.frame;
    for _ in 0..3 {
        assert!(matches!(
            vm.evaluate_watch(frame, "a(1) + CINT(2.5)").unwrap(),
            Value::Int(5)
        ));
        assert!(vm.evaluate_watch(frame, "RND").is_err());
        assert_eq!(vm.debug_location().unwrap(), location);
    }
    assert!(vm.start_immediate(frame, "n =").is_err());
    vm.start_immediate(frame, "n = 10: CALL bump(n): PRINT n")
        .unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
    assert_eq!(vm.debug_location().unwrap(), location);
    assert!(matches!(vm.inspect("n"), Some(Value::Int(15))));
    vm.start_immediate(frame, "ERROR 5").unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 8 });
    assert!(vm.debug.immediate_error.is_some());
    assert_eq!(vm.debug_location().unwrap(), location);
    vm.clear_source_breakpoints();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x"), Some(Value::Int(16))));
}
#[test]
fn immediate_handler_resume_and_break_on_errors() {
    let mut vm = vm("ON ERROR GOTO handler\nx%=1\nx%=x%+1\nEND\nhandler:\ny%=ERR\nRESUME NEXT\n");
    let mut host = CaptureHost::default();
    vm.add_source_breakpoint(0, 0, 3);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
    let location = vm.debug_location().unwrap();
    vm.debug.break_errors = true;
    vm.start_immediate(location.frame, "ERROR 5: x%=10")
        .unwrap();
    assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
    assert_eq!(vm.debug.error.as_ref().unwrap().code, 5);
    assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 3 });
    assert_eq!(vm.debug_location().unwrap(), location);
    assert!(matches!(vm.inspect("y%"), Some(Value::Int(5))));
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(10))));
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(11))));
}
#[test]
fn step_over_recursive_frames_history_and_safe_jump() {
    let mut vm=vm("CALL rec(2)\nx%=1\nx%=2\nEND\nSUB rec(n AS INTEGER)\nIF n>0 THEN\nCALL rec(n-1)\nEND IF\nPRINT n\nEND SUB\n");
    let mut host = CaptureHost::default();
    vm.debug.history_on = true;
    vm.add_source_breakpoint(0, 0, 7);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 7 });
    let first = vm.debug_location().unwrap();
    vm.clear_source_breakpoints();
    vm.step_over();
    assert!(matches!(vm.run(&mut host), RunEvent::Stepped { .. }));
    assert_eq!(vm.debug_location().unwrap().frame, first.frame);
    vm.set_step(false);
    vm.run_to_cursor(0, 0, 2).unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    let at = vm.debug_location().unwrap();
    vm.set_next_statement(at.frame, 0, 3).unwrap();
    let moved = vm.debug_location().unwrap();
    assert!(vm.set_next_statement(at.frame, 0, 7).is_err());
    assert_eq!(vm.debug_location().unwrap(), moved);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
    assert!(!vm.debug.history.is_empty());
}
#[test]
fn include_identity_qualified_globals_udts_and_older_frames() {
    use tb_frontend::source::SourceSegment;
    let mut compiler = ProjectCompiler::default();
    let first=SourceUnit{name:"FIRST".into(),segments:vec![SourceSegment{file:"one.bi".into(),first_line:1,text:"'one\nx%=1\n".into()},SourceSegment{file:"two.bi".into(),first_line:1,text:"'two\nx%=2\nCALL rec(2)\nEND\nSUB rec(n AS INTEGER)\nDIM a(1 TO 2) AS INTEGER\na(1)=n\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n".into()}]};
    let second=SourceUnit::new("SECOND","second.bas","TYPE pair\nvalue AS INTEGER\nEND TYPE\nDIM SHARED r AS pair\nDIM SHARED a(1 TO 2) AS INTEGER\n");
    let code = compiler
        .compile("P", &[first, second], &FormCatalog::default(), &[])
        .unwrap();
    let mut vm = Vm::new(code);
    vm.debug.compiler = Some(compiler.debug_compiler());
    vm.add_source_breakpoint(0, 1, 2);
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    assert_eq!(vm.current_file(), "two.bi");
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(1))));
    vm.clear_source_breakpoints();
    vm.add_source_breakpoint(0, 1, 9);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 9 });
    let frames = vm.debug_frames();
    assert!(frames.len() >= 4);
    let top = vm.debug_location();
    assert!(matches!(
        vm.evaluate_watch(frames[0].id, "a(1)").unwrap(),
        Value::Int(0)
    ));
    assert!(matches!(
        vm.evaluate_watch(frames[1].id, "a(1)").unwrap(),
        Value::Int(1)
    ));
    assert!(matches!(
        vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
            .unwrap(),
        Value::Int(0)
    ));
    assert_eq!(vm.debug_location(), top);
    vm.start_immediate(frames[1].id, "SECOND!r.value=7: SECOND!a(1)=8")
        .unwrap();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(
        vm.evaluate_watch(frames[1].id, "SECOND!r.value + SECOND!a(1)")
            .unwrap(),
        Value::Int(15)
    ));
}
#[test]
fn watchpoint_history_ring_and_error_differential() {
    use tb_vm::interp::DebugWatch;
    let mut vm = vm("FOR i%=1 TO 1500\nx%=i%\nNEXT\nEND\n");
    vm.debug.enabled = true;
    vm.debug.history_on = true;
    vm.debug.watches.push(DebugWatch {
        expression: "x%=2".into(),
        condition: true,
        frame: None,
        value: Err("pending".into()),
    });
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 3 });
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(2))));
    vm.debug.watches.clear();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert_eq!(vm.debug.history.len(), 1024);
    let source="10 ON ERROR GOTO handler\n20 ERROR 6\n30 x%=x%+1\n40 END\nhandler:\ne%=ERR: l&=ERL\nRESUME NEXT\n";
    let run = |break_errors| {
        let mut vm = self::vm(source);
        vm.debug.break_errors = break_errors;
        let mut host = CaptureHost::default();
        if break_errors {
            assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
            assert_eq!(vm.debug.error.as_ref().unwrap().code, 6);
            assert!(matches!(vm.inspect("e%"), Some(Value::Int(0))));
        }
        assert_eq!(vm.run(&mut host), RunEvent::Ended);
        format!(
            "{:?}{:?}{:?}",
            vm.inspect("e%"),
            vm.inspect("l&"),
            vm.inspect("x%")
        )
    };
    assert_eq!(run(true), run(false));
}
#[test]
fn gosub_step_over_breakpoint_priority_and_temporary_target_cleanup() {
    let mut vm = vm("GOSUB work\nx%=4\nEND\nwork:\nx%=1\nx%=2\nRETURN\n");
    let mut host = CaptureHost::default();
    vm.set_step(true);
    assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 1 });
    vm.step_over();
    vm.add_source_breakpoint(0, 0, 6);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 6 });
    vm.clear_source_breakpoints();
    vm.set_step(false);
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    let mut vm = self::vm("FOR i%=1 TO 2\nx%=x%+1\ny%=y%+1\nNEXT\nEND\n");
    vm.add_source_breakpoint(0, 0, 2);
    vm.run_to_cursor(0, 0, 3).unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    vm.clear_source_breakpoints();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn immediate_wait_interrupt_nested_error_end_and_run() {
    use tb_vm::interp::PollResult;
    let source="x%=1\nx%=x%+1\nEND\nSUB ask(n AS INTEGER)\nINPUT n\nEND SUB\nSUB fail()\nON LOCAL ERROR GOTO handler\nERROR 6\nEXIT SUB\nhandler:\nx%=ERR\nRESUME NEXT\nEND SUB\nSUB done()\nEND\nEND SUB\nSUB restart()\nRUN\nEND SUB\n";
    let mut vm = vm(source);
    let mut host = CaptureHost::default();
    vm.add_source_breakpoint(0, 0, 2);
    vm.run(&mut host);
    let location = vm.debug_location().unwrap();
    vm.start_immediate(location.frame, "CALL ask(x%)").unwrap();
    assert!(matches!(
        vm.poll(&mut host, 1000),
        PollResult::Waiting { .. }
    ));
    assert!(vm.immediate_active());
    vm.rt.abbruch = true;
    assert!(matches!(
        vm.poll(&mut host, 1000),
        PollResult::Event(RunEvent::Interrupted { .. })
    ));
    vm.rt.abbruch = false;
    host.tippe("9\r");
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert_eq!(vm.debug_location().unwrap(), location);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(9))));
    vm.start_immediate(location.frame, "CALL fail()").unwrap();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert_eq!(vm.debug_location().unwrap(), location);
    vm.start_immediate(location.frame, "CALL restart()")
        .unwrap();
    assert_eq!(
        vm.run(&mut host),
        RunEvent::Restart {
            program: None,
            line: None
        }
    );
    let mut vm = self::vm(source);
    vm.add_source_breakpoint(0, 0, 2);
    vm.run(&mut host);
    let frame = vm.debug_location().unwrap().frame;
    vm.start_immediate(frame, "CALL done()").unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(vm.is_terminated());
}
#[test]
fn form_event_breakpoint_and_step_over_keep_the_event_frame() {
    use tb_frontend::forms::ObjectClass;
    let mut catalog = FormCatalog::default();
    catalog.add("Form1", ObjectClass::Form, None, false);
    let source="Form1.Show\nx%=1\nEND\nSUB Form_Load()\nCALL bump\nPRINT 4\nEND SUB\nSUB bump()\nPRINT 3\nEND SUB\n";
    let mut compiler = ProjectCompiler::default();
    let code = compiler
        .compile(
            "Form1",
            &[SourceUnit::new("Form1", "form1.frm", source)],
            &catalog,
            &[],
        )
        .unwrap();
    let mut vm = Vm::new(code);
    vm.debug.compiler = Some(compiler.debug_compiler());
    vm.add_source_breakpoint(0, 0, 5);
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 5 });
    let frame = vm.debug_location().unwrap().frame;
    vm.clear_source_breakpoints();
    vm.step_over();
    assert_eq!(vm.run(&mut host), RunEvent::Stepped { line: 6 });
    assert_eq!(vm.debug_location().unwrap().frame, frame);
    vm.add_source_breakpoint(0, 0, 2);
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 2 });
    vm.clear_source_breakpoints();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_loop_gosub_handler_and_pending_operations_atomically() {
    let source = "x%=1\nFOR i%=1 TO 2\nx%=x%+1\nNEXT\nGOSUB worker\nEND\nworker:\nx%=3\nRETURN\n";
    let mut vm = vm(source);
    let mut host = CaptureHost::default();
    vm.add_source_breakpoint(0, 0, 1);
    vm.run(&mut host);
    let before = vm.debug_location().unwrap();
    for line in [3, 4, 8] {
        assert!(
            vm.set_next_statement(before.frame, 0, line).is_err(),
            "line {line}"
        );
        assert_eq!(vm.debug_location().unwrap(), before);
    }
    vm.clear_source_breakpoints();
    vm.add_source_breakpoint(0, 0, 8);
    vm.run(&mut host);
    let before = vm.debug_location().unwrap();
    assert!(vm.set_next_statement(before.frame, 0, 6).is_err());
    assert_eq!(vm.debug_location().unwrap(), before);
}
#[test]
fn same_line_calls_and_immediate_do_not_retrigger_a_line_breakpoint() {
    let mut vm = vm("x%=1: CALL bump(x%): x%=x%+1\nEND\nSUB bump(n AS INTEGER)\nn=n+1\nEND SUB\n");
    vm.debug.history_on = true;
    vm.add_source_breakpoint(0, 0, 1);
    let mut host = CaptureHost::default();
    assert_eq!(vm.run(&mut host), RunEvent::Breakpoint { line: 1 });
    assert!(vm.debug.history.is_empty());
    let frame = vm.debug_location().unwrap().frame;
    vm.start_immediate(frame, "x%=5").unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Stopped { line: 1 });
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(3))));
    assert_eq!(
        vm.debug
            .history
            .iter()
            .filter(|p| p.procedure == 0 && p.line == 1)
            .count(),
        3
    );
}
#[test]
fn immediate_handler_exit_and_cached_new_imports_are_safe() {
    let mut compiler = ProjectCompiler::default();
    compiler.debug_symbols = true;
    let first = SourceUnit::new("ONE", "one.bas", "x%=1\nSTOP\nx%=x%+1\nEND\n");
    let old = SourceUnit::new("TWO", "two.bas", "SUB oldproc()\nPRINT 1\nEND SUB\n");
    compiler
        .compile("P", &[first.clone(), old], &FormCatalog::default(), &[])
        .unwrap();
    let second = SourceUnit::new(
        "TWO",
        "two.bas",
        "SUB newproc(n AS INTEGER)\nn=n+5\nEND SUB\n",
    );
    let code = compiler
        .compile("P", &[first, second], &FormCatalog::default(), &[])
        .unwrap();
    let mut vm = Vm::new(code);
    vm.debug.compiler = Some(compiler.debug_compiler());
    let mut host = CaptureHost::default();
    vm.run(&mut host);
    let frame = vm.debug_location().unwrap().frame;
    assert_eq!(vm.execution_location().unwrap().line, 3);
    vm.start_immediate(frame, "CALL newproc(x%)").unwrap();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert!(matches!(vm.inspect("ONE!x%"), Some(Value::Int(6))));
    let mut vm=self::vm("CALL work\nEND\nSUB work()\nON LOCAL ERROR GOTO handler\nx%=1\nx%=2\nEXIT SUB\nhandler:\nx%=7\nEND SUB\n");
    vm.add_source_breakpoint(0, 0, 6);
    vm.run(&mut host);
    let before = vm.debug_location().unwrap();
    vm.start_immediate(before.frame, "ERROR 5").unwrap();
    assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
    assert_eq!(vm.debug_location().unwrap(), before);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(7))));
    vm.clear_source_breakpoints();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
}
#[test]
fn set_next_rejects_else_and_case_entry_but_allows_peer_statements() {
    let source="x%=1\nIF x% THEN\nx%=2\nELSE\nx%=3\nEND IF\nSELECT CASE x%\nCASE 1\nx%=4\nCASE ELSE\nx%=5\nEND SELECT\nx%=6\nEND\n";
    let mut vm = vm(source);
    let mut host = CaptureHost::default();
    vm.add_source_breakpoint(0, 0, 1);
    vm.run(&mut host);
    let before = vm.debug_location().unwrap();
    for line in [3, 5, 9, 11] {
        assert!(
            vm.set_next_statement(before.frame, 0, line).is_err(),
            "line {line}"
        );
        assert_eq!(vm.debug_location().unwrap(), before);
    }
    vm.set_next_statement(before.frame, 0, 13).unwrap();
    assert_eq!(vm.run(&mut host), RunEvent::Ended);
    assert!(matches!(vm.inspect("x%"), Some(Value::Int(6))));
}
#[test]
fn watches_never_initialize_arrays_or_run_user_functions_and_keep_selected_context() {
    let mut vm=vm("DIM SHARED changed%\nSTOP\nx%=a%(2)\nCALL rec(2)\nEND\nFUNCTION impure%()\nchanged%=changed%+1\nimpure%=changed%\nEND FUNCTION\nSUB rec(n AS INTEGER)\nIF n>0 THEN CALL rec(n-1)\nPRINT n\nEND SUB\n");
    let mut host = CaptureHost::default();
    vm.run(&mut host);
    let frame = vm.debug_location().unwrap().frame;
    let frames = format!("{:?}", vm.debug_frames());
    for _ in 0..4 {
        assert!(vm.evaluate_watch(frame, "a%(2)").is_err());
        assert!(matches!(vm.inspect("a%"), Some(Value::Empty)));
        assert!(vm.evaluate_watch(frame, "impure%").is_err());
        assert!(matches!(vm.inspect("changed%"), Some(Value::Int(0))));
        assert!(vm.evaluate_watch(frame, "1 / 0").is_err());
        assert!(matches!(
            vm.evaluate_watch(frame, "ERR").unwrap(),
            Value::Lng(0)
        ));
    }
    assert_eq!(format!("{:?}", vm.debug_frames()), frames);
    vm.add_source_breakpoint(0, 0, 12);
    vm.run(&mut host);
    let frames = vm.debug_frames();
    let older = frames[1].id;
    vm.clear_source_breakpoints();
    vm.debug.watches.push(tb_vm::interp::DebugWatch {
        expression: "n=1".into(),
        condition: true,
        frame: Some(older),
        value: Err("pending".into()),
    });
    assert!(matches!(vm.run(&mut host), RunEvent::Breakpoint { .. }));
    assert!(matches!(vm.debug.watches[0].value, Ok(Value::Int(-1))));
}
