#include "common/log/log.h"

#include "goalc/compiler/Compiler.h"
#include "gtest/gtest.h"
#include "test/goalc/framework/test_runner.h"

#ifdef __linux

namespace {
void connect_compiler_and_debugger(Compiler& compiler, bool do_break) {
  lg::info("connect_compiler_and_debugger:\n");
  bool connect_status = compiler.connect_to_target();
  lg::info("connected: {}\n", connect_status);
  ASSERT_TRUE(connect_status);
  lg::info("poking...\n");
  compiler.poke_target();
  for (int i = 0; i < 100; i++) {
    if (compiler.get_debugger().is_valid()) {
      break;
    } else {
      std::this_thread::sleep_for(std::chrono::milliseconds(50));
      lg::info("Failed to get debugging context {}/100", i);
    }
  }
  ASSERT_TRUE(compiler.get_debugger().is_valid());

  if (do_break) {
    lg::info("break...\n");
    compiler.run_test_from_string("(dbg)");
    lg::info("OK! {} {} {}\n", compiler.get_debugger().is_valid(),
             compiler.get_debugger().is_attached(), compiler.get_debugger().is_halted());
  }
}
}  // namespace
TEST(Jak1Debugger, DebuggerBasicConnect) {
  Compiler compiler(GameVersion::Jak1);
  // evidently you can't ptrace threads in your own process, so we need to run the runtime in a
  // separate process.
  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, true);
    EXPECT_TRUE(compiler.get_debugger().is_valid());
    EXPECT_TRUE(compiler.get_debugger().is_halted());
    compiler.shutdown_target();  // will detach/unhalt, then send the usual shutdown message
    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

TEST(Jak1Debugger, DebuggerBreakAndContinue) {
  Compiler compiler(GameVersion::Jak1);
  // evidently you can't ptrace threads in your own process, so we need to run the runtime in a
  // separate process.
  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, true);
    EXPECT_TRUE(compiler.get_debugger().is_valid());
    EXPECT_TRUE(compiler.get_debugger().is_halted());
    for (int i = 0; i < 20; i++) {
      EXPECT_TRUE(compiler.get_debugger().do_continue());
      EXPECT_TRUE(compiler.get_debugger().do_break());
    }
    compiler.shutdown_target();

    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

TEST(Jak1Debugger, DebuggerReadMemory) {
  Compiler compiler(GameVersion::Jak1);
  // evidently you can't ptrace threads in your own process, so we need to run the runtime in a
  // separate process.
  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, true);
    EXPECT_TRUE(compiler.get_debugger().do_continue());
    auto result = compiler.run_test_from_string("\"test_string!\"");
    EXPECT_TRUE(compiler.get_debugger().do_break());
    auto addr = std::stoi(result.at(0));
    u8 str_buff[256];
    compiler.get_debugger().read_memory(str_buff, 256, addr + 4);

    EXPECT_EQ(0, strcmp((char*)str_buff, "test_string!"));
    compiler.shutdown_target();

    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

TEST(Jak1Debugger, DebuggerWriteMemory) {
  Compiler compiler(GameVersion::Jak1);
  // evidently you can't ptrace threads in your own process, so we need to run the runtime in a
  // separate process.
  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, true);
    EXPECT_TRUE(compiler.get_debugger().do_continue());
    auto result = compiler.run_test_from_string("(define x (the int 123)) 'x");
    EXPECT_TRUE(compiler.get_debugger().do_break());
    auto addr = std::stoi(result.at(0));
    u32 value;
    EXPECT_TRUE(compiler.get_debugger().read_value(&value, addr));
    EXPECT_EQ(value, 123);
    EXPECT_TRUE(compiler.get_debugger().write_value<u32>(456, addr));
    EXPECT_TRUE(compiler.get_debugger().read_value(&value, addr));
    EXPECT_EQ(value, 456);

    EXPECT_TRUE(compiler.get_debugger().do_continue());
    result = compiler.run_test_from_string("x");
    EXPECT_EQ(456, std::stoi(result.at(0)));

    compiler.shutdown_target();

    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

TEST(Jak1Debugger, Symbol) {
  Compiler compiler(GameVersion::Jak1);
  // evidently you can't ptrace threads in your own process, so we need to run the runtime in a
  // separate process.
  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, true);
    EXPECT_TRUE(compiler.get_debugger().do_continue());
    auto result = compiler.run_test_from_string("(define test-symbol (the int 123))");
    EXPECT_TRUE(compiler.get_debugger().do_break());
    auto addr = compiler.get_debugger().get_symbol_address("test-symbol");
    u32 value;
    EXPECT_TRUE(compiler.get_debugger().read_value(&value, addr));
    EXPECT_EQ(value, 123);
    EXPECT_TRUE(compiler.get_debugger().write_value<u32>(456, addr));
    EXPECT_TRUE(compiler.get_debugger().read_value(&value, addr));
    EXPECT_EQ(value, 456);

    EXPECT_TRUE(compiler.get_debugger().do_continue());
    result = compiler.run_test_from_string("test-symbol");
    EXPECT_EQ(456, std::stoi(result.at(0)));

    compiler.shutdown_target();

    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

TEST(Jak1Debugger, SimpleBreakpoint) {
  Compiler compiler(GameVersion::Jak1);

  if (!fork()) {
    GoalTest::runtime_no_kernel_jak1();
    exit(0);
  } else {
    connect_compiler_and_debugger(compiler, false);
    compiler.run_test_from_string(
        "(defun fake-function () 0) (defun test-function () (+ 1 2 3 4 5 6)) (defun "
        "fake-function-2 () 0)");
    compiler.run_test_from_string("(dbg)", "a");
    u32 func_addr;
    EXPECT_TRUE(compiler.get_debugger().get_symbol_value("test-function", &func_addr));
    EXPECT_TRUE(compiler.get_debugger().is_valid());
    EXPECT_TRUE(compiler.get_debugger().is_halted());

    compiler.get_debugger().add_addr_breakpoint(func_addr);  // todo from code.
    compiler.run_test_from_string("(:cont)", "a");
    compiler.run_test_from_string("(test-function)", "a");
    // wait for breakpoint to be hit.
    while (!compiler.get_debugger().is_halted()) {
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    compiler.get_debugger().update_break_info();
    auto expected_instr_before_rip = compiler.get_debugger().get_x86_base_addr() + func_addr;
    auto rip = compiler.get_debugger().get_regs().rip;
    // instructions can be at most 15 bytes long.
    EXPECT_TRUE(rip > expected_instr_before_rip && rip < expected_instr_before_rip + 15);

    // check rsp in goal code to make sure the GOAL stack is in the right space.
    auto rsp = compiler.get_debugger().get_regs().gprs[emitter::RSP];
    EXPECT_TRUE(rsp < compiler.get_debugger().get_x86_base_addr() + EE_MAIN_MEM_SIZE);
    EXPECT_TRUE(rsp > compiler.get_debugger().get_x86_base_addr() + EE_MAIN_MEM_SIZE - (16 * 1024));

    EXPECT_TRUE(compiler.get_debugger().is_halted());
    auto bi = compiler.get_debugger().get_cached_break_info();
    EXPECT_TRUE(bi.knows_function);
    EXPECT_TRUE(bi.knows_object);
    EXPECT_TRUE(bi.object_name == "*listener*");
    EXPECT_TRUE(bi.function_name == "test-function");
    auto disasm = compiler.get_debugger().disassemble_at_rip(bi);
    EXPECT_FALSE(disasm.failed);
    // if we change this to be before the break instruction this might need to be 0 in the future.
    EXPECT_EQ(bi.function_offset, 1);

    compiler.get_debugger().remove_addr_breakpoint(func_addr);
    compiler.get_debugger().do_continue();

    auto result = compiler.run_test_from_string("(test-function)");
    EXPECT_EQ(std::stoi(result.at(0)), 21);
    compiler.shutdown_target();

    // and now the child process should be done!
    EXPECT_TRUE(wait(nullptr) >= 0);
  }
}

#endif
