/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.fx.DraggerTest');
goog.setTestOnly();

const BrowserEvent = goog.require('goog.events.BrowserEvent');
const Dragger = goog.require('goog.fx.Dragger');
const EventType = goog.require('goog.events.EventType');
const GoogEvent = goog.require('goog.events.Event');
const GoogRect = goog.require('goog.math.Rect');
const StrictMock = goog.require('goog.testing.StrictMock');
const TagName = goog.require('goog.dom.TagName');
const bidi = goog.require('goog.style.bidi');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
const testSuite = goog.require('goog.testing.testSuite');
const testingEvents = goog.require('goog.testing.events');

/** @suppress {visibility} suppression added to enable type checking */
const HAS_SET_CAPTURE = Dragger.HAS_SET_CAPTURE_;

let target;
let targetRtl;

/**
 * @suppress {missingProperties,checkTypes} suppression added to enable type
 * checking
 */
function runStartDragTest(handleId, targetElement) {
  let dragger = new Dragger(targetElement, dom.getElement(handleId));
  if (handleId == 'handle_rtl') {
    dragger.enableRightPositioningForRtl(true);
  }
  const e = new StrictMock(BrowserEvent);
  /**
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  e.type = EventType.MOUSEDOWN;
  /**
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  e.clientX = 1;
  /**
   * @suppress {strictMissingProperties} suppression added to enable type
   * checking
   */
  e.clientY = 2;
  e.isMouseActionButton().$returns(true);
  e.preventDefault();
  e.isMouseActionButton().$returns(true);
  e.preventDefault();
  e.$replay();

  events.listen(dragger, Dragger.EventType.START, () => {
    targetElement.style.display = 'block';
  });

  dragger.startDrag(e);

  assertTrue(
      'Start drag with no hysteresis must actually start the drag.',
      dragger.isDragging());
  if (handleId == 'handle_rtl') {
    assertEquals(10, bidi.getOffsetStart(targetElement));
  }
  assertEquals(
      'Dragger startX must match event\'s clientX.', 1, dragger.startX);
  assertEquals(
      'Dragger clientX must match event\'s clientX', 1, dragger.clientX);
  assertEquals(
      'Dragger startY must match event\'s clientY.', 2, dragger.startY);
  assertEquals(
      'Dragger clientY must match event\'s clientY', 2, dragger.clientY);
  assertEquals(
      'Dragger deltaX must match target\'s offsetLeft', 10, dragger.deltaX);
  assertEquals(
      'Dragger deltaY must match target\'s offsetTop', 15, dragger.deltaY);

  dragger = new Dragger(targetElement, dom.getElement(handleId));
  dragger.setHysteresis(1);
  dragger.startDrag(e);
  assertFalse(
      'Start drag with a valid non-zero hysteresis should not start ' +
          'the drag.',
      dragger.isDragging());
  e.$verify();
}

testSuite({
  setUp() {
    const sandbox = dom.getElement('sandbox');
    target = dom.createDom(TagName.DIV, {
      'id': 'target',
      'style': 'display:none;position:absolute;top:15px;left:10px',
    });
    sandbox.appendChild(target);
    sandbox.appendChild(dom.createDom(TagName.DIV, {id: 'handle'}));

    const sandboxRtl = dom.getElement('sandbox_rtl');
    targetRtl = dom.createDom(TagName.DIV, {
      'id': 'target_rtl',
      'style': 'position:absolute; top:15px; right:10px; width:10px; ' +
          'height: 10px; background: green;',
    });
    sandboxRtl.appendChild(targetRtl);
    sandboxRtl.appendChild(dom.createDom(TagName.DIV, {
      'id': 'background_rtl',
      'style': 'width: 10000px;height:50px;position:absolute;color:blue;',
    }));
    sandboxRtl.appendChild(dom.createDom(TagName.DIV, {id: 'handle_rtl'}));
  },

  tearDown() {
    dom.removeChildren(dom.getElement('sandbox'));
    dom.removeChildren(dom.getElement('sandbox_rtl'));
    events.removeAll(document);
  },

  testStartDrag() {
    runStartDragTest('handle', target);
  },

  testStartDrag_rtl() {
    runStartDragTest('handle_rtl', targetRtl);
  },

  /**
     @bug 1381317 Cancelling start drag didn't end the attempt to drag.
     @suppress {missingProperties,checkTypes,visibility} suppression added to
     enable type checking
   */
  testStartDrag_Cancel() {
    const dragger = new Dragger(target);

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEDOWN;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(true);
    e.$replay();

    events.listen(dragger, Dragger.EventType.START, (e) => {
      // Cancel drag.
      e.preventDefault();
    });

    dragger.startDrag(e);

    assertFalse('Start drag must have been cancelled.', dragger.isDragging());
    assertFalse(
        'Dragger must not have registered mousemove handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEMOVE, !HAS_SET_CAPTURE));
    assertFalse(
        'Dragger must not have registered mouseup handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEUP, !HAS_SET_CAPTURE));
    e.$verify();
  },

  /**
     Tests that start drag happens on left mousedown.
     @suppress {missingProperties,checkTypes,visibility} suppression added to
     enable type checking
   */
  testStartDrag_LeftMouseDownOnly() {
    const dragger = new Dragger(target);

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEDOWN;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(false);
    e.$replay();

    events.listen(dragger, Dragger.EventType.START, (e) => {
      fail('No drag START event should have been dispatched');
    });

    dragger.startDrag(e);

    assertFalse('Start drag must have been cancelled.', dragger.isDragging());
    assertFalse(
        'Dragger must not have registered mousemove handlers.',
        events.hasListener(dragger.document_, EventType.MOUSEMOVE, true));
    assertFalse(
        'Dragger must not have registered mouseup handlers.',
        events.hasListener(dragger.document_, EventType.MOUSEUP, true));
    e.$verify();
  },

  /**
     Tests that start drag happens on other event type than MOUSEDOWN.
     @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testStartDrag_MouseMove() {
    const dragger = new Dragger(target);

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEMOVE;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    // preventDefault is not called.
    e.$replay();

    let startDragFired = false;
    events.listen(dragger, Dragger.EventType.START, (e) => {
      startDragFired = true;
    });

    dragger.startDrag(e);

    assertTrue('Dragging should be in progress.', dragger.isDragging());
    assertTrue('Start drag event should have fired.', startDragFired);
    assertTrue(
        'Dragger must have registered mousemove handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEMOVE, !HAS_SET_CAPTURE));
    assertTrue(
        'Dragger must have registered mouseup handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEUP, !HAS_SET_CAPTURE));
    e.$verify();
  },

  /**
     Tests that preventDefault is not called for TOUCHSTART event.
     @suppress {checkTypes} suppression added to enable type checking
   */
  testStartDrag_TouchStart() {
    const dragger = new Dragger(target);

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.TOUCHSTART;
    // preventDefault is not called.
    e.$replay();

    let startDragFired = false;
    events.listen(dragger, Dragger.EventType.START, (e) => {
      startDragFired = true;
    });

    dragger.startDrag(e);

    assertTrue('Dragging should be in progress.', dragger.isDragging());
    assertTrue('Start drag event should have fired.', startDragFired);
    assertTrue(
        'Dragger must have registered touchstart listener.',
        events.hasListener(
            dragger.handle, EventType.TOUCHSTART, false /*opt_cap*/));
    e.$verify();
  },

  /**
   * Tests that preventDefault is not called for TOUCHSTART event when
   * hysteresis is set to be greater than zero.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testStartDrag_TouchStart_NonZeroHysteresis() {
    const dragger = new Dragger(target);
    dragger.setHysteresis(5);
    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.TOUCHSTART;
    // preventDefault is not called.
    e.$replay();

    let startDragFired = false;
    events.listen(dragger, Dragger.EventType.START, (e) => {
      startDragFired = true;
    });

    dragger.startDrag(e);

    assertFalse(
        'Start drag must not start drag because of hysterisis.',
        dragger.isDragging());
    assertTrue(
        'Dragger must have registered touchstart listener.',
        events.hasListener(
            dragger.handle, EventType.TOUCHSTART, false /*opt_cap*/));
    e.$verify();
  },

  /**
     @bug 1381317 Cancelling start drag didn't end the attempt to drag.
     @suppress {missingProperties,checkTypes,visibility} suppression added to
     enable type checking
   */
  testHandleMove_Cancel() {
    const dragger = new Dragger(target);
    dragger.setHysteresis(5);

    events.listen(dragger, Dragger.EventType.START, (e) => {
      // Cancel drag.
      e.preventDefault();
    });

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(true).$anyTimes();
    // preventDefault is not called.
    e.$replay();
    dragger.startDrag(e);
    assertFalse(
        'Start drag must not start drag because of hysterisis.',
        dragger.isDragging());
    assertTrue(
        'Dragger must have registered mousemove handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEMOVE, !HAS_SET_CAPTURE));
    assertTrue(
        'Dragger must have registered mouseup handlers.',
        events.hasListener(
            dragger.document_, EventType.MOUSEUP, !HAS_SET_CAPTURE));

    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 10;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 10;
    dragger.handleMove_(e);
    assertFalse('Drag must be cancelled.', dragger.isDragging());
    assertFalse(
        'Dragger must unregistered mousemove handlers.',
        events.hasListener(dragger.document_, EventType.MOUSEMOVE, true));
    assertFalse(
        'Dragger must unregistered mouseup handlers.',
        events.hasListener(dragger.document_, EventType.MOUSEUP, true));
    e.$verify();
  },

  /**
     @suppress {missingProperties,checkTypes} suppression added to enable type
     checking
   */
  testPreventMouseDown() {
    const dragger = new Dragger(target);
    dragger.setPreventMouseDown(false);

    const e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEDOWN;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(true);
    // preventDefault is not called.
    e.$replay();

    dragger.startDrag(e);

    assertTrue('Dragging should be in progess.', dragger.isDragging());
    e.$verify();
  },

  testLimits() {
    const dragger = new Dragger(target);

    assertEquals(100, dragger.limitX(100));
    assertEquals(100, dragger.limitY(100));

    dragger.setLimits(new GoogRect(10, 20, 30, 40));

    assertEquals(10, dragger.limitX(0));
    assertEquals(40, dragger.limitX(100));
    assertEquals(20, dragger.limitY(0));
    assertEquals(60, dragger.limitY(100));
  },

  /**
     @suppress {missingProperties,checkTypes} suppression added to enable type
     checking
   */
  testWindowBlur() {
    const dragger = new Dragger(target);
    dragger.setAllowSetCapture(false);

    let dragEnded = false;
    events.listen(dragger, Dragger.EventType.END, (e) => {
      dragEnded = true;
    });

    let e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEDOWN;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(true);
    e.preventDefault();
    e.$replay();
    dragger.startDrag(e);
    e.$verify();

    assertTrue(dragger.isDragging());

    e = new BrowserEvent();
    e.type = EventType.BLUR;
    /** @suppress {checkTypes} suppression added to enable type checking */
    e.target = window;
    /** @suppress {checkTypes} suppression added to enable type checking */
    e.currentTarget = window;
    testingEvents.fireBrowserEvent(e);

    assertTrue(dragEnded);
  },

  /**
     @suppress {missingProperties,checkTypes} suppression added to enable type
     checking
   */
  testBlur() {
    const dragger = new Dragger(target);
    dragger.setAllowSetCapture(false);

    let dragEnded = false;
    events.listen(dragger, Dragger.EventType.END, (e) => {
      dragEnded = true;
    });

    let e = new StrictMock(BrowserEvent);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.type = EventType.MOUSEDOWN;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientX = 1;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    e.clientY = 2;
    e.isMouseActionButton().$returns(true);
    e.preventDefault();
    e.$replay();
    dragger.startDrag(e);
    e.$verify();

    assertTrue(dragger.isDragging());

    e = new BrowserEvent();
    e.type = EventType.BLUR;
    e.target = document.body;
    e.currentTarget = document.body;
    // Blur events do not bubble but the test event system does not emulate that
    // part so we add a capturing listener on the target and stops the
    // propagation at the target, preventing any event from bubbling.
    events.listen(document.body, EventType.BLUR, (e) => {
      e.propagationStopped_ = true;
    }, true);
    testingEvents.fireBrowserEvent(e);

    assertFalse(dragEnded);
  },

  /**
     @suppress {strictMissingProperties} suppression added to enable type
     checking
   */
  testCloneNode() {
    const element = dom.createDom(TagName.DIV);
    element.innerHTML = '<input type="hidden" value="v0">' +
        '<textarea>v1</textarea>' +
        '<textarea>v2</textarea>';
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    element.childNodes[0].value = '\'new\'\n"value"';
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    element.childNodes[1].value = '<' +
        '/textarea>&lt;3';
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    element.childNodes[2].value = '<script>\n\talert("oops!");<' +
        '/script>';
    let clone = Dragger.cloneNode(element);
    assertEquals(element.childNodes[0].value, clone.childNodes[0].value);
    assertEquals(element.childNodes[1].value, clone.childNodes[1].value);
    assertEquals(element.childNodes[2].value, clone.childNodes[2].value);
    /** @suppress {checkTypes} suppression added to enable type checking */
    clone = Dragger.cloneNode(element.childNodes[2]);
    assertEquals(element.childNodes[2].value, clone.value);
  },
});
