suite('saneKeyboardEvents', function () {
  var el;
  var Event = function (type, props) {
    return jQuery.extend(jQuery.Event(type), props);
  };

  function supportsSelectionAPI() {
    return 'selectionStart' in el[0];
  }

  setup(function () {
    el = $('<textarea>').appendTo('#mock');
  });

  test('normal keys', function (done) {
    var counter = 0;
    saneKeyboardEvents(el, {
      keystroke: noop,
      typedText: function (text, keydown, keypress) {
        counter += 1;
        assert.ok(counter <= 1, 'callback is only called once');
        assert.equal(text, 'a', 'text comes back as a');
        assert.equal(el.val(), '', 'the textarea remains empty');

        done();
      },
    });

    el.trigger(Event('keydown', { which: 97 }));
    el.trigger(Event('keypress', { which: 97 }));
    el.val('a');
  });

  test('normal keys without keypress', function (done) {
    var counter = 0;
    saneKeyboardEvents(el, {
      keystroke: noop,
      typedText: function (text) {
        counter += 1;
        assert.ok(counter <= 1, 'callback is only called once');
        assert.equal(text, 'a', 'text comes back as a');
        assert.equal(el.val(), '', 'the textarea remains empty');

        done();
      },
    });

    el.trigger(Event('keydown', { which: 97 }));
    el.trigger(Event('keyup', { which: 97 }));
    el.val('a');
  });

  test('one keydown only', function (done) {
    var counter = 0;

    saneKeyboardEvents(el, {
      keystroke: function (key, evt) {
        counter += 1;
        assert.ok(counter <= 1, 'callback is called only once');
        assert.equal(key, 'Backspace', 'key is correctly set');

        done();
      },
    });

    el.trigger(Event('keydown', { which: 8 }));
  });

  test('a series of keydowns only', function (done) {
    var counter = 0;

    saneKeyboardEvents(el, {
      keystroke: function (key, keydown) {
        counter += 1;
        assert.ok(counter <= 3, 'callback is called at most 3 times');

        assert.ok(keydown);
        assert.equal(key, 'Left');

        if (counter === 3) done();
      },
    });

    el.trigger(Event('keydown', { which: 37 }));
    el.trigger(Event('keydown', { which: 37 }));
    el.trigger(Event('keydown', { which: 37 }));
  });

  test('one keydown and a series of keypresses', function (done) {
    var counter = 0;

    saneKeyboardEvents(el, {
      keystroke: function (key, keydown) {
        counter += 1;
        assert.ok(counter <= 3, 'callback is called at most 3 times');

        assert.ok(keydown);
        assert.equal(key, 'Backspace');

        if (counter === 3) done();
      },
    });

    el.trigger(Event('keydown', { which: 8 }));
    el.trigger(Event('keypress', { which: 8 }));
    el.trigger(Event('keypress', { which: 8 }));
    el.trigger(Event('keypress', { which: 8 }));
  });

  suite('select', function () {
    test("select populates the textarea but doesn't call .typedText()", function () {
      var shim = saneKeyboardEvents(el, { keystroke: noop });

      shim.select('foobar');

      assert.equal(el.val(), 'foobar');
      el.trigger('keydown');
      assert.equal(el.val(), 'foobar', 'value remains after keydown');

      if (supportsSelectionAPI()) {
        el.trigger('keypress');
        assert.equal(el.val(), 'foobar', 'value remains after keypress');
        el.trigger('input');
        assert.equal(
          el.val(),
          'foobar',
          'value remains after flush after keypress'
        );
      }
    });

    test(
      "select populates the textarea but doesn't call text" +
        ' on keydown, even when the selection is not properly' +
        ' detectable',
      function () {
        var shim = saneKeyboardEvents(el, { keystroke: noop });

        shim.select('foobar');
        // monkey-patch the dom-level selection so that hasSelection()
        // returns false, as in IE < 9.
        el[0].selectionStart = el[0].selectionEnd = 0;

        el.trigger('keydown');
        assert.equal(el.val(), 'foobar', 'value remains after keydown');
      }
    );

    test('blurring', function () {
      var shim = saneKeyboardEvents(el, { keystroke: noop });

      shim.select('foobar');
      el.trigger('blur');
      el.focus();

      // IE < 9 doesn't support selection{Start,End}
      if (supportsSelectionAPI()) {
        assert.equal(
          el[0].selectionStart,
          0,
          'it is not selected at the start'
        );
        assert.equal(el[0].selectionEnd, 0, 'it is not selected at the end');
      }

      assert.equal(el.val(), '', 'it has no content');
    });

    test('blur then empty selection', function () {
      var shim = saneKeyboardEvents(el, { keystroke: noop });
      shim.select('foobar');
      el.blur();
      shim.select('');
      assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
    });

    test('blur in keystroke handler', function (done) {
      if (!document.hasFocus()) {
        console.warn(
          'The test "blur in keystroke handler" needs the document to have ' +
            'focus. Only when the document has focus does .select() on an ' +
            'element also focus it, which is part of the problematic behavior ' +
            'we are testing robustness against. (Specifically, erroneously ' +
            'calling .select() in a timeout after the textarea has blurred, ' +
            '"stealing back" focus.)\n' +
            'Normally, the page being open and focused is enough to have focus, ' +
            'but with the Developer Tools open, it depends on whether you last ' +
            'clicked on something in the Developer Tools or on the page itself. ' +
            'Click the page, or close the Developer Tools, and Refresh.'
        );
        $('#mock').empty(); // LOL next line skips teardown https://git.io/vaUWq
        this.skip();
      }

      var shim = saneKeyboardEvents(el, {
        keystroke: function (key) {
          assert.equal(key, 'Left');
          el[0].blur();
        },
      });

      shim.select('foobar');
      assert.ok(document.activeElement === el[0], 'textarea focused');

      el.trigger(Event('keydown', { which: 37 }));
      assert.ok(document.activeElement !== el[0], 'textarea blurred');

      setTimeout(function () {
        assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
        done();
      });
    });

    suite(
      "selected text after keypress or paste doesn't get mistaken" +
        ' for inputted text',
      function () {
        test('select() immediately after paste', function () {
          var pastedText;
          var onPaste = function (text) {
            pastedText = text;
          };
          var shim = saneKeyboardEvents(el, {
            paste: function (text) {
              onPaste(text);
            },
          });

          el.trigger('paste').val('$x^2+1$');

          shim.select('$\\frac{x^2+1}{2}$');
          assert.equal(pastedText, '$x^2+1$');
          assert.equal(el.val(), '$\\frac{x^2+1}{2}$');

          onPaste = null;

          shim.select('$2$');
          assert.equal(el.val(), '$2$');
        });

        test('select() after paste/input', function () {
          var pastedText;
          var onPaste = function (text) {
            pastedText = text;
          };
          var shim = saneKeyboardEvents(el, {
            paste: function (text) {
              onPaste(text);
            },
          });

          el.trigger('paste').val('$x^2+1$');

          el.trigger('input');
          assert.equal(pastedText, '$x^2+1$');
          assert.equal(el.val(), '');

          onPaste = null;

          shim.select('$\\frac{x^2+1}{2}$');
          assert.equal(el.val(), '$\\frac{x^2+1}{2}$');

          shim.select('$2$');
          assert.equal(el.val(), '$2$');
        });

        test('select() immediately after keydown/keypress', function () {
          var typedText;
          var onText = function (text) {
            typedText = text;
          };
          var shim = saneKeyboardEvents(el, {
            keystroke: noop,
            typedText: function (text) {
              onText(text);
            },
          });

          el.trigger(Event('keydown', { which: 97 }));
          el.trigger(Event('keypress', { which: 97 }));
          el.val('a');

          shim.select('$\\frac{a}{2}$');
          assert.equal(typedText, 'a');
          assert.equal(el.val(), '$\\frac{a}{2}$');

          onText = null;

          shim.select('$2$');
          assert.equal(el.val(), '$2$');
        });

        test('select() after keydown/keypress/input', function () {
          var typedText;
          var onText = function (text) {
            typedText = text;
          };
          var shim = saneKeyboardEvents(el, {
            keystroke: noop,
            typedText: function (text) {
              onText(text);
            },
          });

          el.trigger(Event('keydown', { which: 97 }));
          el.trigger(Event('keypress', { which: 97 }));
          el.val('a');

          el.trigger('input');
          assert.equal(typedText, 'a');

          onText = null;

          shim.select('$\\frac{a}{2}$');
          assert.equal(el.val(), '$\\frac{a}{2}$');

          shim.select('$2$');
          assert.equal(el.val(), '$2$');
        });

        suite(
          'unrecognized keys that move cursor and clear selection',
          function () {
            test('without keypress', function () {
              var shim = saneKeyboardEvents(el, { keystroke: noop });

              shim.select('a');
              assert.equal(el.val(), 'a');

              if (!supportsSelectionAPI()) return;

              el.trigger(Event('keydown', { which: 37, altKey: true }));
              el[0].selectionEnd = 0;
              el.trigger(Event('keyup', { which: 37, altKey: true }));
              assert.ok(el[0].selectionStart !== el[0].selectionEnd);

              el.blur();
              shim.select('');
              assert.ok(
                document.activeElement !== el[0],
                'textarea remains blurred'
              );
            });

            test('with keypress, many characters selected', function () {
              var shim = saneKeyboardEvents(el, { keystroke: noop });

              shim.select('many characters');
              assert.equal(el.val(), 'many characters');

              if (!supportsSelectionAPI()) return;

              el.trigger(Event('keydown', { which: 37, altKey: true }));
              el.trigger(Event('keypress', { which: 37, altKey: true }));
              el[0].selectionEnd = 0;

              el.trigger('keyup');
              assert.ok(el[0].selectionStart !== el[0].selectionEnd);

              el.blur();
              shim.select('');
              assert.ok(
                document.activeElement !== el[0],
                'textarea remains blurred'
              );
            });

            test('with keypress, only 1 character selected', function () {
              var count = 0;
              var shim = saneKeyboardEvents(el, {
                keystroke: noop,
                typedText: function (ch) {
                  assert.equal(ch, 'a');
                  assert.equal(el.val(), '');
                  count += 1;
                },
              });

              shim.select('a');
              assert.equal(el.val(), 'a');

              if (!supportsSelectionAPI()) return;

              el.trigger(Event('keydown', { which: 37, altKey: true }));
              el.trigger(Event('keypress', { which: 37, altKey: true }));
              el[0].selectionEnd = 0;

              el.trigger('keyup');
              assert.equal(count, 1);

              el.blur();
              shim.select('');
              assert.ok(
                document.activeElement !== el[0],
                'textarea remains blurred'
              );
            });
          }
        );
      }
    );
  });

  suite('paste', function () {
    test('paste event only', function (done) {
      saneKeyboardEvents(el, {
        paste: function (text) {
          assert.equal(text, '$x^2+1$');

          done();
        },
      });

      el.trigger('paste');
      el.val('$x^2+1$');
    });

    test('paste after keydown/keypress', function (done) {
      saneKeyboardEvents(el, {
        keystroke: noop,
        paste: function (text) {
          assert.equal(text, 'foobar');
          done();
        },
      });

      // Ctrl-V in Firefox or Opera, according to unixpapa.com/js/key.html
      // without an `input` event
      el.trigger('keydown', { which: 86, ctrlKey: true });
      el.trigger('keypress', { which: 118, ctrlKey: true });
      el.trigger('paste');
      el.val('foobar');
    });

    test('paste after keydown/keypress/input', function (done) {
      saneKeyboardEvents(el, {
        keystroke: noop,
        paste: function (text) {
          assert.equal(text, 'foobar');
          done();
        },
      });

      // Ctrl-V in Firefox or Opera, according to unixpapa.com/js/key.html
      // with an `input` event
      el.trigger('keydown', { which: 86, ctrlKey: true });
      el.trigger('keypress', { which: 118, ctrlKey: true });
      el.trigger('paste');
      el.val('foobar');
      el.trigger('input');
    });

    test('keypress timeout happening before paste timeout', function (done) {
      saneKeyboardEvents(el, {
        keystroke: noop,
        paste: function (text) {
          assert.equal(text, 'foobar');
          done();
        },
      });

      el.trigger('keydown', { which: 86, ctrlKey: true });
      el.trigger('keypress', { which: 118, ctrlKey: true });
      el.trigger('paste');
      el.val('foobar');

      // this synthesizes the keypress timeout calling handleText()
      // before the paste timeout happens.
      el.trigger('input');
    });

    test('pasting into a focused textarea should not fire a redundant focus event', function (done) {
      el.focus();

      var focusCalled = false;
      el.focus(function () {
        focusCalled = true;
      });

      saneKeyboardEvents(el, {
        paste: function () {
          assert.ok(
            !focusCalled,
            'Pasting into a focused mathquill should not fire a focus event'
          );
          done();
        },
      });

      // Simulate a paste
      el.trigger('paste');
      el.val('2');
      el.trigger('input');
    });
  });

  suite('copy', function () {
    test('only runs handler once even if handler synchronously selects', function () {
      // ...which MathQuill does and resulted in a stack overflow: https://git.io/vosm0
      var shim = saneKeyboardEvents(el, {
        copy: function () {
          shim.select();
        },
      });

      el.trigger('copy');
    });
  });
});
