// Copyright 2008 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.editor.plugins.LinkBubbleTest');
goog.setTestOnly('goog.editor.plugins.LinkBubbleTest');

goog.require('goog.dom');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.Command');
goog.require('goog.editor.Link');
goog.require('goog.editor.plugins.LinkBubble');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.testing.FunctionMock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.editor.FieldMock');
goog.require('goog.testing.editor.TestHelper');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');

var fieldDiv;
var FIELDMOCK;
var linkBubble;
var link;
var linkChild;
var mockWindowOpen;
var stubs;
var testHelper;

function setUpPage() {
  fieldDiv = goog.dom.$('field');
  stubs = new goog.testing.PropertyReplacer();
  testHelper = new goog.testing.editor.TestHelper(goog.dom.getElement('field'));
}

function setUp() {
  testHelper.setUpEditableElement();
  FIELDMOCK = new goog.testing.editor.FieldMock();

  linkBubble = new goog.editor.plugins.LinkBubble();
  linkBubble.fieldObject = FIELDMOCK;

  link = fieldDiv.firstChild;
  linkChild = link.lastChild;

  mockWindowOpen = new goog.testing.FunctionMock('open');
  stubs.set(window, 'open', mockWindowOpen);
}

function tearDown() {
  linkBubble.closeBubble();
  testHelper.tearDownEditableElement();
  stubs.reset();
}

function testLinkSelected() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);
  goog.dom.Range.createFromNodeContents(link).select();
  linkBubble.handleSelectionChange();
  assertBubble();
  FIELDMOCK.$verify();
}

function testLinkClicked() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);
  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();
  FIELDMOCK.$verify();
}

function testImageLink() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);
  link.setAttribute('imageanchor', 1);
  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();
  FIELDMOCK.$verify();
}

function closeBox() {
  var closeBox = goog.dom.getElementsByTagNameAndClass(
      goog.dom.TagName.DIV, 'tr_bubble_closebox');
  assertEquals('Should find only one close box', 1, closeBox.length);
  assertNotNull('Found close box', closeBox[0]);
  goog.testing.events.fireClickSequence(closeBox[0]);
}

function testCloseBox() {
  testLinkClicked();
  closeBox();
  assertNoBubble();
  FIELDMOCK.$verify();
}

function testChangeClicked() {
  FIELDMOCK.execCommand(
      goog.editor.Command.MODAL_LINK_EDITOR, new goog.editor.Link(link, false));
  FIELDMOCK.$registerArgumentListVerifier('execCommand', function(arr1, arr2) {
    return arr1.length == arr2.length && arr1.length == 2 &&
        arr1[0] == goog.editor.Command.MODAL_LINK_EDITOR &&
        arr2[0] == goog.editor.Command.MODAL_LINK_EDITOR &&
        arr1[1] instanceof goog.editor.Link &&
        arr2[1] instanceof goog.editor.Link;
  });
  FIELDMOCK.$times(1);
  FIELDMOCK.$returns(true);
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  goog.testing.events.fireClickSequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_));
  assertNoBubble();
  FIELDMOCK.$verify();
}

function testChangePressed() {
  FIELDMOCK.execCommand(
      goog.editor.Command.MODAL_LINK_EDITOR, new goog.editor.Link(link, false));
  FIELDMOCK.$registerArgumentListVerifier('execCommand', function(arr1, arr2) {
    return arr1.length == arr2.length && arr1.length == 2 &&
        arr1[0] == goog.editor.Command.MODAL_LINK_EDITOR &&
        arr2[0] == goog.editor.Command.MODAL_LINK_EDITOR &&
        arr1[1] instanceof goog.editor.Link &&
        arr2[1] instanceof goog.editor.Link;
  });
  FIELDMOCK.$times(1);
  FIELDMOCK.$returns(true);
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  var defaultPrevented = !goog.testing.events.fireKeySequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_),
      goog.events.KeyCodes.ENTER);
  assertTrue(defaultPrevented);
  assertNoBubble();
  FIELDMOCK.$verify();
}

function testDeleteClicked() {
  FIELDMOCK.dispatchBeforeChange();
  FIELDMOCK.$times(1);
  FIELDMOCK.dispatchChange();
  FIELDMOCK.$times(1);
  FIELDMOCK.focus();
  FIELDMOCK.$times(1);
  FIELDMOCK.$replay();

  linkBubble.enable(FIELDMOCK);

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  goog.testing.events.fireClickSequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.DELETE_LINK_ID_));
  var element = goog.userAgent.GECKO ? document.body : fieldDiv;
  assertNotEquals(
      'Link removed', element.firstChild.nodeName, String(goog.dom.TagName.A));
  assertNoBubble();
  var range = goog.dom.Range.createFromWindow();
  assertEquals('Link selection on link text', linkChild, range.getEndNode());
  assertEquals(
      'Link selection on link text end',
      goog.dom.getRawTextContent(linkChild).length, range.getEndOffset());
  FIELDMOCK.$verify();
}

function testDeletePressed() {
  FIELDMOCK.dispatchBeforeChange();
  FIELDMOCK.$times(1);
  FIELDMOCK.dispatchChange();
  FIELDMOCK.$times(1);
  FIELDMOCK.focus();
  FIELDMOCK.$times(1);
  FIELDMOCK.$replay();

  linkBubble.enable(FIELDMOCK);

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  var defaultPrevented = !goog.testing.events.fireKeySequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.DELETE_LINK_ID_),
      goog.events.KeyCodes.ENTER);
  assertTrue(defaultPrevented);
  var element = goog.userAgent.GECKO ? document.body : fieldDiv;
  assertNotEquals(
      'Link removed', element.firstChild.nodeName, String(goog.dom.TagName.A));
  assertNoBubble();
  var range = goog.dom.Range.createFromWindow();
  assertEquals('Link selection on link text', linkChild, range.getEndNode());
  assertEquals(
      'Link selection on link text end',
      goog.dom.getRawTextContent(linkChild).length, range.getEndOffset());
  FIELDMOCK.$verify();
}

function testActionClicked() {
  var SPAN = 'actionSpanId';
  var LINK = 'actionLinkId';
  var toShowCount = 0;
  var actionCount = 0;

  var linkAction = new goog.editor.plugins.LinkBubble.Action(
      SPAN, LINK, 'message',
      function() {
        toShowCount++;
        return toShowCount == 1;  // Show it the first time.
      },
      function() { actionCount++; });

  linkBubble = new goog.editor.plugins.LinkBubble(linkAction);
  linkBubble.fieldObject = FIELDMOCK;
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  // The first time the bubble is shown, show our custom action.
  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();
  assertEquals('Should check showing the action', 1, toShowCount);
  assertEquals('Action should not have fired yet', 0, actionCount);

  assertTrue(
      'Action should be visible 1st time',
      goog.style.isElementShown(goog.dom.$(SPAN)));
  goog.testing.events.fireClickSequence(goog.dom.$(LINK));

  assertEquals('Should not check showing again yet', 1, toShowCount);
  assertEquals('Action should be fired', 1, actionCount);

  closeBox();
  assertNoBubble();

  // The action won't be shown the second time around.
  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();
  assertEquals('Should check showing again', 2, toShowCount);
  assertEquals('Action should not fire again', 1, actionCount);
  assertFalse(
      'Action should not be shown 2nd time',
      goog.style.isElementShown(goog.dom.$(SPAN)));

  FIELDMOCK.$verify();
}

function testLinkTextClicked() {
  mockWindowOpen('http://www.google.com/', '_blank', '');
  mockWindowOpen.$replay();
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  goog.testing.events.fireClickSequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.TEST_LINK_ID_));

  assertBubble();
  mockWindowOpen.$verify();
  FIELDMOCK.$verify();
}

function testLinkTextClickedCustomUrlFn() {
  mockWindowOpen('http://images.google.com/', '_blank', '');
  mockWindowOpen.$replay();
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  linkBubble.setTestLinkUrlFn(function(url) {
    return url.replace('www', 'images');
  });

  linkBubble.handleSelectionChange(createMouseEvent(link));
  assertBubble();

  goog.testing.events.fireClickSequence(
      goog.dom.$(goog.editor.plugins.LinkBubble.TEST_LINK_ID_));

  assertBubble();
  mockWindowOpen.$verify();
  FIELDMOCK.$verify();
}


/**
 * Urls with invalid schemes shouldn't be linkified.
 * @bug 2585360
 */
function testDontLinkifyInvalidScheme() {
  mockWindowOpen.$replay();
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  var badLink = goog.dom.createElement(goog.dom.TagName.A);
  badLink.href = 'javascript:alert(1)';
  goog.dom.setTextContent(badLink, 'bad link');

  linkBubble.handleSelectionChange(createMouseEvent(badLink));
  assertBubble();

  // The link shouldn't exist at all
  assertNull(goog.dom.$(goog.editor.plugins.LinkBubble.TEST_LINK_ID_));

  assertBubble();
  mockWindowOpen.$verify();
  FIELDMOCK.$verify();
}

function testIsSafeSchemeToOpen() {
  // Urls with no scheme at all are ok too since 'http://' will be prepended.
  var good = [
    'http://google.com', 'http://google.com/', 'https://google.com',
    'null@google.com', 'http://www.google.com', 'http://site.com', 'google.com',
    'google', 'http://google', 'HTTP://GOOGLE.COM', 'HtTp://www.google.com'
  ];

  var bad = [
    'javascript:google.com', 'httpp://google.com', 'data:foo',
    'javascript:alert(\'hi\');', 'abc:def'
  ];

  for (var i = 0; i < good.length; i++) {
    assertTrue(
        good[i] + ' should have a safe scheme',
        linkBubble.isSafeSchemeToOpen_(good[i]));
  }

  for (i = 0; i < bad.length; i++) {
    assertFalse(
        bad[i] + ' should have an unsafe scheme',
        linkBubble.isSafeSchemeToOpen_(bad[i]));
  }
}

function testShouldOpenWithWhitelist() {
  linkBubble.setSafeToOpenSchemes(['abc']);

  assertTrue(
      'Scheme should be safe', linkBubble.shouldOpenUrl('abc://google.com'));
  assertFalse(
      'Scheme should be unsafe', linkBubble.shouldOpenUrl('http://google.com'));

  linkBubble.setBlockOpeningUnsafeSchemes(false);
  assertTrue(
      'Non-whitelisted should now be safe after disabling blocking',
      linkBubble.shouldOpenUrl('http://google.com'));
}


/**
 * @bug 763211
 * @bug 2182147
 */
function testLongUrlTestLinkAnchorTextCorrect() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  var longUrl = 'http://www.reallylonglinkthatshouldbetruncated' +
      'becauseitistoolong.com';
  var truncatedLongUrl = goog.string.truncateMiddle(longUrl, 48);

  var longLink = goog.dom.createElement(goog.dom.TagName.A);
  longLink.href = longUrl;
  goog.dom.setTextContent(longLink, 'Google');
  fieldDiv.appendChild(longLink);

  linkBubble.handleSelectionChange(createMouseEvent(longLink));
  assertBubble();

  var testLinkEl = goog.dom.$(goog.editor.plugins.LinkBubble.TEST_LINK_ID_);
  assertEquals(
      'The test link\'s anchor text should be the truncated URL.',
      truncatedLongUrl, testLinkEl.innerHTML);

  fieldDiv.removeChild(longLink);
  FIELDMOCK.$verify();
}


/**
 * @bug 2416024
 */
function testOverridingCreateBubbleContentsDoesntNpeGetTargetUrl() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  stubs.set(linkBubble, 'createBubbleContents', function(elem) {
    // getTargetUrl would cause an NPE if urlUtil_ wasn't defined yet.
    linkBubble.getTargetUrl();
  });
  assertNotThrows(
      'Accessing this.urlUtil_ should not NPE',
      goog.bind(
          linkBubble.handleSelectionChange, linkBubble,
          createMouseEvent(link)));

  FIELDMOCK.$verify();
}


/**
 * @bug 15379294
 */
function testUpdateLinkCommandDoesNotTriggerAnException() {
  FIELDMOCK.$replay();
  linkBubble.enable(FIELDMOCK);

  // At this point, the bubble was not created yet using its createBubble
  // public method.
  assertNotThrows(
      'Executing goog.editor.Command.UPDATE_LINK_BUBBLE should not trigger ' +
          'an exception even if the bubble was not created yet using its ' +
          'createBubble method.',
      goog.bind(
          linkBubble.execCommandInternal, linkBubble,
          goog.editor.Command.UPDATE_LINK_BUBBLE));

  FIELDMOCK.$verify();
}

function assertBubble() {
  assertTrue('Link bubble visible', linkBubble.isVisible());
  assertNotNull(
      'Link bubble created',
      goog.dom.$(goog.editor.plugins.LinkBubble.LINK_DIV_ID_));
}

function assertNoBubble() {
  assertFalse('Link bubble not visible', linkBubble.isVisible());
  assertNull(
      'Link bubble not created',
      goog.dom.$(goog.editor.plugins.LinkBubble.LINK_DIV_ID_));
}

function createMouseEvent(target) {
  var eventObj = new goog.events.Event(goog.events.EventType.MOUSEUP, target);
  eventObj.button = goog.events.BrowserEvent.MouseButton.LEFT;

  return new goog.events.BrowserEvent(eventObj, target);
}
