import TextBuffer from "../src/text-buffer";
import { describe, it, expect } from "vitest";

describe("TextBuffer – basic editing parity with Rust suite", () => {
  /* ------------------------------------------------------------------ */
  /*  insert_char                                                        */
  /* ------------------------------------------------------------------ */
  it("insert_char / printable (single line)", () => {
    // (col, char, expectedLine)
    const cases: Array<[number, string, string]> = [
      [0, "x", "xab"],
      [1, "x", "axb"],
      [2, "x", "abx"],
      [1, "あ", "aあb"],
    ];

    for (const [col, ch, want] of cases) {
      const buf = new TextBuffer("ab");
      buf.move("end"); // go to col 2
      while (buf.getCursor()[1] > col) {
        buf.move("left");
      }
      buf.insert(ch);
      expect(buf.getText()).toBe(want);
      expect(buf.getCursor()).toEqual([0, col + 1]);
    }
  });

  /* ------------------------------------------------------------------ */
  /*  insert_char – newline support                                      */
  /* ------------------------------------------------------------------ */
  it("insert_char with a newline should split the line", () => {
    const buf = new TextBuffer("ab");
    // jump to end of first (and only) line
    buf.move("end");
    // Insert a raw \n character – the Rust implementation splits the line
    buf.insert("\n");

    // We expect the text to be split into two separate lines
    expect(buf.getLines()).toEqual(["ab", ""]);
    expect(buf.getCursor()).toEqual([1, 0]);
  });

  /* ------------------------------------------------------------------ */
  /*  insert_str helpers                                                 */
  /* ------------------------------------------------------------------ */
  it("insert_str should insert multi‑line strings", () => {
    const initial = ["ab", "cd", "ef"].join("\n");
    const buf = new TextBuffer(initial);

    // place cursor at (row:0, col:0)
    // No move needed – cursor starts at 0,0

    buf.insertStr("x\ny");

    const wantLines = ["x", "yab", "cd", "ef"];
    expect(buf.getLines()).toEqual(wantLines);
    expect(buf.getCursor()).toEqual([1, 1]);
  });

  /* ------------------------------------------------------------------ */
  /*  Undo / Redo                                                        */
  /* ------------------------------------------------------------------ */
  it("undo / redo history should revert edits", () => {
    const buf = new TextBuffer("hello");
    buf.move("end");
    buf.insert("!"); // text becomes "hello!"

    expect(buf.undo()).toBe(true);
    expect(buf.getText()).toBe("hello");

    expect(buf.redo()).toBe(true);
    expect(buf.getText()).toBe("hello!");
  });

  /* ------------------------------------------------------------------ */
  /*  Selection model                                                    */
  /* ------------------------------------------------------------------ */
  it("copy & paste should operate on current selection", () => {
    const buf = new TextBuffer("hello world");
    buf.startSelection();
    // Select the word "hello"
    buf.move("right"); // h
    buf.move("right"); // e
    buf.move("right"); // l
    buf.move("right"); // l
    buf.move("right"); // o
    buf.endSelection();
    buf.copy();

    // Move to end and paste
    buf.move("end");
    // add one space before pasting copied word
    buf.insert(" ");
    buf.paste();

    expect(buf.getText()).toBe("hello world hello");
  });

  /* ------------------------------------------------------------------ */
  /*  Backspace behaviour                                                */
  /* ------------------------------------------------------------------ */

  describe("backspace", () => {
    it("deletes the character to the *left* of the caret within a line", () => {
      const buf = new TextBuffer("abc");

      // Move caret after the second character ( index 2 => after 'b' )
      buf.move("right"); // -> a|bc (col 1)
      buf.move("right"); // -> ab|c (col 2)

      buf.backspace();

      expect(buf.getLines()).toEqual(["ac"]);
      expect(buf.getCursor()).toEqual([0, 1]);
    });

    it("merges with the previous line when invoked at column 0", () => {
      const buf = new TextBuffer(["ab", "cd"].join("\n"));

      // Place caret at the beginning of second line
      buf.move("down"); // row = 1, col = 0

      buf.backspace();

      expect(buf.getLines()).toEqual(["abcd"]);
      expect(buf.getCursor()).toEqual([0, 2]); // after 'b'
    });

    it("is a no‑op at the very beginning of the buffer", () => {
      const buf = new TextBuffer("ab");
      buf.backspace(); // caret starts at (0,0)

      expect(buf.getLines()).toEqual(["ab"]);
      expect(buf.getCursor()).toEqual([0, 0]);
    });
  });

  /* ------------------------------------------------------------------ */
  /*  Vertical cursor movement – we should preserve the preferred column  */
  /* ------------------------------------------------------------------ */

  describe("up / down navigation keeps the preferred column", () => {
    it("restores horizontal position when moving across shorter lines", () => {
      // Three lines: long / short / long
      const lines = ["abcdef", "x", "abcdefg"].join("\n");
      const buf = new TextBuffer(lines);

      // Place caret after the 5th char in first line (col = 5)
      buf.move("end"); // col 6 (after 'f')
      buf.move("left"); // col 5 (between 'e' and 'f')

      // Move down twice – through a short line and back to a long one
      buf.move("down"); // should land on (1, 1) due to clamp
      buf.move("down"); // desired: (2, 5)

      expect(buf.getCursor()).toEqual([2, 5]);
    });
  });

  /* ------------------------------------------------------------------ */
  /*  Left / Right arrow navigation across Unicode surrogate pairs       */
  /* ------------------------------------------------------------------ */

  describe("left / right navigation", () => {
    it("should treat multi‑code‑unit emoji as a single character", () => {
      // '🐶' is a surrogate‑pair (length 2) but one user‑perceived char.
      const buf = new TextBuffer("🐶a");

      // Move caret once to the right – logically past the emoji.
      buf.move("right");

      // Insert another printable character
      buf.insert("x");

      // We expect the emoji to stay intact and the text to be 🐶xa
      expect(buf.getLines()).toEqual(["🐶xa"]);
      // Cursor should be after the inserted char (two visible columns along)
      expect(buf.getCursor()).toEqual([0, 2]);
    });
  });

  /* ------------------------------------------------------------------ */
  /*  HandleInput – raw DEL bytes should map to backspace                */
  /* ------------------------------------------------------------------ */

  it("handleInput should treat \x7f input as backspace", () => {
    const buf = new TextBuffer("");
    const vp = { width: 80, height: 25 };

    // Type "hello" via printable input path
    for (const ch of "hello") {
      buf.handleInput(ch, {}, vp);
    }

    // Two DEL bytes – terminal's backspace
    buf.handleInput("\x7f", {}, vp);
    buf.handleInput("\x7f", {}, vp);

    expect(buf.getText()).toBe("hel");
    expect(buf.getCursor()).toEqual([0, 3]);
  });

  /* ------------------------------------------------------------------ */
  /*  HandleInput – `key.delete` should ALSO behave as backspace          */
  /* ------------------------------------------------------------------ */

  it("handleInput should treat key.delete as backspace", () => {
    const buf = new TextBuffer("");
    const vp = { width: 80, height: 25 };

    for (const ch of "hello") {
      buf.handleInput(ch, {}, vp);
    }

    // Simulate the Delete (Mac backspace) key three times
    buf.handleInput(undefined, { delete: true }, vp);
    buf.handleInput(undefined, { delete: true }, vp);
    buf.handleInput(undefined, { delete: true }, vp);

    expect(buf.getText()).toBe("he");
    expect(buf.getCursor()).toEqual([0, 2]);
  });

  /* ------------------------------------------------------------------ */
  /*  Cursor positioning semantics                                       */
  /* ------------------------------------------------------------------ */

  describe("cursor movement & backspace semantics", () => {
    it("typing should leave cursor after the last inserted character", () => {
      const vp = { width: 80, height: 25 };
      const buf = new TextBuffer("");

      buf.handleInput("h", {}, vp);
      expect(buf.getCursor()).toEqual([0, 1]);

      for (const ch of "ello") {
        buf.handleInput(ch, {}, vp);
      }
      expect(buf.getCursor()).toEqual([0, 5]); // after 'o'
    });

    it("arrow‑left moves the caret to *between* characters (highlight next)", () => {
      const vp = { width: 80, height: 25 };
      const buf = new TextBuffer("");
      for (const ch of "bar") {
        buf.handleInput(ch, {}, vp);
      } // cursor at col 3

      buf.move("left"); // col 2 (right before 'r')
      buf.move("left"); // col 1 (right before 'a')

      expect(buf.getCursor()).toEqual([0, 1]);
      // Character to the RIGHT of caret should be 'a'
      const charRight = [...buf.getLines()[0]!][buf.getCursor()[1]];
      expect(charRight).toBe("a");

      // Backspace should delete the char to the *left* (i.e. 'b'), leaving "ar"
      buf.backspace();
      expect(buf.getLines()[0]).toBe("ar");
      expect(buf.getCursor()).toEqual([0, 0]);
    });
  });
});
