/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import '../../test/common-test-setup-karma.js';
import {
  KeyboardShortcutMixin, Shortcut,
  ShortcutManager, ShortcutSection, SPECIAL_SHORTCUT,
} from './keyboard-shortcut-mixin.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';

const basicFixture =
    fixtureFromElement('keyboard-shortcut-mixin-test-element');

const withinOverlayFixture = fixtureFromTemplate(html`
<gr-overlay>
  <keyboard-shortcut-mixin-test-element>
  </keyboard-shortcut-mixin-test-element>
</gr-overlay>
`);

class GrKeyboardShortcutMixinTestElement extends
  KeyboardShortcutMixin(PolymerElement) {
  static get is() {
    return 'keyboard-shortcut-mixin-test-element';
  }

  get keyBindings() {
    return {
      k: '_handleKey',
      enter: '_handleKey',
    };
  }

  _handleKey() {}
}

customElements.define(GrKeyboardShortcutMixinTestElement.is,
    GrKeyboardShortcutMixinTestElement);

suite('keyboard-shortcut-mixin tests', () => {
  let element;
  let overlay;

  setup(() => {
    element = basicFixture.instantiate();
    overlay = withinOverlayFixture.instantiate();
  });

  suite('ShortcutManager', () => {
    test('bindings management', () => {
      const mgr = new ShortcutManager();
      const NEXT_FILE = Shortcut.NEXT_FILE;

      assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
      mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
      assert.deepEqual(
          mgr.getBindingsForShortcut(NEXT_FILE),
          [']', '}', 'right']);
    });

    test('getShortcut', () => {
      const mgr = new ShortcutManager();
      const NEXT_FILE = Shortcut.NEXT_FILE;

      assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
      mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
      assert.equal(mgr.getShortcut(NEXT_FILE), '],},→');
    });

    test('getShortcut with modifiers', () => {
      const mgr = new ShortcutManager();
      const NEXT_FILE = Shortcut.NEXT_FILE;

      assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
      mgr.bindShortcut(NEXT_FILE, 'Shift+a:key');
      assert.equal(mgr.getShortcut(NEXT_FILE), 'Shift+a');
    });

    suite('binding descriptions', () => {
      function mapToObject(m) {
        const o = {};
        m.forEach((v, k) => o[k] = v);
        return o;
      }

      test('single combo description', () => {
        const mgr = new ShortcutManager();
        assert.deepEqual(mgr.describeBinding('a'), ['a']);
        assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
        assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
        assert.deepEqual(
            mgr.describeBinding('ctrl+shift+up:keyup'),
            ['Ctrl', 'Shift', '↑']);
      });

      test('combo set description', () => {
        const mgr = new ShortcutManager();
        assert.isNull(mgr.describeBindings(Shortcut.NEXT_FILE));

        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
            SPECIAL_SHORTCUT.GO_KEY, 'o');
        assert.deepEqual(
            mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
            [['g', 'o']]);

        mgr.bindShortcut(Shortcut.NEXT_FILE, SPECIAL_SHORTCUT.DOC_ONLY,
            ']', 'ctrl+shift+right:keyup');
        assert.deepEqual(
            mgr.describeBindings(Shortcut.NEXT_FILE),
            [[']'], ['Ctrl', 'Shift', '→']]);

        mgr.bindShortcut(Shortcut.PREV_FILE, '[');
        assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
      });

      test('combo set description width', () => {
        const mgr = new ShortcutManager();
        assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
        assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
        assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
        assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
        assert.strictEqual(
            mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
            12);
      });

      test('distribute shortcut help', () => {
        const mgr = new ShortcutManager();
        assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
        assert.deepEqual(
            mgr.distributeBindingDesc([['g', 'o']]),
            [[['g', 'o']]]);
        assert.deepEqual(
            mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
            [[['ctrl', 'shift', 'meta', 'enter']]]);
        assert.deepEqual(
            mgr.distributeBindingDesc([
              ['ctrl', 'shift', 'meta', 'enter'],
              ['o'],
            ]),
            [
              [['ctrl', 'shift', 'meta', 'enter']],
              [['o']],
            ]);
        assert.deepEqual(
            mgr.distributeBindingDesc([
              ['ctrl', 'enter'],
              ['meta', 'enter'],
              ['ctrl', 's'],
              ['meta', 's'],
            ]),
            [
              [['ctrl', 'enter'], ['meta', 'enter']],
              [['ctrl', 's'], ['meta', 's']],
            ]);
      });

      test('active shortcuts by section', () => {
        const mgr = new ShortcutManager();
        mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
        mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES, 'g+o');
        mgr.bindShortcut(Shortcut.SEARCH, '/');

        assert.deepEqual(
            mapToObject(mgr.activeShortcutsBySection()),
            {});

        mgr.attachHost({
          keyboardShortcuts() {
            return {
              [Shortcut.NEXT_FILE]: null,
            };
          },
        });
        assert.deepEqual(
            mapToObject(mgr.activeShortcutsBySection()),
            {
              [ShortcutSection.NAVIGATION]: [
                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
              ],
            });

        mgr.attachHost({
          keyboardShortcuts() {
            return {
              [Shortcut.NEXT_LINE]: null,
            };
          },
        });
        assert.deepEqual(
            mapToObject(mgr.activeShortcutsBySection()),
            {
              [ShortcutSection.DIFFS]: [
                {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
              ],
              [ShortcutSection.NAVIGATION]: [
                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
              ],
            });

        mgr.attachHost({
          keyboardShortcuts() {
            return {
              [Shortcut.SEARCH]: null,
              [Shortcut.GO_TO_OPENED_CHANGES]: null,
            };
          },
        });
        assert.deepEqual(
            mapToObject(mgr.activeShortcutsBySection()),
            {
              [ShortcutSection.DIFFS]: [
                {shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
              ],
              [ShortcutSection.EVERYWHERE]: [
                {shortcut: Shortcut.SEARCH, text: 'Search'},
                {
                  shortcut: Shortcut.GO_TO_OPENED_CHANGES,
                  text: 'Go to Opened Changes',
                },
              ],
              [ShortcutSection.NAVIGATION]: [
                {shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
              ],
            });
      });

      test('directory view', () => {
        const mgr = new ShortcutManager();
        mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
        mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
        mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
            SPECIAL_SHORTCUT.GO_KEY, 'o');
        mgr.bindShortcut(Shortcut.SEARCH, '/');
        mgr.bindShortcut(
            Shortcut.SAVE_COMMENT, 'ctrl+enter', 'meta+enter',
            'ctrl+s', 'meta+s');

        assert.deepEqual(mapToObject(mgr.directoryView()), {});

        mgr.attachHost({
          keyboardShortcuts() {
            return {
              [Shortcut.GO_TO_OPENED_CHANGES]: null,
              [Shortcut.NEXT_FILE]: null,
              [Shortcut.NEXT_LINE]: null,
              [Shortcut.SAVE_COMMENT]: null,
              [Shortcut.SEARCH]: null,
            };
          },
        });
        assert.deepEqual(
            mapToObject(mgr.directoryView()),
            {
              [ShortcutSection.DIFFS]: [
                {binding: [['j']], text: 'Go to next line'},
                {
                  binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
                  text: 'Save comment',
                },
                {
                  binding: [['Ctrl', 's'], ['Meta', 's']],
                  text: 'Save comment',
                },
              ],
              [ShortcutSection.EVERYWHERE]: [
                {binding: [['/']], text: 'Search'},
                {binding: [['g', 'o']], text: 'Go to Opened Changes'},
              ],
              [ShortcutSection.NAVIGATION]: [
                {binding: [[']']], text: 'Go to next file'},
              ],
            });
      });
    });
  });

  test('doesn’t block kb shortcuts for non-allowed els', done => {
    const divEl = document.createElement('div');
    element.appendChild(divEl);
    element._handleKey = e => {
      assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(divEl, 75, null, 'k');
  });

  test('blocks kb shortcuts for input els', done => {
    const inputEl = document.createElement('input');
    element.appendChild(inputEl);
    element._handleKey = e => {
      assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(inputEl, 75, null, 'k');
  });

  test('doesn’t block kb shortcuts for checkboxes', done => {
    const inputEl = document.createElement('input');
    inputEl.setAttribute('type', 'checkbox');
    element.appendChild(inputEl);
    element._handleKey = e => {
      assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(inputEl, 75, null, 'k');
  });

  test('blocks kb shortcuts for textarea els', done => {
    const textareaEl = document.createElement('textarea');
    element.appendChild(textareaEl);
    element._handleKey = e => {
      assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
  });

  test('blocks kb shortcuts for anything in a gr-overlay', done => {
    const divEl = document.createElement('div');
    const element =
        overlay.querySelector('keyboard-shortcut-mixin-test-element');
    element.appendChild(divEl);
    element._handleKey = e => {
      assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(divEl, 75, null, 'k');
  });

  test('blocks enter shortcut on an anchor', done => {
    const anchorEl = document.createElement('a');
    const element =
        overlay.querySelector('keyboard-shortcut-mixin-test-element');
    element.appendChild(anchorEl);
    element._handleKey = e => {
      assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
      done();
    };
    MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
  });

  test('modifierPressed returns accurate values', () => {
    const spy = sinon.spy(element, 'modifierPressed');
    element._handleKey = e => {
      element.modifierPressed(e);
    };
    MockInteractions.keyDownOn(element, 75, 'shift', 'k');
    assert.isTrue(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
    assert.isTrue(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'meta', 'k');
    assert.isTrue(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'alt', 'k');
    assert.isTrue(spy.lastCall.returnValue);
  });

  test('isModifierPressed returns accurate value', () => {
    const spy = sinon.spy(element, 'isModifierPressed');
    element._handleKey = e => {
      element.isModifierPressed(e, 'shiftKey');
    };
    MockInteractions.keyDownOn(element, 75, 'shift', 'k');
    assert.isTrue(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'meta', 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, null, 'k');
    assert.isFalse(spy.lastCall.returnValue);
    MockInteractions.keyDownOn(element, 75, 'alt', 'k');
    assert.isFalse(spy.lastCall.returnValue);
  });

  suite('GO_KEY timing', () => {
    let handlerStub;

    setup(() => {
      element._shortcut_go_table.set('a', '_handleA');
      handlerStub = element._handleA = sinon.stub();
      sinon.stub(Date, 'now').returns(10000);
    });

    test('success', () => {
      const e = {detail: {key: 'a'}, preventDefault: () => {}};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      element._shortcut_go_key_last_pressed = 9000;
      element._handleGoAction(e);
      assert.isTrue(handlerStub.calledOnce);
      assert.strictEqual(handlerStub.lastCall.args[0], e);
    });

    test('go key not pressed', () => {
      const e = {detail: {key: 'a'}, preventDefault: () => {}};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      element._shortcut_go_key_last_pressed = null;
      element._handleGoAction(e);
      assert.isFalse(handlerStub.called);
    });

    test('go key pressed too long ago', () => {
      const e = {detail: {key: 'a'}, preventDefault: () => {}};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      element._shortcut_go_key_last_pressed = 3000;
      element._handleGoAction(e);
      assert.isFalse(handlerStub.called);
    });

    test('should suppress', () => {
      const e = {detail: {key: 'a'}, preventDefault: () => {}};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
      element._shortcut_go_key_last_pressed = 9000;
      element._handleGoAction(e);
      assert.isFalse(handlerStub.called);
    });

    test('unrecognized key', () => {
      const e = {detail: {key: 'f'}, preventDefault: () => {}};
      sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
      element._shortcut_go_key_last_pressed = 9000;
      element._handleGoAction(e);
      assert.isFalse(handlerStub.called);
    });
  });
});

