import { expect, describe, test } from "vitest";
import TextEditor from "~/editor/TextEditor";
import { createRoot } from "~/editor/content/dom/Root";
import { createEmptyParagraph, createParagraph } from "~/editor/content/dom/Paragraph";
import { createInline } from "~/editor/content/dom/Inline";
import { createLineBreak } from "~/editor/content/dom/LineBreak";
import { TextEditorMock } from "~/test/TextEditorMock";
import { SelectionController } from "./SelectionController";
import { SelectionDirection } from "./SelectionDirection";

/* @vitest-environment jsdom */

/**
 * Utility function to make focus and selections work properly in JSDOM.
 *
 * @param {Selection} selection
 * @param {TextEditor} textEditor
 * @param {Node} focusNode
 * @param {number} [focusOffset=0]
 * @param {Node} [anchorNode=null]
 * @param {number} [anchorOffset=0]
 */
function focus(selection, textEditor, focusNode, focusOffset = 0, anchorNode = focusNode, anchorOffset = focusOffset) {
  textEditor.element.focus();
  selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
  document.dispatchEvent(new Event("selectionchange"));
}

describe("SelectionController", () => {
  test("`selection` should return the Selection object kept by the SelectionController", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
    const selection = document.getSelection();
    const selectionController = new SelectionController(textEditorMock, selection);
    expect(selectionController.selection).toBe(selection);
  });

  test("`range` should return the Range object kept by the SelectionController", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    // When the editor hasn't been focused
    // range is null.
    expect(selectionController.range).toBe(null);
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.range).toBeInstanceOf(Range);
  });

  test("`focusAtStart` should return `true` if the offset is 0", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.focusAtStart).toBe(true);
  });

  test("`focusAtEnd` should return `true` if the offset is the length of the `textContent`", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      "Hello, World!".length,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.focusAtEnd).toBe(true);
  });

  test("`anchorAtStart` should return `true` if the offset is 0", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.anchorAtStart).toBe(true);
  });

  test("`anchorAtEnd` should return `true` if the offset is the length of the `textContent`", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      "Hello, World!".length
    );
    expect(selectionController.anchorAtEnd).toBe(true);
  });

  test("`direction` should return the direction of the focus and anchor nodes", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.direction).toBe(SelectionDirection.NONE);
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      5,
      root.firstChild.firstChild.firstChild,
      0
    );
    expect(selectionController.direction).toBe(SelectionDirection.FORWARD);
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.firstChild.firstChild,
      5
    );
    expect(selectionController.direction).toBe(SelectionDirection.BACKWARD);
  });

  test("`insertText` should insert some text in a Text node", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      "Hello".length
    );
    selectionController.insertText(", World!");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(HTMLSpanElement);
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(Text);
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe("Hello, World!");
  });

  test("`replaceLineBreak` should replace a <br> with some text", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockEmpty();
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild
    );
    selectionController.replaceLineBreak("Hello, World!");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(
      HTMLDivElement
    );
    expect(textEditorMock.root.firstChild.dataset.itype).toBe(
      "paragraph"
    );
    expect(
      textEditorMock.root.firstChild.firstChild
    ).toBeInstanceOf(HTMLSpanElement);
    expect(
      textEditorMock.root.firstChild.firstChild.dataset.itype
    ).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
        .nodeValue
    ).toBe("Hello, World!");
  });

  test("`removeBackwardText` should remove text in backward direction (backspace)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      "Hello, World!".length
    );
    selectionController.removeBackwardText();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(
      HTMLDivElement
    );
    expect(textEditorMock.root.firstChild.dataset.itype).toBe(
      "paragraph"
    );
    expect(
      textEditorMock.root.firstChild.firstChild
    ).toBeInstanceOf(HTMLSpanElement);
    expect(
      textEditorMock.root.firstChild.firstChild.dataset.itype
    ).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, World");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
        .nodeValue
    ).toBe("Hello, World");
  });

  test("`removeBackwardText` should remove text in backward direction (backspace) and create a new empty paragraph when there's nothing left", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithText("H");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      "H".length
    );
    selectionController.removeBackwardText();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("");
  });

  test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, "))]),
      createParagraph([createInline(new Text("World!"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.childNodes.item(1).firstChild.firstChild,
      0
    );
    selectionController.mergeBackwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.children.length).toBe(1);
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
  });

  test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, "))]),
      createEmptyParagraph(),
      createParagraph([createInline(new Text("World!"))])
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(textEditorMock, selection);
    focus(
      selection,
      textEditorMock,
      root.childNodes.item(2).firstChild.firstChild,
      0
    );
    selectionController.mergeBackwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
    expect(textEditorMock.root.lastChild.textContent).toBe("World!");
  });

  test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, "))]),
      createParagraph([createInline(new Text("World!"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      root.firstChild.firstChild.firstChild.nodeValue.length
    );
    selectionController.mergeForwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.children.length).toBe(1);
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
  });

  test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, "))]),
      createEmptyParagraph(),
      createParagraph([createInline(new Text("World!"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.childNodes.item(2).firstChild.firstChild,
      0
    );
    selectionController.mergeBackwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
    expect(textEditorMock.root.lastChild.textContent).toBe("World!");
  });

  test("`removeForwardText` should remove text in forward direction (delete)", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
    );
    selectionController.removeForwardText();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(
      HTMLDivElement
    );
    expect(textEditorMock.root.firstChild.dataset.itype).toBe(
      "paragraph"
    );
    expect(
      textEditorMock.root.firstChild.firstChild
    ).toBeInstanceOf(HTMLSpanElement);
    expect(
      textEditorMock.root.firstChild.firstChild.dataset.itype
    ).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("ello, World!");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
        .nodeValue
    ).toBe("ello, World!");
  });

  test("`replaceText` should replace the selected text", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      7,
      root.firstChild.firstChild.firstChild,
      12
    );
    selectionController.replaceText("Mundo");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(
      HTMLDivElement
    );
    expect(textEditorMock.root.firstChild.dataset.itype).toBe(
      "paragraph"
    );
    expect(
      textEditorMock.root.firstChild.firstChild
    ).toBeInstanceOf(HTMLSpanElement);
    expect(
      textEditorMock.root.firstChild.firstChild.dataset.itype
    ).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, Mundo!");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
        .nodeValue
    ).toBe("Hello, Mundo!");
  });

  test("`replaceInlines` should replace the selected text in multiple inlines (2 completelly selected)", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithParagraph([
        createInline(new Text("Hello, ")),
        createInline(new Text("World!"))
      ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.lastChild.firstChild,
      "World!".length
    );
    selectionController.replaceInlines("Mundo");

    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(
      HTMLDivElement
    );
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe(
      "paragraph"
    );
    expect(
      textEditorMock.root.firstChild.firstChild
    ).toBeInstanceOf(HTMLSpanElement);
    expect(
      textEditorMock.root.firstChild.firstChild.dataset.itype
    ).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Mundo");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
        .nodeValue
    ).toBe("Mundo");
  });

  test("`replaceInlines` should replace the selected text in multiple inlines (2 partially selected)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
      createInline(new Text("Hello, ")),
      createInline(new Text("World!")),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      2,
      root.firstChild.lastChild.firstChild,
      "World!".length - 3
    );
    selectionController.replaceInlines("Mundo");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(2);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("HeMundold!");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
      "HeMundo"
    );
    expect(textEditorMock.root.firstChild.lastChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
      "ld!"
    );
  });

  test("`replaceInlines` should replace the selected text in multiple inlines (1 partially selected, 1 completelly selected)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
      createInline(new Text("Hello, ")),
      createInline(new Text("World!")),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      2,
      root.firstChild.lastChild.firstChild,
      "World!".length
    );
    selectionController.replaceInlines("Mundo");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("HeMundo");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
      "HeMundo"
    );
  });

  test("`replaceInlines` should replace the selected text in multiple inlines (1 completelly selected, 1 partially selected)", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
      createInline(new Text("Hello, ")),
      createInline(new Text("World!")),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.lastChild.firstChild,
      "World!".length - 3
    );
    selectionController.replaceInlines("Mundo");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Mundold!");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
      "Mundold!"
    );
  });

  test("`removeSelected` removes a word", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      7,
      root.firstChild.lastChild.firstChild,
      "Hello, World!".length - 1
    );
    selectionController.removeSelected();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, !");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild.nodeValue
    ).toBe("Hello, !");
  });

  test("`removeSelected` multiple inlines", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
      createInline(new Text("Hello, ")),
      createInline(new Text("World!")),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      0,
      root.firstChild.lastChild.firstChild,
      "World!".length
    );
    selectionController.removeSelected();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      HTMLBRElement
    );
  });

  test("`removeSelected` multiple paragraphs", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([
        createInline(new Text("Hello, "))
      ]),
      createParagraph([
        createInline(createLineBreak())
      ]),
      createParagraph([
        createInline(new Text("World!"))
      ]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.lastElementChild,
      0,
      root.children.item(1).firstChild,
      0
    );
    selectionController.removeSelected();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
      "Hello, "
    );
    expect(textEditorMock.root.lastChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
      "World!"
    );
  });

  test("`removeSelected` and `removeBackwardParagraph`", () => {
    const textEditorMock =
      TextEditorMock.createTextEditorMockWithParagraphs([
        createParagraph([createInline(new Text("Hello, World!"))]),
        createParagraph([createInline(createLineBreak())]),
        createParagraph([createInline(new Text("This is a test"))]),
      ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.lastElementChild.firstElementChild.firstChild, // This is a test text
      0,
      root.lastElementChild.firstElementChild.firstChild,
      "This is a test".length
    );
    selectionController.removeSelected();
    selectionController.removeBackwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild
    ).toBeInstanceOf(Text);
    expect(
      textEditorMock.root.firstChild.firstChild.firstChild.nodeValue
    ).toBe("Hello, World!");
  });

  test("`removeSelected` and `removeForwardParagraph`", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, World!"))]),
      createParagraph([createInline(createLineBreak())]),
      createParagraph([createInline(new Text("This is a test"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstElementChild.firstElementChild.firstChild, // This is a test text
      0,
      root.firstElementChild.firstElementChild.firstChild,
      "Hello, World!".length
    );
    selectionController.removeSelected();
    selectionController.removeForwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("This is a test");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      HTMLBRElement
    );
    expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
      "This is a test"
    );
  });

  test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, World!"))]),
      createParagraph([createInline(createLineBreak())]),
      createParagraph([createInline(new Text("This is a test"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstElementChild.firstElementChild.firstChild, // This is a test text
      0,
      root.firstElementChild.firstElementChild.firstChild,
      "Hello, World!".length
    );
    selectionController.removeSelected();

    // This should do nothing.
    selectionController.removeSelected();

    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(3);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("This is a test");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      HTMLBRElement
    );
    expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
      "This is a test"
    );
  });

  test("`removeSelected` removes everything", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, World!"))]),
      createParagraph([createInline(createLineBreak())]),
      createParagraph([createInline(new Text("This is a test"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstElementChild.firstElementChild.firstChild, // This is a test text
      0,
      root.lastElementChild.firstElementChild.firstChild,
      "This is a test".length
    );
    selectionController.removeSelected();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(1);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.textContent).toBe("");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      HTMLBRElement
    );
  });

  test("`removeSelected` removes everything and insert text", () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, World!"))]),
      createParagraph([createInline(createLineBreak())]),
      createParagraph([createInline(new Text("This is a test"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstElementChild.firstElementChild.firstChild, // This is a test text
      0,
      root.lastElementChild.firstElementChild.firstChild,
      "This is a test".length
    );
    selectionController.removeSelected();
    selectionController.replaceLineBreak("Hello, World!");
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children).toHaveLength(1);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.children).toHaveLength(1);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
      Text
    );
    expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
      "Hello, World!"
    );
  });

  test('`applyStyles` to text', () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello, World!");
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(textEditorMock, selection);
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      root.firstChild.firstChild.firstChild.nodeValue.length - 1,
      root.firstChild.firstChild.firstChild,
      root.firstChild.firstChild.firstChild.nodeValue.length - 6
    );
    selectionController.applyStyles({
      "font-weight": "bold"
    });
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(1);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.children.length).toBe(3);
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe("inline");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe("Hello, ");
    expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe("World");
    expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe("!");
  });

  test('`applyStyles` to inlines', () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
      createInline(new Text("Hello, "), {
        "font-style": "italic"
      }),
      createInline(new Text("World!"), {
        "font-style": "oblique"
      })
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(textEditorMock, selection);
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      2,
      root.firstChild.lastChild.firstChild,
      root.firstChild.lastChild.firstChild.nodeValue.length - 3
    );
    selectionController.applyStyles({
      "font-weight": "bold"
    });
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(1);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.children.length).toBe(4);
    expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe("inline");
    expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe("He");
    expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe("llo, ");
    expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe("Wor");
    expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe("ld!");
  });

  test('`applyStyles` to paragraphs', () => {
    const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([
        createInline(new Text("Hello, "), {
          "font-style": "italic",
        }),
      ]),
      createParagraph([
        createInline(new Text("World!"), {
          "font-style": "oblique",
        }),
      ]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.firstChild.firstChild.firstChild,
      2,
      root.lastChild.firstChild.firstChild,
      root.lastChild.firstChild.firstChild.nodeValue.length - 3
    );
    selectionController.applyStyles({
      "font-weight": "bold",
    });
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.children.length).toBe(2);
    expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
      "He"
    );
    expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
      "llo, "
    );
    expect(textEditorMock.root.lastChild.children.length).toBe(2);
    expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe(
      "Wor"
    );
    expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe(
      "ld!"
    );
  });
});