# Copyright 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import division
from __future__ import absolute_import
from telemetry import decorators
from telemetry.internal.actions import page_action
from telemetry.internal.actions import scroll
from telemetry.internal.actions import utils
from telemetry.testing import tab_test_case


class ScrollActionTest(tab_test_case.TabTestCase):

  def _MakePageVerticallyScrollable(self):
    # Make page taller than window so it's scrollable vertically.
    self._tab.ExecuteJavaScript(
        'document.body.style.height ='
        '(3 * __GestureCommon_GetWindowHeight() + 1) + "px";')

  def _MakePageHorizontallyScrollable(self):
    # Make page wider than window so it's scrollable horizontally.
    self._tab.ExecuteJavaScript(
        'document.body.style.width ='
        '(3 * __GestureCommon_GetWindowWidth() + 1) + "px";')

  def setUp(self):
    tab_test_case.TabTestCase.setUp(self)
    self.Navigate('blank.html')
    utils.InjectJavaScript(self._tab, 'gesture_common.js')

  def _RunScrollDistanceTest(self, distance, speed, source, maxError):
    # TODO(bokan): Distance tests will fail on versions of Chrome that haven't
    # been fixed.  The fixes landed at the same time as the
    # setBrowserControlsShown method was added so only run the test if that's
    # available. Once that rolls into ref builds we can remove this check.
    distanceFixedInChrome = self._tab.EvaluateJavaScript(
        "'setBrowserControlsShown' in chrome.gpuBenchmarking")
    if not distanceFixedInChrome:
      return

    # Hide the URL bar so we can measure scrolled distance without worrying
    # about the URL bar consuming delta.
    self._tab.ExecuteJavaScript(
        'chrome.gpuBenchmarking.setBrowserControlsShown(false);')

    # Make the document tall enough to accomodate the requested distance but
    # also leave enough space so we can tell if the scroll overshoots the
    # target.
    screenHeight = self._tab.EvaluateJavaScript('window.visualViewport.height')
    documentHeight = (screenHeight + distance) * 2

    self._tab.ExecuteJavaScript(
        'document.body.style.height = "' + str(documentHeight) + 'px";')
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollTop'), 0)

    # Allow for some visual viewport offset. For example, if the test doesn't
    # want any visual viewport offset due to animation handoff error between
    # the two viewports.
    start_offset = self._tab.EvaluateJavaScript('window.visualViewport.pageTop')

    i = scroll.ScrollAction(
        distance=distance,
        direction="down",
        speed_in_pixels_per_second=speed,
        synthetic_gesture_source=source)
    i.WillRunAction(self._tab)
    i.RunAction(self._tab)

    actual = self._tab.EvaluateJavaScript(
        'window.visualViewport.pageTop') - start_offset

    # TODO(bokan): setBrowserControlsShown isn't quite enough. Chrome will hide
    # the browser controls but then they animate in after a timeout. We'll need
    # to add a way to lock them to hidden. Until then, just increase the
    # allowed error.
    urlBarError = 150

    self.assertAlmostEqual(distance, actual, delta=maxError + urlBarError)

  @decorators.Disabled('chromeos', 'linux')  # crbug.com/1006789
  def testScrollDistanceFastTouch(self):
    # Just pass the test on platforms that don't support touch (i.e. Mac)
    if not page_action.IsGestureSourceTypeSupported(self._tab, 'touch'):
      return

    # Scrolling distance for touch will have some error from the excess delta
    # of the event that crosses the slop threshold but isn't applied, also
    # scroll resampling can increase the error amount..
    self._RunScrollDistanceTest(
        500000, 200000, page_action.GESTURE_SOURCE_TOUCH, 200)

  @decorators.Disabled('android-reference')  # crbug.com/934649
  def testScrollDistanceFastWheel(self):
    # Wheel scrolling will have a much greater error than touch. There's 2
    # reasons: 1) synthetic wheel gesture accumulate the sent deltas and use
    # that to determine how much delta to send at each event dispatch time.
    # This assumes that the entire sent delta is applied which is wrong due to
    # physical pixel snapping which accumulates over the gesture.
    # 2) We can only send delta as ticks of the wheel. If the total delta is
    # not a multiple of the tick size, we'll "lose" the remainder.
    self._RunScrollDistanceTest(
        500000, 200000, page_action.GESTURE_SOURCE_MOUSE, 15000)

  def testScrollDistanceSlowTouch(self):
    # Just pass the test on platforms that don't support touch (i.e. Mac)
    if not page_action.IsGestureSourceTypeSupported(self._tab, 'touch'):
      return

    # Scrolling slowly produces larger error since each event will have a
    # smaller delta. Thus error from snapping in each event will be a larger
    # share of the total delta.
    self._RunScrollDistanceTest(
        1000, 300, page_action.GESTURE_SOURCE_TOUCH, 10)

  @decorators.Disabled('android-reference')  # crbug.com/934649
  def testScrollDistanceSlowWheel(self):
    self._RunScrollDistanceTest(
        1000, 300, page_action.GESTURE_SOURCE_MOUSE, 200)

  @decorators.Disabled('android-reference')  # crbug.com/934649
  @decorators.Disabled('win-reference')  # crbug.com/805523
  def testWheelScrollDistanceWhileZoomed(self):
    # TODO(bokan): This API was added recently so only run the test once it's
    # available. Remove this check once it rolls into stable builds.
    chromeSupportsSetPageScaleFactor = self._tab.EvaluateJavaScript(
        "'setPageScaleFactor' in chrome.gpuBenchmarking")
    if not chromeSupportsSetPageScaleFactor:
      return

    self._tab.EvaluateJavaScript('chrome.gpuBenchmarking.setPageScaleFactor(2)')

    # Wheel scrolling can cause animated scrolls. This is a problem here since
    # Chrome currently doesn't hand off the animation between the visual and
    # layout viewports. To account for this, scroll the visual viewport to it's
    # maximum extent so that the entire scroll goes to the layout viewport.
    screenHeight = self._tab.EvaluateJavaScript('window.visualViewport.height')

    i = scroll.ScrollAction(
        distance=screenHeight*2,
        direction="down",
        speed_in_pixels_per_second=5000,
        synthetic_gesture_source=page_action.GESTURE_SOURCE_MOUSE)
    i.WillRunAction(self._tab)
    i.RunAction(self._tab)

    # Ensure the layout viewport isn't scrolled but the visual is.
    self.assertGreater(
        self._tab.EvaluateJavaScript('window.visualViewport.offsetTop'),
        screenHeight // 2 - 1)
    self.assertEqual(self._tab.EvaluateJavaScript('window.scrollY'), 0)

    self._RunScrollDistanceTest(
        2000, 2000, page_action.GESTURE_SOURCE_MOUSE, 60)

  def testTouchScrollDistanceWhileZoomed(self):
    # Just pass the test on platforms that don't support touch (i.e. Mac)
    if not page_action.IsGestureSourceTypeSupported(self._tab, 'touch'):
      return

    # TODO(bokan): This API was added recently so only run the test once it's
    # available. Remove this check once it rolls into stable builds.
    chromeSupportsSetPageScaleFactor = self._tab.EvaluateJavaScript(
        "'setPageScaleFactor' in chrome.gpuBenchmarking")
    if not chromeSupportsSetPageScaleFactor:
      return

    self._tab.EvaluateJavaScript('chrome.gpuBenchmarking.setPageScaleFactor(2)')
    self._RunScrollDistanceTest(
        2000, 2000, page_action.GESTURE_SOURCE_TOUCH, 20)

  def testScrollAction(self):

    self._MakePageVerticallyScrollable()
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollTop'), 0)

    i = scroll.ScrollAction()
    i.WillRunAction(self._tab)

    self._tab.ExecuteJavaScript("""
        window.__scrollAction.beginMeasuringHook = function() {
            window.__didBeginMeasuring = true;
        };
        window.__scrollAction.endMeasuringHook = function() {
            window.__didEndMeasuring = true;
        };""")
    i.RunAction(self._tab)

    self.assertTrue(self._tab.EvaluateJavaScript('window.__didBeginMeasuring'))
    self.assertTrue(self._tab.EvaluateJavaScript('window.__didEndMeasuring'))

    scroll_position = self._tab.EvaluateJavaScript(
        'document.scrollingElement.scrollTop')
    self.assertTrue(
        scroll_position != 0, msg='scroll_position=%d;' % (scroll_position))

  # https://github.com/catapult-project/catapult/issues/3099
  @decorators.Disabled('android')
  @decorators.Disabled('chromeos')  # crbug.com/984016
  def testDiagonalScrollAction(self):
    self._MakePageVerticallyScrollable()
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollTop'), 0)

    self._MakePageHorizontallyScrollable()
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollLeft'), 0)

    i = scroll.ScrollAction(direction='downright')
    i.WillRunAction(self._tab)

    i.RunAction(self._tab)

    viewport_top = self._tab.EvaluateJavaScript(
        'document.scrollingElement.scrollTop')
    self.assertTrue(viewport_top != 0, msg='viewport_top=%d;' % viewport_top)

    viewport_left = self._tab.EvaluateJavaScript(
        'document.scrollingElement.scrollLeft')
    self.assertTrue(viewport_left != 0, msg='viewport_left=%d;' % viewport_left)

  def testBoundingClientRect(self):
    # Verify that the rect returned by getBoundingVisibleRect() in scroll.js is
    # completely contained within the viewport. Scroll events dispatched by the
    # scrolling API use the center of this rect as their location, and this
    # location needs to be within the viewport bounds to correctly decide
    # between main-thread and impl-thread scroll. If the scrollable area were
    # not clipped to the viewport bounds, then the instance used here (the
    # scrollable area being more than twice as tall as the viewport) would
    # result in a scroll location outside of the viewport bounds.
    self._MakePageVerticallyScrollable()
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollTop'), 0)

    self._MakePageHorizontallyScrollable()
    self.assertEquals(
        self._tab.EvaluateJavaScript('document.scrollingElement.scrollLeft'), 0)

    self._tab.ExecuteJavaScript("""
        window.scrollTo(__GestureCommon_GetWindowWidth(),
                        __GestureCommon_GetWindowHeight());""")

    rect_top = int(
        self._tab.EvaluateJavaScript(
            '__GestureCommon_GetBoundingVisibleRect(document.body).top'))
    rect_height = int(
        self._tab.EvaluateJavaScript(
            '__GestureCommon_GetBoundingVisibleRect(document.body).height'))
    rect_bottom = rect_top + rect_height

    rect_left = int(
        self._tab.EvaluateJavaScript(
            '__GestureCommon_GetBoundingVisibleRect(document.body).left'))
    rect_width = int(
        self._tab.EvaluateJavaScript(
            '__GestureCommon_GetBoundingVisibleRect(document.body).width'))
    rect_right = rect_left + rect_width

    viewport_height = int(
        self._tab.EvaluateJavaScript('__GestureCommon_GetWindowHeight()'))
    viewport_width = int(
        self._tab.EvaluateJavaScript('__GestureCommon_GetWindowWidth()'))

    self.assertTrue(rect_top >= 0, msg='%s >= %s' % (rect_top, 0))
    self.assertTrue(rect_left >= 0, msg='%s >= %s' % (rect_left, 0))
    self.assertTrue(
        rect_bottom <= viewport_height,
        msg='%s + %s <= %s' % (rect_top, rect_height, viewport_height))
    self.assertTrue(
        rect_right <= viewport_width,
        msg='%s + %s <= %s' % (rect_left, rect_width, viewport_width))
