#include <gtest/gtest.h>

#include <event2/event.h>
#include <event2/thread.h>
#include <event2/util.h>

#include <chrono>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#ifndef _WIN32
#include <unistd.h>
#endif

#include "interactive.h"
#include "net/sys_telnet.h"
#include "net/telnet.h"
#include "terminal.h"
#include "thirdparty/libtelnet/libtelnet.h"

namespace {

struct TerminalTestSession {
  ~TerminalTestSession() {
    if (terminal_initialized) {
      if (session_id) {
        terminal_close(session_id);
      }
      terminal_shutdown();
    }
    if (base) {
      event_base_free(base);
    }
    for (auto socket : sockets) {
      if (socket != -1) {
        evutil_closesocket(socket);
      }
    }
  }

  evutil_socket_t sockets[2]{-1, -1};
  struct event_base *base{nullptr};
  uint64_t session_id{0};
  bool terminal_initialized{false};
};

void initialize_event_threads() {
  static std::once_flag initialized;
  std::call_once(initialized, []() {
#ifdef _WIN32
    ASSERT_EQ(evthread_use_windows_threads(), 0);
#else
    ASSERT_EQ(evthread_use_pthreads(), 0);
#endif
  });
}

void initialize_terminal(TerminalTestSession *terminal) {
  initialize_event_threads();
#ifdef _WIN32
  constexpr int socket_family = AF_INET;
#else
  constexpr int socket_family = AF_UNIX;
#endif
  ASSERT_EQ(evutil_socketpair(socket_family, SOCK_STREAM, 0, terminal->sockets), 0);
  ASSERT_EQ(evutil_make_socket_nonblocking(terminal->sockets[1]), 0);

  terminal->base = event_base_new();
  ASSERT_NE(terminal->base, nullptr);
  ASSERT_TRUE(terminal_init(terminal->base));
  terminal->terminal_initialized = true;

  terminal->session_id = terminal_next_session_id();
  ASSERT_TRUE(terminal_attach(terminal->session_id, 0, terminal->sockets[0], nullptr));
  terminal->sockets[0] = -1;
}

void send_bytes(evutil_socket_t fd, std::string_view data) {
#ifdef _WIN32
  auto count = send(fd, data.data(), static_cast<int>(data.size()), 0);
#else
  auto count = write(fd, data.data(), data.size());
#endif
  ASSERT_EQ(count, static_cast<decltype(count)>(data.size()));
}

std::string read_until_quiet(evutil_socket_t fd) {
  std::string result;
  auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
  auto quiet_since = std::chrono::steady_clock::time_point{};
  char buffer[256];

  while (std::chrono::steady_clock::now() < deadline) {
#ifdef _WIN32
    auto count = recv(fd, buffer, sizeof(buffer), 0);
#else
    auto count = read(fd, buffer, sizeof(buffer));
#endif
    if (count > 0) {
      result.append(buffer, count);
      quiet_since = std::chrono::steady_clock::now();
      continue;
    }

    if (!result.empty() && quiet_since != std::chrono::steady_clock::time_point{} &&
        std::chrono::steady_clock::now() - quiet_since >= std::chrono::milliseconds(100)) {
      break;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }

  return result;
}

bool contains_bytes(std::string_view haystack, std::string_view needle) {
  return haystack.find(needle) != std::string_view::npos;
}

}  // namespace

TEST(TerminalEditorTest, NormalizesAndEchoesLineEndings) {
  TerminalTestSession terminal;
  initialize_terminal(&terminal);
  terminal_set_editor(terminal.session_id, true);
  terminal_set_echo(terminal.session_id, true);

  send_bytes(terminal.sockets[1], "a\r");
  EXPECT_EQ(read_until_quiet(terminal.sockets[1]), "a\r\n");
  send_bytes(terminal.sockets[1], std::string_view("\0", 1));
  send_bytes(terminal.sockets[1], "b\r");
  EXPECT_EQ(read_until_quiet(terminal.sockets[1]), "b\r\n");
  send_bytes(terminal.sockets[1], "\n");
  send_bytes(terminal.sockets[1], "c\n");
  EXPECT_EQ(read_until_quiet(terminal.sockets[1]), "c\r\n");
}

TEST(TerminalEditorTest, EchoesRawCharacterModeInput) {
  TerminalTestSession terminal;
  initialize_terminal(&terminal);
  terminal_set_editor(terminal.session_id, false);
  terminal_set_echo(terminal.session_id, true);

  send_bytes(terminal.sockets[1], "a\r");
  EXPECT_EQ(read_until_quiet(terminal.sockets[1]), "a\r\n");
  send_bytes(terminal.sockets[1], std::string_view("\0", 1));
  send_bytes(terminal.sockets[1], "b\n");
  EXPECT_EQ(read_until_quiet(terminal.sockets[1]), "b\r\n");
}

TEST(TerminalTelnetTest, TracksEchoNegotiation) {
  TerminalTestSession terminal;
  initialize_terminal(&terminal);
  terminal_set_editor(terminal.session_id, true);

  interactive_t user {};
  user.terminal_session_id = terminal.session_id;
  user.iflags = TERMINAL_EDITOR;
  user.telnet = net_telnet_init(&user);
  ASSERT_NE(user.telnet, nullptr);

  set_localecho(&user, true, true);
  const char will_echo[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_WILL),
      static_cast<char>(TELNET_TELOPT_ECHO)};
  EXPECT_TRUE(contains_bytes(read_until_quiet(terminal.sockets[1]),
                             std::string_view(will_echo, sizeof(will_echo))));
  EXPECT_TRUE(user.terminal_echo_desired);
  EXPECT_FALSE(user.terminal_echo_negotiated);

  const char do_echo[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_DO),
      static_cast<char>(TELNET_TELOPT_ECHO)};
  telnet_recv(user.telnet, do_echo, sizeof(do_echo));
  EXPECT_TRUE(user.terminal_echo_negotiated);

  set_localecho(&user, false, true);
  EXPECT_FALSE(user.terminal_echo_desired);
  EXPECT_TRUE(user.terminal_echo_negotiated);

  const char dont_echo[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_DONT),
      static_cast<char>(TELNET_TELOPT_ECHO)};
  telnet_recv(user.telnet, dont_echo, sizeof(dont_echo));
  EXPECT_FALSE(user.terminal_echo_negotiated);

  telnet_free(user.telnet);
}

TEST(TerminalTelnetTest, KeepsSgaWhenDisablingLinemode) {
  TerminalTestSession terminal;
  initialize_terminal(&terminal);

  interactive_t user {};
  user.terminal_session_id = terminal.session_id;
  user.iflags = TERMINAL_EDITOR;
  user.telnet = net_telnet_init(&user);
  ASSERT_NE(user.telnet, nullptr);

  const char will_linemode[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_WILL),
      static_cast<char>(TELNET_TELOPT_LINEMODE)};
  telnet_recv(user.telnet, will_linemode, sizeof(will_linemode));
  const auto negotiation = read_until_quiet(terminal.sockets[1]);
  const char dont_linemode[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_DONT),
      static_cast<char>(TELNET_TELOPT_LINEMODE)};
  const char will_sga[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_WILL),
      static_cast<char>(TELNET_TELOPT_SGA)};
  EXPECT_TRUE(contains_bytes(negotiation, std::string_view(dont_linemode, sizeof(dont_linemode))));
  EXPECT_TRUE(contains_bytes(negotiation, std::string_view(will_sga, sizeof(will_sga))));

  const char do_sga[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_DO),
      static_cast<char>(TELNET_TELOPT_SGA)};
  telnet_recv(user.telnet, do_sga, sizeof(do_sga));
  EXPECT_NE(user.iflags & SUPPRESS_GA, 0u);

  const char dont_sga[] = {
      static_cast<char>(TELNET_IAC), static_cast<char>(TELNET_DONT),
      static_cast<char>(TELNET_TELOPT_SGA)};
  telnet_recv(user.telnet, dont_sga, sizeof(dont_sga));
  EXPECT_EQ(user.iflags & SUPPRESS_GA, 0u);

  telnet_free(user.telnet);
}
