#include <cstdlib>
#include <notcurses.h>
#include "main.h"

class NcplaneTest : public :: testing::Test {
 protected:
  void SetUp() override {
    setlocale(LC_ALL, "");
    if(getenv("TERM") == nullptr){
      GTEST_SKIP();
    }
    notcurses_options nopts{};
    nopts.inhibit_alternate_screen = true;
    nopts.suppress_bannner = true;
    outfp_ = fopen("/dev/tty", "wb");
    ASSERT_NE(nullptr, outfp_);
    nc_ = notcurses_init(&nopts, outfp_);
    ASSERT_NE(nullptr, nc_);
    n_ = notcurses_stdplane(nc_);
    ASSERT_NE(nullptr, n_);
    ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  }

  void TearDown() override {
    if(nc_){
      EXPECT_EQ(0, notcurses_stop(nc_));
    }
    if(outfp_){
      fclose(outfp_);
    }
  }

  struct notcurses* nc_{};
  struct ncplane* n_{};
  FILE* outfp_{};

  void BoxPermutationsRounded(unsigned edges) {
    int dimx, dimy;
    ncplane_dim_yx(n_, &dimy, &dimx);
    ASSERT_LT(2, dimy);
    ASSERT_LT(47, dimx);
    // we'll try all 16 boxmasks in 3x3 configurations in a 1x16 map
    unsigned boxmask = edges << NCBOXCORNER_SHIFT;
    for(auto x0 = 0 ; x0 < 16 ; ++x0){
      EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, x0 * 3));
      EXPECT_EQ(0, ncplane_rounded_box_sized(n_, 0, 0, 3, 3, boxmask));
      ++boxmask;
    }
    EXPECT_EQ(0, notcurses_render(nc_));
  }

};

// Starting position ought be 0, 0 (the origin)
TEST_F(NcplaneTest, StdPlanePosition) {
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, x);
  EXPECT_EQ(0, y);
}

// Dimensions of the standard plane ought be the same as those of the context
TEST_F(NcplaneTest, StdPlaneDimensions) {
  int cols, rows;
  notcurses_term_dim_yx(nc_, &rows, &cols);
  int ncols, nrows;
  ncplane_dim_yx(n_, &nrows, &ncols);
  EXPECT_EQ(rows, nrows);
  EXPECT_EQ(cols, ncols);
}

// Verify that we can move to all four coordinates of the standard plane
TEST_F(NcplaneTest, MoveStdPlaneDimensions) {
  int cols, rows;
  notcurses_term_dim_yx(nc_, &rows, &cols);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(y, 0);
  EXPECT_EQ(x, 0);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, rows - 1, 0));
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(y, rows - 1);
  EXPECT_EQ(x, 0);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, rows - 1, cols - 1));
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(y, rows - 1);
  EXPECT_EQ(x, cols - 1);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, cols - 1));
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(y, 0);
  EXPECT_EQ(x, cols - 1);
}

// Verify that we can move to all four coordinates of the standard plane
TEST_F(NcplaneTest, MoveBeyondPlaneFails) {
  int cols, rows;
  notcurses_term_dim_yx(nc_, &rows, &cols);
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, -1, 0));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, -1, -1));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, 0, -1));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, rows - 1, -1));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, rows, 0));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, rows + 1, 0));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, rows, cols));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, -1, cols - 1));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, 0, cols));
  EXPECT_NE(0, ncplane_cursor_move_yx(n_, 0, cols + 1));
}

TEST_F(NcplaneTest, SetPlaneRGB) {
  EXPECT_EQ(0, ncplane_set_fg_rgb(n_, 0, 0, 0));
  EXPECT_EQ(0, ncplane_set_fg_rgb(n_, 255, 255, 255));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, RejectBadRGB) {
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, -1, 0, 0));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 0, -1, 0));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 0, 0, -1));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, -1, -1, -1));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 256, 255, 255));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 255, 256, 255));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 255, 255, 256));
  EXPECT_NE(0, ncplane_set_fg_rgb(n_, 256, 256, 256));
}

// Verify we can emit a multibyte character, and it advances the cursor
TEST_F(NcplaneTest, EmitCell) {
  const char cchar[] = "✔";
  cell c{};
  EXPECT_EQ(strlen(cchar), cell_load(n_, &c, cchar));
  EXPECT_LT(0, ncplane_putc(n_, &c));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(1, x);
  EXPECT_EQ(0, notcurses_render(nc_));
}

// Verify we can emit a wide character, and it advances the cursor
TEST_F(NcplaneTest, EmitWchar) {
  const wchar_t* w = L"✔";
  int sbytes = 0;
  EXPECT_LT(0, ncplane_putwegc(n_, w, 0, 0, &sbytes));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(1, x);
  EXPECT_EQ(0, notcurses_render(nc_));
}

// Verify we can emit a multibyte string, and it advances the cursor
TEST_F(NcplaneTest, EmitStr) {
  const char s[] = "Σιβυλλα τι θελεις; respondebat illa: αποθανειν θελω.";
  int wrote = ncplane_putstr(n_, s);
  EXPECT_EQ(strlen(s), wrote);
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_NE(1, x); // FIXME tighten in on this
  EXPECT_EQ(0, notcurses_render(nc_));
}

// Verify we can emit a wide string, and it advances the cursor
TEST_F(NcplaneTest, EmitWideStr) {
  const wchar_t s[] = L"Σιβυλλα τι θελεις; respondebat illa: αποθανειν θελω.";
  int wrote = ncplane_putwstr(n_, s);
  EXPECT_LT(0, wrote);
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_NE(1, x); // FIXME tighten in on this
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, EmitEmojiStr) {
  const wchar_t e[] =
    L"🍺🚬🌿💉💊🔫💣🤜🤛🐌🐍🐎🐑🐒🐔🐗🐘🐙🐚"
     "🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐫🐬🐭🐮"
     "🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🦉🐊🐸🦕🦖"
     "🐬🐙🦂🦠🦀";
  int wrote = ncplane_putwstr(n_, e);
  EXPECT_LT(0, wrote);
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_LE(0, y);
  EXPECT_NE(1, x); // FIXME tighten in on this
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, HorizontalLines) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(0, y);
  ASSERT_LT(0, x);
  cell c{};
  cell_load(n_, &c, "-");
  for(int yidx = 0 ; yidx < y ; ++yidx){
    EXPECT_EQ(0, ncplane_cursor_move_yx(n_, yidx, 1));
    EXPECT_EQ(x - 2, ncplane_hline(n_, &c, x - 2));
    int posx, posy;
    ncplane_cursor_yx(n_, &posy, &posx);
    EXPECT_EQ(yidx, posy);
    EXPECT_EQ(x - 1, posx);
  }
  cell_release(n_, &c);
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, VerticalLines) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(0, y);
  ASSERT_LT(0, x);
  cell c{};
  cell_load(n_, &c, "|");
  for(int xidx = 1 ; xidx < x - 1 ; ++xidx){
    EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 1, xidx));
    EXPECT_EQ(y - 2, ncplane_vline(n_, &c, y - 2));
    int posx, posy;
    ncplane_cursor_yx(n_, &posy, &posx);
    EXPECT_EQ(y - 2, posy);
    EXPECT_EQ(xidx, posx - 1);
  }
  cell_release(n_, &c);
  EXPECT_EQ(0, notcurses_render(nc_));
}

// reject attempts to draw boxes beyond the boundaries of the ncplane
TEST_F(NcplaneTest, BadlyPlacedBoxen) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(2, y);
  ASSERT_LT(2, x);
  cell ul{}, ll{}, lr{}, ur{}, hl{}, vl{};
  ASSERT_EQ(0, cells_rounded_box(n_, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, y + 1, x + 1, 0));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 1, 0));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, y, x, 0));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, 1));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, y, x, 0));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 1, x - 1));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, 2, 2, 0));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 2, x - 1));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, 2, 2, 0));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 1, x - 2));
  EXPECT_GT(0, ncplane_box(n_, &ul, &ur, &ll, &lr, &hl, &vl, 2, 2, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, BoxPermutationsRoundedZeroEdges) {
  BoxPermutationsRounded(0);
}

TEST_F(NcplaneTest, BoxPermutationsRoundedOneEdges) {
  BoxPermutationsRounded(1);
}

TEST_F(NcplaneTest, BoxPermutationsRoundedTwoEdges) {
  BoxPermutationsRounded(2);
}

TEST_F(NcplaneTest, BoxPermutationsRoundedThreeEdges) {
  BoxPermutationsRounded(3);
}

TEST_F(NcplaneTest, BoxPermutationsDouble) {
  int dimx, dimy;
  ncplane_dim_yx(n_, &dimy, &dimx);
  ASSERT_LT(2, dimx);
  ASSERT_LT(47, dimx);
  // we'll try all 16 boxmasks in 3x3 configurations in a 1x16 map
  unsigned boxmask = 0;
  for(auto x0 = 0 ; x0 < 16 ; ++x0){
    EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, x0 * 3));
    EXPECT_EQ(0, ncplane_double_box_sized(n_, 0, 0, 3, 3, boxmask));
    ++boxmask;
  }
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, PerimeterRoundedBox) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(2, y);
  ASSERT_LT(2, x);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  EXPECT_EQ(0, ncplane_rounded_box(n_, 0, 0, y - 1, x - 1, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, PerimeterRoundedBoxSized) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(2, y);
  ASSERT_LT(2, x);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  EXPECT_EQ(0, ncplane_rounded_box_sized(n_, 0, 0, y, x, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, PerimeterDoubleBox) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(2, y);
  ASSERT_LT(2, x);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  EXPECT_EQ(0, ncplane_double_box(n_, 0, 0, y - 1, x - 1, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, PerimeterDoubleBoxSized) {
  int x, y;
  ncplane_dim_yx(n_, &y, &x);
  ASSERT_LT(2, y);
  ASSERT_LT(2, x);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  EXPECT_EQ(0, ncplane_double_box_sized(n_, 0, 0, y, x, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, EraseScreen) {
  ncplane_erase(n_);
  EXPECT_EQ(0, notcurses_render(nc_));
}

// we're gonna run both a composed latin a with grave, and then a latin a with
// a combining nonspacing grave
TEST_F(NcplaneTest, CellLoadCombining) {
  const char* w1 = "à"; // U+00E0, U+0000         (c3 a0)
  const char* w2 = "à"; // U+0061, U+0300, U+0000 (61 cc 80)
  const char* w3 = "a"; // U+0061, U+0000         (61)
  cell cell1 = CELL_TRIVIAL_INITIALIZER;
  cell cell2 = CELL_TRIVIAL_INITIALIZER;
  cell cell3 = CELL_TRIVIAL_INITIALIZER;
  auto u1 = cell_load(n_, &cell1, w1);
  auto u2 = cell_load(n_, &cell2, w2);
  auto u3 = cell_load(n_, &cell3, w3);
  ASSERT_EQ(2, u1);
  ASSERT_EQ(3, u2);
  ASSERT_EQ(1, u3);
  cell_release(n_, &cell1);
  cell_release(n_, &cell2);
  cell_release(n_, &cell3);
}

TEST_F(NcplaneTest, CellDuplicateCombining) {
  const char* w1 = "à"; // U+00E0, U+0000         (c3 a0)
  const char* w2 = "à"; // U+0061, U+0300, U+0000 (61 cc 80)
  const char* w3 = "a"; // U+0061, U+0000         (61)
  cell cell1 = CELL_TRIVIAL_INITIALIZER;
  cell cell2 = CELL_TRIVIAL_INITIALIZER;
  cell cell3 = CELL_TRIVIAL_INITIALIZER;
  auto u1 = cell_load(n_, &cell1, w1);
  auto u2 = cell_load(n_, &cell2, w2);
  auto u3 = cell_load(n_, &cell3, w3);
  ASSERT_EQ(2, u1);
  ASSERT_EQ(3, u2);
  ASSERT_EQ(1, u3);
  cell cell4 = CELL_TRIVIAL_INITIALIZER;
  cell cell5 = CELL_TRIVIAL_INITIALIZER;
  cell cell6 = CELL_TRIVIAL_INITIALIZER;
  EXPECT_EQ(2, cell_duplicate(n_, &cell4, &cell1));
  EXPECT_EQ(3, cell_duplicate(n_, &cell5, &cell2));
  EXPECT_EQ(1, cell_duplicate(n_, &cell6, &cell3));
  cell_release(n_, &cell1);
  cell_release(n_, &cell2);
  cell_release(n_, &cell3);
  cell_release(n_, &cell4);
  cell_release(n_, &cell5);
  cell_release(n_, &cell6);
}

TEST_F(NcplaneTest, CellMultiColumn) {
  const char* w1 = "👨";
  const char* w2 = "N";
  cell c1 = CELL_TRIVIAL_INITIALIZER;
  cell c2 = CELL_TRIVIAL_INITIALIZER;
  auto u1 = cell_load(n_, &c1, w1);
  auto u2 = cell_load(n_, &c2, w2);
  ASSERT_EQ(strlen(w1), u1);
  ASSERT_EQ(strlen(w2), u2);
  EXPECT_TRUE(cell_double_wide_p(&c1));
  EXPECT_FALSE(cell_double_wide_p(&c2));
  cell_release(n_, &c1);
  cell_release(n_, &c2);
}

// verifies that the initial userptr is what we provided, that it is a nullptr
// for the standard plane, and that we can change it.
TEST_F(NcplaneTest, UserPtr) {
  EXPECT_EQ(nullptr, ncplane_userptr(n_));
  int x, y;
  void* sentinel = &x;
  notcurses_term_dim_yx(nc_, &y, &x);
  struct ncplane* ncp = notcurses_newplane(nc_, y, x, 0, 0, sentinel);
  ASSERT_NE(ncp, nullptr);
  EXPECT_EQ(&x, ncplane_userptr(ncp));
  EXPECT_EQ(sentinel, ncplane_set_userptr(ncp, nullptr));
  EXPECT_EQ(nullptr, ncplane_userptr(ncp));
  sentinel = &y;
  EXPECT_EQ(nullptr, ncplane_set_userptr(ncp, sentinel));
  EXPECT_EQ(&y, ncplane_userptr(ncp));
  EXPECT_EQ(0, ncplane_destroy(ncp));
}

// create a new plane, the same size as the terminal, and verify that it
// occupies the same dimensions as the standard plane.
TEST_F(NcplaneTest, NewPlaneSameSize) {
  int x, y;
  notcurses_term_dim_yx(nc_, &y, &x);
  struct ncplane* ncp = notcurses_newplane(nc_, y, x, 0, 0, nullptr);
  ASSERT_NE(nullptr, ncp);
  int px, py;
  ncplane_dim_yx(ncp, &py, &px);
  EXPECT_EQ(y, py);
  EXPECT_EQ(x, px);
  int sx, sy;
  ncplane_dim_yx(n_, &sy, &sx);
  EXPECT_EQ(sy, py);
  EXPECT_EQ(sx, px);
  EXPECT_EQ(0, ncplane_destroy(ncp));
}

TEST_F(NcplaneTest, ShrinkPlane) {
  int maxx, maxy;
  int x = 0, y = 0;
  notcurses_term_dim_yx(nc_, &maxy, &maxx);
  struct ncplane* newp = notcurses_newplane(nc_, maxy, maxx, y, x, nullptr);
  ASSERT_NE(nullptr, newp);
  EXPECT_EQ(0, notcurses_render(nc_));
  while(y > 4 && x > 4){
    maxx -= 2;
    maxy -= 2;
    ++x;
    ++y;
    ASSERT_EQ(0, ncplane_resize(newp, 1, 1, maxy, maxx, 1, 1, maxy, maxx));
    EXPECT_EQ(0, notcurses_render(nc_));
    // FIXME check dims, pos
  }
  while(y > 4){
    maxy -= 2;
    ++y;
    ASSERT_EQ(0, ncplane_resize(newp, 1, 0, maxy, maxx, 1, 0, maxy, maxx));
    EXPECT_EQ(0, notcurses_render(nc_));
    // FIXME check dims, pos
  }
  while(x > 4){
    maxx -= 2;
    ++x;
    ASSERT_EQ(0, ncplane_resize(newp, 0, 1, maxy, maxx, 0, 1, maxy, maxx));
    EXPECT_EQ(0, notcurses_render(nc_));
    // FIXME check dims, pos
  }
  ASSERT_EQ(0, ncplane_resize(newp, 0, 0, 0, 0, 0, 0, 2, 2));
  EXPECT_EQ(0, notcurses_render(nc_));
  // FIXME check dims, pos
  ASSERT_EQ(0, ncplane_destroy(newp));
}

TEST_F(NcplaneTest, GrowPlane) {
  int maxx = 2, maxy = 2;
  int x = 0, y = 0;
  int dimy, dimx;
  notcurses_term_dim_yx(nc_, &dimy, &dimx);
  x = dimx / 2 - 1;
  y = dimy / 2 - 1;
  struct ncplane* newp = notcurses_newplane(nc_, maxy, maxx, y, x, nullptr);
  ASSERT_NE(nullptr, newp);
  while(dimx - maxx > 4 && dimy - maxy > 4){
    maxx += 2;
    maxy += 2;
    --x;
    --y;
    // ASSERT_EQ(0, ncplane_resize(newp, 1, 1, maxy, maxx, 1, 1, maxy, maxx));
    // FIXME check dims, pos
  }
  while(y < dimy){
    ++maxy;
    if(y){
      ++y;
    }
    // ASSERT_EQ(0, ncplane_resize(newp, 1, 0, maxy, maxx, 1, 0, maxy, maxx));
    // FIXME check dims, pos
  }
  while(x < dimx){
    ++maxx;
    if(x){
      ++x;
    }
    // ASSERT_EQ(0, ncplane_resize(newp, 0, 1, maxy, maxx, 0, 1, maxy, maxx));
    // FIXME check dims, pos
  }
  ASSERT_EQ(0, ncplane_resize(newp, 0, 0, 0, 0, 0, 0, dimy, dimx));
  // FIXME check dims, pos
  ASSERT_EQ(0, ncplane_destroy(newp));
}

// we ought be able to see what we're about to render, or have just rendered, or
// in any case whatever's in the virtual framebuffer for a plane
TEST_F(NcplaneTest, PlaneAtCursorSimples){
  const char STR1[] = "Jackdaws love my big sphinx of quartz";
  const char STR2[] = "Cwm fjord bank glyphs vext quiz";
  const char STR3[] = "Pack my box with five dozen liquor jugs";
  ncplane_styles_set(n_, 0);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  ASSERT_LT(0, ncplane_putstr(n_, STR1));
  int dimy, dimx;
  ncplane_dim_yx(n_, &dimy, &dimx);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - strlen(STR2)));
  ASSERT_LT(0, ncplane_putstr(n_, STR2));
  int y, x;
  ncplane_cursor_yx(n_, &y, &x);
  ASSERT_EQ(2, y);
  ASSERT_EQ(0, x);
  ASSERT_LT(0, ncplane_putstr(n_, STR3));
  cell testcell = CELL_TRIVIAL_INITIALIZER;
  ASSERT_EQ(0, ncplane_at_cursor(n_, &testcell)); // want nothing at the cursor
  EXPECT_EQ(0, testcell.gcluster);
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR1
  EXPECT_EQ(STR1[0], testcell.gcluster);
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - 1));
  ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want last char of STR2
  EXPECT_EQ(STR2[strlen(STR2) - 1], testcell.gcluster);
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  // FIXME maybe check all cells?
  EXPECT_EQ(0, notcurses_render(nc_));
}

// ensure we read back what's expected for latinesque complex characters
TEST_F(NcplaneTest, PlaneAtCursorComplex){
  const char STR1[] = "Σιβυλλα τι θελεις; respondebat illa:";
  const char STR2[] = "αποθανειν θελω";
  const char STR3[] = "Война и мир"; // just thrown in to complicate things
  ncplane_styles_set(n_, 0);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  ASSERT_LT(0, ncplane_putstr(n_, STR1));
  int dimy, dimx;
  ncplane_dim_yx(n_, &dimy, &dimx);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - mbstowcs(NULL, STR2, 0)));
  ASSERT_LT(0, ncplane_putstr(n_, STR2));
  int y, x;
  ncplane_cursor_yx(n_, &y, &x);
  ASSERT_EQ(2, y);
  ASSERT_EQ(0, x);
  ASSERT_LT(0, ncplane_putstr(n_, STR3));
  cell testcell = CELL_TRIVIAL_INITIALIZER;
  ncplane_at_cursor(n_, &testcell); // should be nothing at the cursor
  EXPECT_EQ(0, testcell.gcluster);
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
  ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR1
  EXPECT_STREQ("Σ", cell_extended_gcluster(n_, &testcell));
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 1, dimx - mbstowcs(NULL, STR2, 0)));
  ASSERT_LT(0, ncplane_at_cursor(n_, &testcell)); // want first char of STR2
  EXPECT_STREQ("α", cell_extended_gcluster(n_, &testcell));
  EXPECT_EQ(0, testcell.attrword);
  EXPECT_EQ(0, testcell.channels);
  // FIXME maybe check all cells?
  EXPECT_EQ(0, notcurses_render(nc_));
}

// half-width, double-width, huge-yet-narrow, all that crap
TEST_F(NcplaneTest, PlaneAtCursorInsane){
  const char EGC0[] = "\uffe0"; // fullwidth cent sign ￠
  const char EGC1[] = "\u00c5"; // neutral A with ring above Å
  const char EGC2[] = "\u20a9"; // half-width won ₩
  const char EGC3[] = "\u212b"; // ambiguous angstrom Å
  const char EGC4[] = "\ufdfd"; // neutral yet huge bismillah ﷽
  std::array<cell, 5> tcells;
  for(auto i = 0u ; i < tcells.size() ; ++i){
    cell_init(&tcells[i]);
  }
  ASSERT_LT(1, cell_load(n_, &tcells[0], EGC0));
  ASSERT_LT(1, cell_load(n_, &tcells[1], EGC1));
  ASSERT_LT(1, cell_load(n_, &tcells[2], EGC2));
  ASSERT_LT(1, cell_load(n_, &tcells[3], EGC3));
  ASSERT_LT(1, cell_load(n_, &tcells[4], EGC4));
  for(auto i = 0u ; i < tcells.size() ; ++i){
    ASSERT_LT(0, ncplane_putc(n_, &tcells[i]));
  }
  EXPECT_EQ(0, notcurses_render(nc_));
  int x = 0;
  for(auto i = 0u ; i < tcells.size() ; ++i){
    EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 0, x));
    cell testcell = CELL_TRIVIAL_INITIALIZER;
    ASSERT_LT(0, ncplane_at_cursor(n_, &testcell));
    EXPECT_STREQ(cell_extended_gcluster(n_, &tcells[i]),
                 cell_extended_gcluster(n_, &testcell));
    EXPECT_EQ(0, testcell.attrword);
    wchar_t w;
    ASSERT_LT(0, mbtowc(&w, cell_extended_gcluster(n_, &tcells[i]), MB_CUR_MAX));
    if(wcwidth(w) == 2){
      EXPECT_EQ(CELL_WIDEASIAN_MASK, testcell.channels);
      ++x;
    }else{
      EXPECT_EQ(0, testcell.channels);
    }
    ++x;
  }
}

// test that we read back correct attrs/colors despite changing defaults
TEST_F(NcplaneTest, PlaneAtCursorAttrs){
  const char STR1[] = "this has been a world destroyer production";
  const char STR2[] = "not to mention dank";
  const char STR3[] = "da chronic lives";
  ncplane_styles_set(n_, CELL_STYLE_BOLD);
  ASSERT_LT(0, ncplane_putstr(n_, STR1));
  int y, x;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y + 1, x - strlen(STR2)));
  ncplane_styles_on(n_, CELL_STYLE_ITALIC);
  ASSERT_LT(0, ncplane_putstr(n_, STR2));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y + 2, x - strlen(STR3)));
  ncplane_styles_off(n_, CELL_STYLE_BOLD);
  ASSERT_LT(0, ncplane_putstr(n_, STR3));
  ncplane_styles_off(n_, CELL_STYLE_ITALIC);
  EXPECT_EQ(0, notcurses_render(nc_));
  int newx;
  ncplane_cursor_yx(n_, &y, &newx);
  EXPECT_EQ(newx, x);
  cell testcell = CELL_TRIVIAL_INITIALIZER;
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 2, x - 1));
  ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
  EXPECT_EQ(testcell.gcluster, STR1[strlen(STR1) - 1]);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y - 1, x - 1));
  ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
  EXPECT_EQ(testcell.gcluster, STR2[strlen(STR2) - 1]);
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y, x - 1));
  ASSERT_EQ(1, ncplane_at_cursor(n_, &testcell));
  EXPECT_EQ(testcell.gcluster, STR3[strlen(STR3) - 1]);
}

TEST_F(NcplaneTest, BoxGradients) {
  const auto sidesz = 5;
  int dimx, dimy;
  ncplane_dim_yx(n_, &dimy, &dimx);
  ASSERT_LT(20, dimy);
  ASSERT_LT(40, dimx);
  cell ul{}, ll{}, lr{}, ur{}, hl{}, vl{};
  ASSERT_EQ(0, cells_double_box(n_, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl));
  EXPECT_EQ(0, channels_set_fg_rgb(&ul.channels, 255, 0, 0));
  EXPECT_EQ(0, channels_set_fg_rgb(&ur.channels, 0, 255, 0));
  EXPECT_EQ(0, channels_set_fg_rgb(&ll.channels, 0, 0, 255));
  EXPECT_EQ(0, channels_set_fg_rgb(&lr.channels, 255, 255, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&ul.channels, 0, 255, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&ur.channels, 255, 0, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&ll.channels, 255, 255, 0));
  EXPECT_EQ(0, channels_set_bg_rgb(&lr.channels, 0, 0, 0));
  // we'll try all 16 gradmasks in sideszXsidesz configs in a 4x4 map
  unsigned gradmask = 0;
  for(auto y0 = 0 ; y0 < 4 ; ++y0){
    for(auto x0 = 0 ; x0 < 4 ; ++x0){
      EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y0 * sidesz, x0 * (sidesz + 1)));
      EXPECT_EQ(0, ncplane_box_sized(n_, &ul, &ur, &ll, &lr, &hl, &vl,
                                     sidesz, sidesz, gradmask << 4u));
      ++gradmask;
    }
  }
  gradmask = 0;
  for(auto y0 = 0 ; y0 < 4 ; ++y0){
    for(auto x0 = 0 ; x0 < 4 ; ++x0){
      EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y0 * sidesz, x0 * (sidesz + 1) + (4 * (sidesz + 1))));
      EXPECT_EQ(0, ncplane_box_sized(n_, &ul, &ur, &ll, &lr, &hl, &vl,
                                     sidesz, sidesz, gradmask << 4u));
      ++gradmask;
    }
  }
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, BoxSideColors) {
  const auto sidesz = 5;
  int dimx, dimy;
  ncplane_dim_yx(n_, &dimy, &dimx);
  ASSERT_LT(20, dimy);
  ASSERT_LT(40, dimx);
  cell ul{}, ll{}, lr{}, ur{}, hl{}, vl{};
  ASSERT_EQ(0, cells_rounded_box(n_, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl));
  // we'll try all 16 boxmasks in sideszXsidesz configurations in a 4x4 map
  EXPECT_EQ(0, channels_set_fg_rgb(&ul.channels, 255, 0, 0));
  EXPECT_EQ(0, channels_set_fg_rgb(&ur.channels, 0, 255, 0));
  EXPECT_EQ(0, channels_set_fg_rgb(&ll.channels, 0, 0, 255));
  EXPECT_EQ(0, channels_set_fg_rgb(&lr.channels, 0, 0, 0));
  EXPECT_EQ(0, channels_set_bg_rgb(&ul.channels, 0, 255, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&ur.channels, 255, 0, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&ll.channels, 255, 255, 0));
  EXPECT_EQ(0, channels_set_bg_rgb(&lr.channels, 0, 0, 0));
  EXPECT_EQ(0, channels_set_fg_rgb(&hl.channels, 255, 0, 255));
  EXPECT_EQ(0, channels_set_fg_rgb(&vl.channels, 255, 255, 255));
  EXPECT_EQ(0, channels_set_bg_rgb(&hl.channels, 0, 255, 0));
  EXPECT_EQ(0, channels_set_bg_rgb(&vl.channels, 0, 0, 0));
  for(auto y0 = 0 ; y0 < 4 ; ++y0){
    for(auto x0 = 0 ; x0 < 4 ; ++x0){
      EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y0 * sidesz, x0 * (sidesz + 1)));
      EXPECT_EQ(0, ncplane_box_sized(n_, &ul, &ur, &ll, &lr, &hl, &vl,
                                     sidesz, sidesz, 0));
    }
  }
  for(auto y0 = 0 ; y0 < 4 ; ++y0){
    for(auto x0 = 0 ; x0 < 4 ; ++x0){
      EXPECT_EQ(0, ncplane_cursor_move_yx(n_, y0 * sidesz, x0 * (sidesz + 1) + (4 * (sidesz + 1))));
      EXPECT_EQ(0, ncplane_box_sized(n_, &ul, &ur, &ll, &lr, &hl, &vl,
                                     sidesz, sidesz, 0));
    }
  }
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, RightToLeft) {
  // give us some room on both sides
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 1, 10));
  int sbytes = -1;
  EXPECT_LT(0, ncplane_putegc(n_, "־", 0, 0, &sbytes));
  EXPECT_EQ(0, notcurses_render(nc_));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 3, 10));
  EXPECT_LT(0, ncplane_putstr(n_, "I can write English with מילים בעברית in the same sentence."));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 5, 10));
  EXPECT_LT(0, ncplane_putstr(n_, "|🔥|I have not yet ־ begun to hack|🔥|"));
  EXPECT_EQ(0, ncplane_cursor_move_yx(n_, 7, 10));
  EXPECT_LT(0, ncplane_putstr(n_, "㉀㉁㉂㉃㉄㉅㉆㉇㉈㉉㉊㉋㉌㉍㉎㉏㉐㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟"));
  EXPECT_EQ(0, notcurses_render(nc_));
}

TEST_F(NcplaneTest, NewPlaneOnRight) {
  int ncols, nrows;
  ncplane_dim_yx(n_, &nrows, &ncols);
  cell ul{}, ll{}, lr{}, ur{}, hl{}, vl{};
  int y, x;
  ncplane_yx(n_, &y, &x);
  struct ncplane* ncp = notcurses_newplane(nc_, 2, 2, y, ncols - 3, nullptr);
  ASSERT_NE(nullptr, ncp);
  ASSERT_EQ(0, cells_rounded_box(ncp, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl));
  EXPECT_EQ(0, ncplane_box(ncp, &ul, &ur, &ll, &lr, &hl, &vl, y + 1, x + 1, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
  // FIXME verify with ncplane_at_cursor()
  EXPECT_EQ(0, ncplane_destroy(ncp));
}
TEST_F(NcplaneTest, MoveToLowerRight) {
  int ncols, nrows;
  ncplane_dim_yx(n_, &nrows, &ncols);
  cell ul{}, ll{}, lr{}, ur{}, hl{}, vl{};
  int y, x;
  ncplane_yx(n_, &y, &x);
  struct ncplane* ncp = notcurses_newplane(nc_, 2, 2, y, x, nullptr);
  ASSERT_NE(nullptr, ncp);
  ASSERT_EQ(0, cells_rounded_box(ncp, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl));
  EXPECT_EQ(0, ncplane_box(ncp, &ul, &ur, &ll, &lr, &hl, &vl, y + 1, x + 1, 0));
  EXPECT_EQ(0, notcurses_render(nc_));
  EXPECT_EQ(0, ncplane_move_yx(ncp, nrows - 3, ncols - 3));
  EXPECT_EQ(0, notcurses_render(nc_));
  EXPECT_EQ(0, ncplane_destroy(ncp));
  // FIXME verify with ncplane_at_cursor()
}

// Placing a wide char to the immediate left of any other char ought obliterate
// that cell.
TEST_F(NcplaneTest, WideCharAnnihilatesRight) {
  const wchar_t* w = L"🐸";
  const wchar_t* wbashed = L"🦂";
  const char bashed = 'X';
  int sbytes = 0;
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, wbashed, 0, 0, &sbytes));
  EXPECT_LT(0, ncplane_putsimple_yx(n_, 1, 1, bashed));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(1, y);
  EXPECT_EQ(2, x);
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, w, 0, 0, &sbytes));
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 1, 0, w, 0, 0, &sbytes));
  cell c = CELL_TRIVIAL_INITIALIZER;
  ncplane_at_yx(n_, 0, 0, &c);
  const char* wres = extended_gcluster(n_, &c);
  EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog
  ncplane_at_yx(n_, 0, 1, &c);
  EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide
  ncplane_at_yx(n_, 0, 2, &c);
  EXPECT_EQ(0, c.gcluster); // should be nothing
  ncplane_at_yx(n_, 1, 0, &c);
  wres = extended_gcluster(n_, &c);
  EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog
  ncplane_at_yx(n_, 1, 1, &c);
  EXPECT_TRUE(cell_double_wide_p(&c)); //should be wide
  ncplane_at_yx(n_, 0, 2, &c);
  EXPECT_EQ(0, c.gcluster);
  EXPECT_EQ(0, notcurses_render(nc_)); // should be nothing
}

// Placing a wide char on the right half of a wide char ought obliterate the
// original wide char.
TEST_F(NcplaneTest, WideCharAnnihilatesWideLeft) {
  const wchar_t* w = L"🐍";
  const wchar_t* wbashed = L"🦂";
  int sbytes = 0;
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashed, 0, 0, &sbytes));
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, w, 0, 0, &sbytes));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(3, x);
  cell c = CELL_TRIVIAL_INITIALIZER;
  ncplane_at_yx(n_, 0, 0, &c);
  EXPECT_EQ(0, c.gcluster); // should be nothing
  ncplane_at_yx(n_, 0, 1, &c);
  const char* wres = extended_gcluster(n_, &c);
  EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake
  ncplane_at_yx(n_, 0, 2, &c);
  EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide
  EXPECT_EQ(0, notcurses_render(nc_));
}

// Placing a normal char on either half of a wide char ought obliterate
// the original wide char.
TEST_F(NcplaneTest, WideCharsAnnihilated) {
  const char cc = 'X';
  const wchar_t* wbashedl = L"🐍";
  const wchar_t* wbashedr = L"🦂";
  int sbytes = 0;
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashedl, 0, 0, &sbytes));
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 2, wbashedr, 0, 0, &sbytes));
  EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 1, cc));
  EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(3, x);
  cell c = CELL_TRIVIAL_INITIALIZER;
  ncplane_at_yx(n_, 0, 0, &c);
  EXPECT_EQ(0, c.gcluster); // should be nothing
  ncplane_at_yx(n_, 0, 1, &c);
  EXPECT_EQ(cc, c.gcluster); // should be 'X'
  ncplane_at_yx(n_, 0, 2, &c);
  EXPECT_EQ(cc, c.gcluster); // should be 'X"
  ncplane_at_yx(n_, 0, 3, &c);
  EXPECT_EQ(0, c.gcluster); // should be nothing
  EXPECT_EQ(0, notcurses_render(nc_));
}

// But placing something to the immediate right of any glyph, that is not a
// problem. Ensure it is so.
TEST_F(NcplaneTest, AdjacentCharsSafe) {
  const char cc = 'X';
  const wchar_t* wsafel = L"🐍";
  const wchar_t* wsafer = L"🦂";
  int sbytes = 0;
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wsafel, 0, 0, &sbytes));
  EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 3, wsafer, 0, 0, &sbytes));
  EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc));
  int x, y;
  ncplane_cursor_yx(n_, &y, &x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(3, x);
  cell c = CELL_TRIVIAL_INITIALIZER;
  ncplane_at_yx(n_, 0, 0, &c);
  const char* wres = extended_gcluster(n_, &c);
  EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake
  ncplane_at_yx(n_, 0, 1, &c);
  EXPECT_TRUE(cell_double_wide_p(&c)); // should be snake
  ncplane_at_yx(n_, 0, 2, &c);
  EXPECT_EQ(cc, c.gcluster); // should be 'X'
  ncplane_at_yx(n_, 0, 3, &c);
  wres = extended_gcluster(n_, &c);
  EXPECT_EQ(0, strcmp(wres, "🦂")); // should be scorpion
  ncplane_at_yx(n_, 0, 4, &c);
  EXPECT_TRUE(cell_double_wide_p(&c)); // should be scorpion
  EXPECT_EQ(0, notcurses_render(nc_));
}
