// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.ui.ac.InputHandlerTest');
goog.setTestOnly('goog.ui.ac.InputHandlerTest');

goog.require('goog.dom.selection');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.KeyCodes');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.userAgent');



/**
 * Mock out the input element.
 * @constructor
 */
function MockElement() {
  goog.events.EventTarget.call(this);
  this.setAttributeNS = function() {};
  this.setAttribute = function(key, value) { this[key] = value; };
  this.focus = function() {};
  this.blur = function() {};
  this.ownerDocument = document;
  this.selectionStart = 0;
}
goog.inherits(MockElement, goog.events.EventTarget);



/**
 * @constructor
 */
function MockAutoCompleter() {
  this.setToken = null;
  this.setTokenWasCalled = false;
  this.selectHilitedWasCalled = false;
  this.dismissWasCalled = false;
  this.getTarget = function() { return mockElement };
  this.setTarget = function() { };
  this.setToken = function(token) {
    this.setTokenWasCalled = true;
    this.setToken = token;
  };
  this.selectHilited = function() {
    this.selectHilitedWasCalled = true;
    return true; // Success.
  };
  this.cancelDelayedDismiss = function() { };
  this.dismissOnDelay = function() {};
  this.dismiss = function() { this.dismissWasCalled = true; };
  this.isOpen = goog.functions.TRUE;
}



/**
 * MockInputHandler simulates key events for testing the IME behavior of
 * InputHandler.
 * @constructor
 */
function MockInputHandler() {
  goog.ui.ac.InputHandler.call(this);

  this.ac_ = new MockAutoCompleter();
  this.cursorPosition_ = 0;

  this.attachInput(mockElement);
}
goog.inherits(MockInputHandler, goog.ui.ac.InputHandler);


/** Checks for updates to the text area, should not happen during IME. */
MockInputHandler.prototype.update = function() {
  this.updates++;
};


/** Simulates key events. */
MockInputHandler.prototype.fireKeyEvents = function(
    keyCode, down, press, up, opt_properties) {
  if (down) this.fireEvent('keydown', keyCode, opt_properties);
  if (press) this.fireEvent('keypress', keyCode, opt_properties);
  if (up) this.fireEvent('keyup', keyCode, opt_properties);
};


/** Simulates an event. */
MockInputHandler.prototype.fireEvent = function(
    type, keyCode, opt_properties) {
  var e = {};
  e.type = type;
  e.keyCode = keyCode;
  e.preventDefault = function() {};
  if (!goog.userAgent.IE) {
    e.which = type == 'keydown' ? keyCode : 0;
  }
  if (opt_properties) {
    goog.object.extend(e, opt_properties);
  }
  e = new goog.events.BrowserEvent(e);
  mockElement.dispatchEvent(e);
};

MockInputHandler.prototype.setCursorPosition = function(cursorPosition) {
  this.cursorPosition_ = cursorPosition;
};

MockInputHandler.prototype.getCursorPosition = function() {
  return this.cursorPosition_;
};

// Variables used by all test
var mh = null;
var oldMac, oldWin, oldLinux, oldIe, oldFf, oldWebkit, oldVersion;
var oldUsesKeyDown;
var mockElement;
var mockClock;

function setUp() {
  oldMac = goog.userAgent.MAC;
  oldWin = goog.userAgent.WINDOWS;
  oldLinux = goog.userAgent.LINUX;
  oldIe = goog.userAgent.IE;
  oldFf = goog.userAgent.GECKO;
  oldWebkit = goog.userAgent.WEBKIT;
  oldVersion = goog.userAgent.VERSION;
  oldUsesKeyDown = goog.events.KeyHandler.USES_KEYDOWN_;
  mockClock = new goog.testing.MockClock(true);
  mockElement = new MockElement;
  mh = new MockInputHandler;
}

function tearDown() {
  goog.userAgent.MAC = oldMac;
  goog.userAgent.WINDOWS = oldWin;
  goog.userAgent.LINUX = oldLinux;
  goog.userAgent.IE = oldIe;
  goog.userAgent.GECKO = oldFf;
  goog.userAgent.WEBKIT = oldWebkit;
  goog.userAgent.VERSION = oldVersion;
  goog.events.KeyHandler.USES_KEYDOWN_ = oldUsesKeyDown;
  mockClock.dispose();
  mockElement.dispose();
}


/** Used to simulate behavior of Windows/Firefox3 */
function simulateWinFirefox3() {
  goog.userAgent.MAC = false;
  goog.userAgent.WINDOWS = true;
  goog.userAgent.LINUX = false;
  goog.userAgent.IE = false;
  goog.userAgent.GECKO = true;
  goog.userAgent.WEBKIT = false;
  goog.events.KeyHandler.USES_KEYDOWN_ = false;
}


/** Used to simulate behavior of Windows/InternetExplorer7 */
function simulateWinIe7() {
  goog.userAgent.MAC = false;
  goog.userAgent.WINDOWS = true;
  goog.userAgent.LINUX = false;
  goog.userAgent.IE = true;
  goog.userAgent.DOCUMENT_MODE = 7;
  goog.userAgent.GECKO = false;
  goog.userAgent.WEBKIT = false;
  goog.events.KeyHandler.USES_KEYDOWN_ = true;
}


/** Used to simulate behavior of Windows/Chrome */
function simulateWinChrome() {
  goog.userAgent.MAC = false;
  goog.userAgent.WINDOWS = true;
  goog.userAgent.LINUX = false;
  goog.userAgent.IE = false;
  goog.userAgent.GECKO = false;
  goog.userAgent.WEBKIT = true;
  goog.userAgent.VERSION = '525';
  goog.events.KeyHandler.USES_KEYDOWN_ = true;
}


/** Used to simulate behavior of Mac/Firefox3 */
function simulateMacFirefox3() {
  goog.userAgent.MAC = true;
  goog.userAgent.WINDOWS = false;
  goog.userAgent.LINUX = false;
  goog.userAgent.IE = false;
  goog.userAgent.GECKO = true;
  goog.userAgent.WEBKIT = false;
  goog.events.KeyHandler.USES_KEYDOWN_ = true;
}


/** Used to simulate behavior of Mac/Safari3 */
function simulateMacSafari3() {
  goog.userAgent.MAC = true;
  goog.userAgent.WINDOWS = false;
  goog.userAgent.LINUX = false;
  goog.userAgent.IE = false;
  goog.userAgent.GECKO = false;
  goog.userAgent.WEBKIT = true;
  goog.userAgent.VERSION = '525';
  goog.events.KeyHandler.USES_KEYDOWN_ = true;
}


/** Used to simulate behavior of Linux/Firefox3 */
function simulateLinuxFirefox3() {
  goog.userAgent.MAC = false;
  goog.userAgent.WINDOWS = false;
  goog.userAgent.LINUX = true;
  goog.userAgent.IE = false;
  goog.userAgent.GECKO = true;
  goog.userAgent.WEBKIT = false;
  goog.events.KeyHandler.USES_KEYDOWN_ = true;
}


/** Test the normal, non-IME case */
function testRegularKey() {
  // Each key fires down, press, and up in that order, and each should
  // trigger an autocomplete update
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireKeyEvents(goog.events.KeyCodes.K, true, true, true);
  assertFalse('IME should not be triggered by K', mh.waitingForIme_);

  mh.fireKeyEvents(goog.events.KeyCodes.A, true, true, true);
  assertFalse('IME should not be triggered by A', mh.waitingForIme_);
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Windows/Firefox3.
 */
function testImeWinFirefox3() {
  simulateWinFirefox3();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
  // Event is not generated for key code a.
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
  // Event is not generated for key code i.
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Windows/InternetExplorer7.
 */
function testImeWinIe7() {
  simulateWinIe7();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Windows/Chrome.
 */
function testImeWinChrome() {
  simulateWinChrome();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Mac/Firefox3.
 */
function testImeMacFirefox3() {
  // TODO(user): Currently our code cannot distinguish preedit characters
  // from normal ones for Mac/Firefox3.
  // Enable this test after we fix it.

  simulateMacFirefox3();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
  assertTrue('IME should be triggered', mh.waitingForIme_);
  mh.fireKeyEvents(goog.events.KeyCodes.A, true, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
  mh.fireKeyEvents(goog.events.KeyCodes.I, true, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Mac/Safari3.
 */
function testImeMacSafari3() {
  simulateMacSafari3();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
  mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * This test simulates the key inputs generated by pressing
 * '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
 * on Linux/Firefox3.
 */
function testImeLinuxFirefox3() {
  // TODO(user): Currently our code cannot distinguish preedit characters
  // from normal ones for Linux/Firefox3.
  // Enable this test after we fix it.


  simulateLinuxFirefox3();
  mh.fireEvent('focus', '');
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // ime_on
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);

  // a
  mh.fireKeyEvents(goog.events.KeyCodes.A, true, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // enter
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  // i
  mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
  mh.fireKeyEvents(goog.events.KeyCodes.I, true, false, true);
  assertTrue('IME should be triggered', mh.waitingForIme_);

  // ime_off

  // u
  mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
  assertFalse('IME should not be triggered', mh.waitingForIme_);

  mh.fireEvent('blur', '');
}


/**
 * Check attaching to an EventTarget instead of an element.
 */
function testAttachEventTarget() {
  var target = new goog.events.EventTarget();

  assertNull(mh.activeElement_);
  mh.attachInput(target);
  assertNull(mh.activeElement_);

  mockElement.dispatchEvent(new goog.events.Event('focus', mockElement));
  assertEquals(mockElement, mh.activeElement_);

  mh.detachInput(target);
}


/**
 * Make sure that the active element handling works.
 */
function testActiveElement() {
  assertNull(mh.activeElement_);

  mockElement.dispatchEvent('keydown');
  assertEquals(mockElement, mh.activeElement_);

  mockElement.dispatchEvent('blur');
  assertNull(mh.activeElement_);

  mockElement.dispatchEvent('focus');
  assertEquals(mockElement, mh.activeElement_);

  mh.detachInput(mockElement);
  assertNull(mh.activeElement_);
}


/**
 * We can attach an EventTarget that isn't an element.
 */
function testAttachEventTarget() {
  var target = new goog.events.EventTarget();

  assertNull(mh.activeElement_);
  mh.attachInput(target);
  assertNull(mh.activeElement_);

  target.dispatchEvent(new goog.events.Event('focus', mockElement));
  assertEquals(mockElement, mh.activeElement_);

  mh.detachInput(target);
}


/**
 * Make sure an already-focused element becomes active immediately.
 */
function testActiveElementAlreadyFocused() {
  var element = document.getElementById('textInput');
  element.style.display = '';
  element.focus();

  assertNull(mh.activeElement_);

  mh.attachInput(element);
  assertEquals(element, mh.activeElement_);

  mh.detachInput(element);
  element.style.display = 'none';
}

function testUpdateDoesNotTriggerSetTokenForSelectRow() {

  var ih = new goog.ui.ac.InputHandler();

  // Set up our input handler with the necessary mocks
  var mockAutoCompleter = new MockAutoCompleter();
  ih.ac_ = mockAutoCompleter;
  ih.activeElement_ = mockElement;

  var row = {};
  ih.selectRow(row, false);

  ih.update();
  assertFalse('update should not call setToken on selectRow',
              mockAutoCompleter.setTokenWasCalled);

  ih.update();
  assertFalse('update should not call setToken on selectRow',
              mockAutoCompleter.setTokenWasCalled);
}

function testSetTokenText() {

  var ih = new MockInputHandler();

  // Set up our input handler with the necessary mocks
  var mockAutoCompleter = new MockAutoCompleter();
  ih.ac_ = mockAutoCompleter;
  ih.activeElement_ = mockElement;
  mockElement.value = 'bob, wal, joey';
  ih.setCursorPosition(8);

  ih.setTokenText('waldo', true /* multi-row */);

  assertEquals('bob, waldo, joey', mockElement.value);
}

function testSetTokenTextLeftHandSideOfToken() {

  var ih = new MockInputHandler();
  ih.setSeparators(' ');
  ih.setWhitespaceWrapEntries(false);

  // Set up our input handler with the necessary mocks
  var mockAutoCompleter = new MockAutoCompleter();
  ih.ac_ = mockAutoCompleter;
  ih.activeElement_ = mockElement;
  mockElement.value = 'foo bar';
  // Sets cursor position right before 'bar'
  ih.setCursorPosition(4);

  ih.setTokenText('bar', true /* multi-row */);

  assertEquals('foo bar ', mockElement.value);
}

function testEmptyTokenWithSeparator() {
  var ih = new goog.ui.ac.InputHandler();
  var mockAutoCompleter = new MockAutoCompleter();
  ih.ac_ = mockAutoCompleter;
  ih.activeElement_ = mockElement;
  mockElement.value = ', ,';
  // Sets cursor position before the second comma
  goog.dom.selection.setStart(mockElement, 2);

  ih.update();
  assertTrue('update should call setToken on selectRow',
      mockAutoCompleter.setTokenWasCalled);
  assertEquals('update should be called with empty string',
      '', mockAutoCompleter.setToken);
}

function testNonEmptyTokenWithSeparator() {
  var ih = new goog.ui.ac.InputHandler();
  var mockAutoCompleter = new MockAutoCompleter();
  ih.ac_ = mockAutoCompleter;
  ih.activeElement_ = mockElement;
  mockElement.value = ', joe ,';
  // Sets cursor position before the second comma
  goog.dom.selection.setStart(mockElement, 5);

  ih.update();
  assertTrue('update should call setToken on selectRow',
      mockAutoCompleter.setTokenWasCalled);
  assertEquals('update should be called with expected string',
      'joe', mockAutoCompleter.setToken);
}

function testGetThrottleTime() {
  var ih = new goog.ui.ac.InputHandler();
  ih.setThrottleTime(999);
  assertEquals('throttle time set+get', 999, ih.getThrottleTime());
}

function testGetUpdateDuringTyping() {
  var ih = new goog.ui.ac.InputHandler();
  ih.setUpdateDuringTyping(false);
  assertFalse('update during typing set+get', ih.getUpdateDuringTyping());
}

function testEnterToSelect() {
  mh.fireEvent('focus', '');
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
  assertTrue('Should hilite', mh.ac_.selectHilitedWasCalled);
  assertFalse('Should NOT be dismissed', mh.ac_.dismissWasCalled);
}

function testEnterDoesNotSelectWhenClosed() {
  mh.fireEvent('focus', '');
  mh.ac_.isOpen = goog.functions.FALSE;
  mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
  assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
  assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
}

function testTabToSelect() {
  mh.fireEvent('focus', '');
  mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true);
  assertTrue('Should hilite', mh.ac_.selectHilitedWasCalled);
  assertFalse('Should NOT be dismissed', mh.ac_.dismissWasCalled);
}

function testTabDoesNotSelectWhenClosed() {
  mh.fireEvent('focus', '');
  mh.ac_.isOpen = goog.functions.FALSE;
  mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true);
  assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
  assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
}

function testShiftTabDoesNotSelect() {
  mh.fireEvent('focus', '');
  mh.ac_.isOpen = goog.functions.TRUE;
  mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true,
      {shiftKey: true});
  assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
  assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
}

function testEmptySeparatorUsesDefaults() {
  var inputHandler = new goog.ui.ac.InputHandler('');
  assertFalse(inputHandler.separatorCheck_.test(''));
  assertFalse(inputHandler.separatorCheck_.test('x'));
  assertTrue(inputHandler.separatorCheck_.test(','));
}

function testMultipleSeparatorUsesEmptyDefaults() {
  var inputHandler = new goog.ui.ac.InputHandler(',\n', null, true);
  inputHandler.setWhitespaceWrapEntries(false);
  inputHandler.setSeparators(',\n', '');

  // Set up our input handler with the necessary mocks
  var mockAutoCompleter = new MockAutoCompleter();
  inputHandler.ac_ = mockAutoCompleter;
  inputHandler.activeElement_ = mockElement;
  mockElement.value = 'bob,wal';
  inputHandler.setCursorPosition(8);

  inputHandler.setTokenText('waldo', true /* multi-row */);

  assertEquals('bob,waldo', mockElement.value);
}
