import { click, fillIn, settled } from "@ember/test-helpers";
import componentTest, {
  setupRenderingTest,
} from "discourse/tests/helpers/component-test";
import {
  discourseModule,
  queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import {
  getTextareaSelection,
  setTextareaSelection,
} from "discourse/tests/helpers/textarea-selection-helper";
import I18n from "I18n";
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import formatTextWithSelection from "discourse/tests/helpers/d-editor-helper";
import hbs from "htmlbars-inline-precompile";
import { next } from "@ember/runloop";
import { withPluginApi } from "discourse/lib/plugin-api";

discourseModule("Integration | Component | d-editor", function (hooks) {
  setupRenderingTest(hooks);

  componentTest("preview updates with markdown", {
    template: hbs`{{d-editor value=value}}`,

    async test(assert) {
      assert.ok(queryAll(".d-editor-button-bar").length);
      await fillIn(".d-editor-input", "hello **world**");

      assert.equal(this.value, "hello **world**");
      assert.equal(
        queryAll(".d-editor-preview").html().trim(),
        "<p>hello <strong>world</strong></p>"
      );
    },
  });

  componentTest("preview sanitizes HTML", {
    template: hbs`{{d-editor value=value}}`,

    async test(assert) {
      await fillIn(".d-editor-input", `"><svg onload="prompt(/xss/)"></svg>`);
      assert.equal(queryAll(".d-editor-preview").html().trim(), '<p>"&gt;</p>');
    },
  });

  componentTest("updating the value refreshes the preview", {
    template: hbs`{{d-editor value=value}}`,

    beforeEach() {
      this.set("value", "evil trout");
    },

    async test(assert) {
      assert.equal(
        queryAll(".d-editor-preview").html().trim(),
        "<p>evil trout</p>"
      );

      this.set("value", "zogstrip");
      await settled();

      assert.equal(
        queryAll(".d-editor-preview").html().trim(),
        "<p>zogstrip</p>"
      );
    },
  });

  function jumpEnd(textarea) {
    textarea.selectionStart = textarea.value.length;
    textarea.selectionEnd = textarea.value.length;
    return textarea;
  }

  function testCase(title, testFunc) {
    componentTest(title, {
      template: hbs`{{d-editor value=value}}`,
      beforeEach() {
        this.set("value", "hello world.");
      },
      test(assert) {
        const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);
        testFunc.call(this, assert, textarea);
      },
    });
  }

  function composerTestCase(title, testFunc) {
    componentTest(title, {
      template: hbs`{{d-editor value=value composerEvents=true}}`,
      beforeEach() {
        this.set("value", "hello world.");
      },

      test(assert) {
        const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);
        testFunc.call(this, assert, textarea);
      },
    });
  }

  testCase(
    `selecting the space before a word`,
    async function (assert, textarea) {
      textarea.selectionStart = 5;
      textarea.selectionEnd = 7;

      await click(`button.bold`);

      assert.equal(this.value, `hello **w**orld.`);
      assert.equal(textarea.selectionStart, 8);
      assert.equal(textarea.selectionEnd, 9);
    }
  );

  testCase(
    `selecting the space after a word`,
    async function (assert, textarea) {
      textarea.selectionStart = 0;
      textarea.selectionEnd = 6;

      await click(`button.bold`);

      assert.equal(this.value, `**hello** world.`);
      assert.equal(textarea.selectionStart, 2);
      assert.equal(textarea.selectionEnd, 7);
    }
  );

  testCase(`bold button with no selection`, async function (assert, textarea) {
    await click(`button.bold`);

    const example = I18n.t(`composer.bold_text`);
    assert.equal(this.value, `hello world.**${example}**`);
    assert.equal(textarea.selectionStart, 14);
    assert.equal(textarea.selectionEnd, 14 + example.length);
  });

  testCase(`bold button with a selection`, async function (assert, textarea) {
    textarea.selectionStart = 6;
    textarea.selectionEnd = 11;

    await click(`button.bold`);
    assert.equal(this.value, `hello **world**.`);
    assert.equal(textarea.selectionStart, 8);
    assert.equal(textarea.selectionEnd, 13);

    await click(`button.bold`);
    assert.equal(this.value, "hello world.");
    assert.equal(textarea.selectionStart, 6);
    assert.equal(textarea.selectionEnd, 11);
  });

  testCase(
    `bold with a multiline selection`,
    async function (assert, textarea) {
      this.set("value", "hello\n\nworld\n\ntest.");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 12;

      await click(`button.bold`);
      assert.equal(this.value, `**hello**\n\n**world**\n\ntest.`);
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 20);

      await click(`button.bold`);
      assert.equal(this.value, `hello\n\nworld\n\ntest.`);
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 12);
    }
  );

  testCase(
    `italic button with no selection`,
    async function (assert, textarea) {
      await click(`button.italic`);
      const example = I18n.t(`composer.italic_text`);
      assert.equal(this.value, `hello world.*${example}*`);

      assert.equal(textarea.selectionStart, 13);
      assert.equal(textarea.selectionEnd, 13 + example.length);
    }
  );

  testCase(`italic button with a selection`, async function (assert, textarea) {
    textarea.selectionStart = 6;
    textarea.selectionEnd = 11;

    await click(`button.italic`);
    assert.equal(this.value, `hello *world*.`);
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 12);

    await click(`button.italic`);
    assert.equal(this.value, "hello world.");
    assert.equal(textarea.selectionStart, 6);
    assert.equal(textarea.selectionEnd, 11);
  });

  testCase(
    `italic with a multiline selection`,
    async function (assert, textarea) {
      this.set("value", "hello\n\nworld\n\ntest.");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 12;

      await click(`button.italic`);
      assert.equal(this.value, `*hello*\n\n*world*\n\ntest.`);
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 16);

      await click(`button.italic`);
      assert.equal(this.value, `hello\n\nworld\n\ntest.`);
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 12);
    }
  );

  componentTest("advanced code", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      this.siteSettings.code_formatting_style = "4-spaces-indent";
      this.set(
        "value",
        `
  function xyz(x, y, z) {
    if (y === z) {
      return true;
    }
  }
  `
      );
    },

    async test(assert) {
      const textarea = queryAll("textarea.d-editor-input")[0];
      textarea.selectionStart = 0;
      textarea.selectionEnd = textarea.value.length;

      await click("button.code");
      assert.equal(
        this.value,
        `
      function xyz(x, y, z) {
        if (y === z) {
          return true;
        }
      }
  `
      );
    },
  });

  componentTest("code button", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      this.siteSettings.code_formatting_style = "4-spaces-indent";
    },

    async test(assert) {
      const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);

      await click("button.code");
      assert.equal(this.value, `    ${I18n.t("composer.code_text")}`);

      this.set("value", "first line\n\nsecond line\n\nthird line");

      textarea.selectionStart = 11;
      textarea.selectionEnd = 11;

      await click("button.code");
      assert.equal(
        this.value,
        `first line
    ${I18n.t("composer.code_text")}
second line

third line`
      );

      this.set("value", "first line\n\nsecond line\n\nthird line");

      await click("button.code");
      assert.equal(
        this.value,
        `first line

second line

third line\`${I18n.t("composer.code_title")}\``
      );
      this.set("value", "first line\n\nsecond line\n\nthird line");

      textarea.selectionStart = 5;
      textarea.selectionEnd = 5;

      await click("button.code");
      assert.equal(
        this.value,
        `first\`${I18n.t("composer.code_title")}\` line

second line

third line`
      );
      this.set("value", "first line\n\nsecond line\n\nthird line");

      textarea.selectionStart = 6;
      textarea.selectionEnd = 10;

      await click("button.code");
      assert.equal(this.value, "first `line`\n\nsecond line\n\nthird line");
      assert.equal(textarea.selectionStart, 7);
      assert.equal(textarea.selectionEnd, 11);

      await click("button.code");
      assert.equal(this.value, "first line\n\nsecond line\n\nthird line");
      assert.equal(textarea.selectionStart, 6);
      assert.equal(textarea.selectionEnd, 10);

      textarea.selectionStart = 0;
      textarea.selectionEnd = 23;

      await click("button.code");
      assert.equal(
        this.value,
        "    first line\n\n    second line\n\nthird line"
      );
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 31);

      await click("button.code");
      assert.equal(this.value, "first line\n\nsecond line\n\nthird line");
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 23);
    },
  });

  componentTest("code fences", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      this.set("value", "");
    },

    async test(assert) {
      const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);

      await click("button.code");
      assert.equal(
        this.value,
        `\`\`\`
${I18n.t("composer.paste_code_text")}
\`\`\``
      );

      assert.equal(textarea.selectionStart, 4);
      assert.equal(textarea.selectionEnd, 27);

      this.set("value", "first line\nsecond line\nthird line");

      textarea.selectionStart = 0;
      textarea.selectionEnd = textarea.value.length;

      await click("button.code");

      assert.equal(
        this.value,
        `\`\`\`
first line
second line
third line
\`\`\`
`
      );

      assert.equal(textarea.selectionStart, textarea.value.length);
      assert.equal(textarea.selectionEnd, textarea.value.length);

      this.set("value", "first line\nsecond line\nthird line");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 0;

      await click("button.code");

      assert.equal(
        this.value,
        `\`${I18n.t("composer.code_title")}\`first line
second line
third line`
      );

      assert.equal(textarea.selectionStart, 1);
      assert.equal(
        textarea.selectionEnd,
        I18n.t("composer.code_title").length + 1
      );

      this.set("value", "first line\nsecond line\nthird line");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 10;

      await click("button.code");

      assert.equal(
        this.value,
        `\`first line\`
second line
third line`
      );

      assert.equal(textarea.selectionStart, 1);
      assert.equal(textarea.selectionEnd, 11);

      this.set("value", "first line\nsecond line\nthird line");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 23;

      await click("button.code");

      assert.equal(
        this.value,
        `\`\`\`
first line
second line
\`\`\`
third line`
      );

      assert.equal(textarea.selectionStart, 30);
      assert.equal(textarea.selectionEnd, 30);

      this.set("value", "first line\nsecond line\nthird line");

      textarea.selectionStart = 6;
      textarea.selectionEnd = 17;

      await click("button.code");

      assert.equal(
        this.value,
        `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line`
      );

      assert.equal(textarea.selectionStart, 27);
      assert.equal(textarea.selectionEnd, 27);
    },
  });

  componentTest("quote button - empty lines", {
    template: hbs`{{d-editor value=value composerEvents=true}}`,
    beforeEach() {
      this.set("value", "one\n\ntwo\n\nthree");
    },
    async test(assert) {
      const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);

      textarea.selectionStart = 0;

      await click("button.blockquote");

      assert.equal(this.value, "> one\n> \n> two\n> \n> three");
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 25);

      await click("button.blockquote");
      assert.equal(this.value, "one\n\ntwo\n\nthree");
    },
  });

  componentTest("quote button - selecting empty lines", {
    template: hbs`{{d-editor value=value composerEvents=true}}`,
    beforeEach() {
      this.set("value", "one\n\n\n\ntwo");
    },
    async test(assert) {
      const textarea = jumpEnd(queryAll("textarea.d-editor-input")[0]);

      textarea.selectionStart = 6;
      textarea.selectionEnd = 10;

      await click("button.blockquote");
      assert.equal(this.value, "one\n\n\n> \n> two");
    },
  });

  testCase("quote button", async function (assert, textarea) {
    textarea.selectionStart = 6;
    textarea.selectionEnd = 9;

    await click("button.blockquote");
    assert.equal(this.value, "hello\n\n> wor\n\nld.");
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 12);

    await click("button.blockquote");

    assert.equal(this.value, "hello\n\nwor\n\nld.");
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 10);

    textarea.selectionStart = 15;
    textarea.selectionEnd = 15;

    await click("button.blockquote");
    assert.equal(this.value, "hello\n\nwor\n\nld.\n\n> Blockquote");
  });

  testCase(
    `bullet button with no selection`,
    async function (assert, textarea) {
      const example = I18n.t("composer.list_item");

      await click(`button.bullet`);
      assert.equal(this.value, `hello world.\n\n* ${example}`);
      assert.equal(textarea.selectionStart, 14);
      assert.equal(textarea.selectionEnd, 16 + example.length);

      await click(`button.bullet`);
      assert.equal(this.value, `hello world.\n\n${example}`);
    }
  );

  testCase(`bullet button with a selection`, async function (assert, textarea) {
    textarea.selectionStart = 6;
    textarea.selectionEnd = 11;

    await click(`button.bullet`);
    assert.equal(this.value, `hello\n\n* world\n\n.`);
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 14);

    await click(`button.bullet`);
    assert.equal(this.value, `hello\n\nworld\n\n.`);
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 12);
  });

  testCase(
    `bullet button with a multiple line selection`,
    async function (assert, textarea) {
      this.set("value", "* Hello\n\nWorld\n\nEvil");

      textarea.selectionStart = 0;
      textarea.selectionEnd = 20;

      await click(`button.bullet`);
      assert.equal(this.value, "Hello\n\nWorld\n\nEvil");
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 18);

      await click(`button.bullet`);
      assert.equal(this.value, "* Hello\n\n* World\n\n* Evil");
      assert.equal(textarea.selectionStart, 0);
      assert.equal(textarea.selectionEnd, 24);
    }
  );

  testCase(`list button with no selection`, async function (assert, textarea) {
    const example = I18n.t("composer.list_item");

    await click(`button.list`);
    assert.equal(this.value, `hello world.\n\n1. ${example}`);
    assert.equal(textarea.selectionStart, 14);
    assert.equal(textarea.selectionEnd, 17 + example.length);

    await click(`button.list`);
    assert.equal(this.value, `hello world.\n\n${example}`);
    assert.equal(textarea.selectionStart, 14);
    assert.equal(textarea.selectionEnd, 14 + example.length);
  });

  testCase(`list button with a selection`, async function (assert, textarea) {
    textarea.selectionStart = 6;
    textarea.selectionEnd = 11;

    await click(`button.list`);
    assert.equal(this.value, `hello\n\n1. world\n\n.`);
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 15);

    await click(`button.list`);
    assert.equal(this.value, `hello\n\nworld\n\n.`);
    assert.equal(textarea.selectionStart, 7);
    assert.equal(textarea.selectionEnd, 12);
  });

  testCase(`list button with line sequence`, async function (assert, textarea) {
    this.set("value", "Hello\n\nWorld\n\nEvil");

    textarea.selectionStart = 0;
    textarea.selectionEnd = 18;

    await click(`button.list`);
    assert.equal(this.value, "1. Hello\n\n2. World\n\n3. Evil");
    assert.equal(textarea.selectionStart, 0);
    assert.equal(textarea.selectionEnd, 27);

    await click(`button.list`);
    assert.equal(this.value, "Hello\n\nWorld\n\nEvil");
    assert.equal(textarea.selectionStart, 0);
    assert.equal(textarea.selectionEnd, 18);
  });

  componentTest("clicking the toggle-direction changes dir from ltr to rtl", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      this.siteSettings.support_mixed_text_direction = true;
      this.siteSettings.default_locale = "en";
    },

    async test(assert) {
      const textarea = queryAll("textarea.d-editor-input");
      await click("button.toggle-direction");
      assert.equal(textarea.attr("dir"), "rtl");
    },
  });

  componentTest("clicking the toggle-direction changes dir from ltr to rtl", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      this.siteSettings.support_mixed_text_direction = true;
      this.siteSettings.default_locale = "en";
    },

    async test(assert) {
      const textarea = queryAll("textarea.d-editor-input");
      textarea.attr("dir", "ltr");
      await click("button.toggle-direction");
      assert.equal(textarea.attr("dir"), "rtl");
    },
  });

  testCase(
    `doesn't jump to bottom with long text`,
    async function (assert, textarea) {
      let longText = "hello world.";
      for (let i = 0; i < 8; i++) {
        longText = longText + longText;
      }
      this.set("value", longText);

      $(textarea).scrollTop(0);
      textarea.selectionStart = 3;
      textarea.selectionEnd = 3;

      await click("button.bold");
      assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up");
    }
  );

  componentTest("emoji", {
    template: hbs`{{d-editor value=value}}`,
    beforeEach() {
      // Test adding a custom button
      withPluginApi("0.1", (api) => {
        api.onToolbarCreate((toolbar) => {
          toolbar.addButton({
            id: "emoji",
            group: "extras",
            icon: "far-smile",
            action: () => toolbar.context.send("emoji"),
          });
        });
      });
      this.set("value", "hello world.");
    },

    afterEach() {
      clearToolbarCallbacks();
    },

    async test(assert) {
      jumpEnd(queryAll("textarea.d-editor-input")[0]);
      await click("button.emoji");

      await click(
        '.emoji-picker .section[data-section="smileys_&_emotion"] img.emoji[title="grinning"]'
      );
      assert.equal(this.value, "hello world. :grinning:");
    },
  });

  testCase("replace-text event by default", async function (assert) {
    this.set("value", "red green blue");

    await this.container
      .lookup("service:app-events")
      .trigger("composer:replace-text", "green", "yellow");

    assert.equal(this.value, "red green blue");
  });

  composerTestCase("replace-text event for composer", async function (assert) {
    this.set("value", "red green blue");

    await this.container
      .lookup("service:app-events")
      .trigger("composer:replace-text", "green", "yellow");

    assert.equal(this.value, "red yellow blue");
  });

  async function paste(element, text) {
    let e = new Event("paste");
    e.clipboardData = { getData: () => text };
    element.dispatchEvent(e);
    await settled();
  }

  componentTest("paste table", {
    template: hbs`{{d-editor value=value composerEvents=true}}`,
    beforeEach() {
      this.set("value", "");
      this.siteSettings.enable_rich_text_paste = true;
    },

    async test(assert) {
      let element = queryAll(".d-editor")[0];
      await paste(element, "\ta\tb\n1\t2\t3");
      assert.equal(this.value, "||a|b|\n|---|---|---|\n|1|2|3|\n");
    },
  });

  componentTest("paste a different table", {
    template: hbs`{{d-editor value=value composerEvents=true}}`,
    beforeEach() {
      this.set("value", "");
      this.siteSettings.enable_rich_text_paste = true;
    },

    async test(assert) {
      let element = queryAll(".d-editor")[0];
      await paste(element, '\ta\tb\n1\t"2\n2.5"\t3');
      assert.equal(this.value, "||a|b|\n|---|---|---|\n|1|2<br>2.5|3|\n");
    },
  });

  (() => {
    // Tests to check cursor/selection after replace-text event.
    const BEFORE = "red green blue";
    const NEEDLE = "green";
    const REPLACE = "yellow";
    const AFTER = BEFORE.replace(NEEDLE, REPLACE);

    const CASES = [
      {
        description: "cursor at start remains there",
        before: [0, 0],
        after: [0, 0],
      },
      {
        description: "cursor before needle becomes cursor before replacement",
        before: [BEFORE.indexOf(NEEDLE), 0],
        after: [AFTER.indexOf(REPLACE), 0],
      },
      {
        description: "cursor at needle start + 1 moves behind replacement",
        before: [BEFORE.indexOf(NEEDLE) + 1, 0],
        after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
      },
      {
        description: "cursor at needle end - 1 stays behind replacement",
        before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 0],
        after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
      },
      {
        description: "cursor behind needle becomes cursor behind replacement",
        before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length, 0],
        after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0],
      },
      {
        description: "cursor at end remains there",
        before: [BEFORE.length, 0],
        after: [AFTER.length, 0],
      },
      {
        description:
          "selection spanning needle start becomes selection until replacement start",
        before: [BEFORE.indexOf(NEEDLE) - 1, 2],
        after: [AFTER.indexOf(REPLACE) - 1, 1],
      },
      {
        description:
          "selection spanning needle end becomes selection from replacement end",
        before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 2],
        after: [AFTER.indexOf(REPLACE) + REPLACE.length, 1],
      },
      {
        description:
          "selection spanning needle becomes selection spanning replacement",
        before: [BEFORE.indexOf(NEEDLE) - 1, NEEDLE.length + 2],
        after: [AFTER.indexOf(REPLACE) - 1, REPLACE.length + 2],
      },
      {
        description: "complete selection remains complete",
        before: [0, BEFORE.length],
        after: [0, AFTER.length],
      },
    ];

    for (let i = 0; i < CASES.length; i++) {
      const CASE = CASES[i];
      // prettier-ignore
      composerTestCase(`replace-text event: ${CASE.description}`, async function( // eslint-disable-line no-loop-func
        assert,
        textarea
      ) {
        this.set("value", BEFORE);

        await focus(textarea);

        assert.ok(textarea.value === BEFORE);

        const [start, len] = CASE.before;
        setTextareaSelection(textarea, start, start + len);

        this.container
          .lookup("service:app-events")
          .trigger("composer:replace-text", "green", "yellow", { forceFocus: true });

        next(() => {
          let expect = formatTextWithSelection(AFTER, CASE.after);
          let actual = formatTextWithSelection(
            this.value,
            getTextareaSelection(textarea)
          );
          assert.equal(actual, expect);
        });
      });
    }
  })();
});
