/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
 * Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

package org.mozilla.geckoview.test

import android.graphics.* // ktlint-disable no-wildcard-imports
import android.graphics.Bitmap
import android.os.SystemClock
import android.util.Base64
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports
import org.hamcrest.Matchers.closeTo
import org.hamcrest.Matchers.equalTo
import org.junit.Assume.assumeThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.ContentDelegate
import org.mozilla.geckoview.GeckoSession.ScrollDelegate
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import java.io.ByteArrayOutputStream

private const val SCREEN_WIDTH = 100
private const val SCREEN_HEIGHT = 200

@RunWith(AndroidJUnit4::class)
@MediumTest
class DynamicToolbarTest : BaseSessionTest() {
    // Makes sure we can load a page when the dynamic toolbar is bigger than the whole content
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun outOfRangeValue() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT + 1
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(HELLO_HTML_PATH)
        mainSession.waitForPageStop()
    }

    private fun assertScreenshotResult(result: GeckoResult<Bitmap>, comparisonImage: Bitmap) {
        sessionRule.waitForResult(result).let {
            assertThat(
                "Screenshot is not null",
                it,
                notNullValue(),
            )
            assertThat("Widths are the same", comparisonImage.width, equalTo(it.width))
            assertThat("Heights are the same", comparisonImage.height, equalTo(it.height))
            assertThat("Byte counts are the same", comparisonImage.byteCount, equalTo(it.byteCount))
            assertThat("Configs are the same", comparisonImage.config, equalTo(it.config))

            if (!comparisonImage.sameAs(it)) {
                val outputForComparison = ByteArrayOutputStream()
                comparisonImage.compress(Bitmap.CompressFormat.PNG, 100, outputForComparison)

                val outputForActual = ByteArrayOutputStream()
                it.compress(Bitmap.CompressFormat.PNG, 100, outputForActual)
                val actualString: String = Base64.encodeToString(outputForActual.toByteArray(), Base64.DEFAULT)
                val comparisonString: String = Base64.encodeToString(outputForComparison.toByteArray(), Base64.DEFAULT)

                assertThat("Encoded strings are the same", comparisonString, equalTo(actualString))
            }

            assertThat("Bytes are the same", comparisonImage.sameAs(it), equalTo(true))
        }
    }

    /**
     * Returns a whole green Bitmap.
     * This Bitmap would be a reference image of tests in this file.
     */
    private fun getComparisonScreenshot(width: Int, height: Int): Bitmap {
        val screenshotFile = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(screenshotFile)
        val paint = Paint()
        paint.color = Color.rgb(0, 128, 0)
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
        return screenshotFile
    }

    // With the dynamic toolbar max height vh units values exceed
    // the top most window height. This is a test case that exceeded area
    // is rendered properly (on the compositor).
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun positionFixedElementClipping() {
        sessionRule.display?.run { setDynamicToolbarMaxHeight(SCREEN_HEIGHT / 2) }

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        // FIXED_VH is an HTML file which has a position:fixed element whose
        // style is "width: 100%; height: 200vh" and the document is scaled by
        // minimum-scale 0.5, so that the height of the element exceeds the
        // window height.
        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        // Scroll down bit, if we correctly render the document, the position
        // fixed element still covers whole the document area.
        mainSession.evaluateJS("window.scrollTo({ top: 100, behavior: 'instant' })")

        // Wait a while to make sure the scrolling result is composited on the compositor
        // since capturePixels() takes a snapshot directly from the compositor without
        // waiting for a corresponding MozAfterPaint on the main-thread so it's possible
        // to take a stale snapshot even if it's a result of syncronous scrolling.
        mainSession.evaluateJS("new Promise(resolve => window.setTimeout(resolve, 1000))")

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    // Asynchronous scrolling with the dynamic toolbar max height causes
    // situations where the visual viewport size gets bigger than the layout
    // viewport on the compositor thread because of 200vh position:fixed
    // elements.  This is a test case that a 200vh position element is
    // properly rendered its positions.
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun layoutViewportExpansion() {
        sessionRule.display?.run { setDynamicToolbarMaxHeight(SCREEN_HEIGHT / 2) }

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        mainSession.evaluateJS("window.scrollTo(0, 100)")

        // Scroll back to the original position by asynchronous scrolling.
        mainSession.evaluateJS("window.scrollTo({ top: 0, behavior: 'smooth' })")

        mainSession.evaluateJS("new Promise(resolve => window.setTimeout(resolve, 1000))")

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun visualViewportEvents() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double
        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double

        for (i in 1..dynamicToolbarMaxHeight) {
            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }

            val expectedViewportHeight = (SCREEN_HEIGHT - dynamicToolbarMaxHeight + i) / scale / pixelRatio
            val promise = mainSession.evaluatePromiseJS(
                """
             new Promise(resolve => {
               window.visualViewport.addEventListener('resize', resolve(window.visualViewport.height));
             });
                """.trimIndent(),
            )

            assertThat(
                "The visual viewport height should be changed in response to the dynamc toolbar transition",
                promise.value as Double,
                closeTo(expectedViewportHeight, .01),
            )
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun percentBaseValueOnPositionFixedElement() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_PERCENT)
        mainSession.waitForPageStop()

        val originalHeight = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        // Set the vertical clipping value to the middle of toolbar transition.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight / 2) }

        var height = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        assertThat(
            "The %-based height should be the static in the middle of toolbar tansition",
            height,
            equalTo(originalHeight),
        )

        // Set the vertical clipping value to hide the toolbar completely.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        height = mainSession.evaluateJS(
            """
            getComputedStyle(document.querySelector('#fixed-element')).height
            """.trimIndent(),
        ) as String

        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double
        val expectedHeight = (SCREEN_HEIGHT / scale).toInt()
        assertThat(
            "The %-based height should be now recomputed based on the screen height",
            height,
            equalTo(expectedHeight.toString() + "px"),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun resizeEvents() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        for (i in 1..dynamicToolbarMaxHeight - 1) {
            val promise = mainSession.evaluatePromiseJS(
                """
                new Promise(resolve => {
                    let fired = false;
                    window.addEventListener('resize', () => { fired = true; }, { once: true });
                    // Note that `resize` event is fired just before rAF callbacks, so under ideal
                    // circumstances waiting for a rAF should be sufficient, even if it's not sufficient
                    // unexpected resize event(s) will be caught in the next loop.
                    requestAnimationFrame(() => { resolve(fired); });
                });
                """.trimIndent(),
            )

            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }
            assertThat(
                "'resize' event on window should not be fired in response to the dynamc toolbar transition",
                promise.value as Boolean,
                equalTo(false),
            )
        }

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                window.addEventListener('resize', () => { resolve(true); }, { once: true });
            });
            """.trimIndent(),
        )

        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        assertThat(
            "'resize' event on window should be fired when the dynamc toolbar is completely hidden",
            promise.value as Boolean,
            equalTo(true),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun windowInnerHeight() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        // We intentionally use FIXED_BOTTOM instead of FIXED_VH in this test since
        // FIXED_VH has `minimum-scale=0.5` thus we can't properly test window.innerHeight
        // with FXIED_VH for now due to bug 1598487.
        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double

        for (i in 1..dynamicToolbarMaxHeight - 1) {
            val promise = mainSession.evaluatePromiseJS(
                """
               new Promise(resolve => {
                 window.visualViewport.addEventListener('resize', resolve(window.innerHeight));
               });
                """.trimIndent(),
            )

            // Simulate the dynamic toolbar is going to be hidden.
            sessionRule.display?.run { setVerticalClipping(-i) }
            assertThat(
                "window.innerHeight should not be changed in response to the dynamc toolbar transition",
                promise.value as Double,
                closeTo(SCREEN_HEIGHT / 2 / pixelRatio, .01),
            )
        }

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                window.addEventListener('resize', () => { resolve(window.innerHeight); }, { once: true });
            });
            """.trimIndent(),
        )

        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        assertThat(
            "window.innerHeight should be changed when the dynamc toolbar is completely hidden",
            promise.value as Double,
            closeTo(SCREEN_HEIGHT / pixelRatio, .01),
        )
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun notCrashOnResizeEvent() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_VH)
        mainSession.waitForPageStop()

        val promise = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => window.addEventListener('resize', () => resolve(true)));
            """.trimIndent(),
        )

        // Do some setVerticalClipping calls that we might try to queue two window resize events.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight + 1) }
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        assertThat("Got a rezie event", promise.value as Boolean, equalTo(true))
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun showDynamicToolbar() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(SHOW_DYNAMIC_TOOLBAR_HTML_PATH)
        mainSession.waitForPageStop()
        mainSession.evaluateJS("window.scrollTo(0, " + dynamicToolbarMaxHeight + ")")
        mainSession.waitUntilCalled(object : ScrollDelegate {
            @AssertCalled(count = 1)
            override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            }
        })

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.synthesizeTap(5, 25)

        mainSession.waitUntilCalled(object : ContentDelegate {
            @AssertCalled(count = 1)
            override fun onShowDynamicToolbar(session: GeckoSession) {
            }
        })
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun showDynamicToolbarOnOverflowHidden() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(SHOW_DYNAMIC_TOOLBAR_HTML_PATH)
        mainSession.waitForPageStop()
        mainSession.evaluateJS("window.scrollTo(0, " + dynamicToolbarMaxHeight + ")")
        mainSession.waitUntilCalled(object : ScrollDelegate {
            @AssertCalled(count = 1)
            override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            }
        })

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.evaluateJS("document.documentElement.style.overflow = 'hidden'")

        mainSession.waitUntilCalled(object : ContentDelegate {
            @AssertCalled(count = 1)
            override fun onShowDynamicToolbar(session: GeckoSession) {
            }
        })
    }

    private fun getComputedViewportHeight(style: String): Double {
        val viewportHeight = mainSession.evaluateJS(
            """
            const target = document.createElement('div');
            target.style.height = '$style';
            document.body.appendChild(target);
            parseFloat(getComputedStyle(target).height);
            """.trimIndent(),
        ) as Double

        return viewportHeight
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun viewportVariants() {
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.VIEWPORT_PATH)
        mainSession.waitForPageStop()

        val pixelRatio = mainSession.evaluateJS("window.devicePixelRatio") as Double
        val scale = mainSession.evaluateJS("window.visualViewport.scale") as Double

        var smallViewportHeight = getComputedViewportHeight("100svh")
        assertThat(
            "svh value at the initial state",
            smallViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        var largeViewportHeight = getComputedViewportHeight("100lvh")
        assertThat(
            "lvh value at the initial state",
            largeViewportHeight,
            closeTo(SCREEN_HEIGHT / scale / pixelRatio, 0.1),
        )

        var dynamicViewportHeight = getComputedViewportHeight("100dvh")
        assertThat(
            "dvh value at the initial state",
            dynamicViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        // Move down the toolbar at a fourth of its position.
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight / 4) }

        smallViewportHeight = getComputedViewportHeight("100svh")
        assertThat(
            "svh value during toolbar transition",
            smallViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight) / scale / pixelRatio, 0.1),
        )

        largeViewportHeight = getComputedViewportHeight("100lvh")
        assertThat(
            "lvh value during toolbar transition",
            largeViewportHeight,
            closeTo(SCREEN_HEIGHT / scale / pixelRatio, 0.1),
        )

        dynamicViewportHeight = getComputedViewportHeight("100dvh")
        assertThat(
            "dvh value during toolbar transition",
            dynamicViewportHeight,
            closeTo((SCREEN_HEIGHT - dynamicToolbarMaxHeight + dynamicToolbarMaxHeight / 4) / scale / pixelRatio, 0.1),
        )
    }

    // With dynamic toolbar, there was a floating point rounding error in Gecko layout side.
    // The error was appeared by user interactive async scrolling, not by programatic async
    // scrolling, e.g. scrollTo() method. If the error happens there will appear 1px gap
    // between <body> and an element which covers up the <body> element.
    // This test simulates the situation.
    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun noGapAppearsBetweenBodyAndElementFullyCoveringBody() {
        // Bug 1764219 - disable the test to reduce intermittent failure rate
        assumeThat(sessionRule.env.isDebugBuild, equalTo(false))
        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        mainSession.loadTestPath(BaseSessionTest.BODY_FULLY_COVERED_BY_GREEN_ELEMENT)
        mainSession.waitForPageStop()
        mainSession.flushApzRepaints()

        // Scrolling down by touch events.
        var downTime = SystemClock.uptimeMillis()
        var down = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            50f,
            70f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(down)
        var move = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_MOVE,
            50f,
            30f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(move)
        var up = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_UP,
            50f,
            10f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(up)
        mainSession.flushApzRepaints()

        // Scrolling up by touch events to restore the original position.
        downTime = SystemClock.uptimeMillis()
        down = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_DOWN,
            50f,
            10f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(down)
        move = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_MOVE,
            50f,
            30f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(move)
        up = MotionEvent.obtain(
            downTime,
            SystemClock.uptimeMillis(),
            MotionEvent.ACTION_UP,
            50f,
            70f,
            0,
        )
        mainSession.panZoomController.onTouchEvent(up)
        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun zoomedOverflowHidden() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for foreground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        // Change the body background color to match the reference image's background color.
        mainSession.evaluateJS("document.body.style.background = 'rgb(0, 128, 0)'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        // Zoom in the content so that the content's visual viewport can be scrollable.
        mainSession.setResolutionAndScaleTo(10.0f)

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun zoomedPositionFixedRoot() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for foreground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.FIXED_BOTTOM)
        mainSession.waitForPageStop()

        // Change the body background color to match the reference image's background color.
        mainSession.evaluateJS("document.body.style.background = 'rgb(0, 128, 0)'")

        // Change the root `overlow` style to make it scrollable and change the position style
        // to `fixed` so that the root container is not scrollable.
        mainSession.evaluateJS("document.body.style.overflow = 'scroll'")
        mainSession.evaluateJS("document.documentElement.style.position = 'fixed'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        // Zoom in the content so that the content's visual viewport can be scrollable.
        mainSession.setResolutionAndScaleTo(10.0f)

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun backgroundImageFixed() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.TOUCH_ACTION_HTML_PATH)
        mainSession.waitForPageStop()

        // Specify the root background-color to match the reference image color and specify
        // `background-attachment: fixed`.
        mainSession.evaluateJS("document.documentElement.style.background = 'linear-gradient(green, green) fixed'")

        // Make the root element scrollable.
        mainSession.evaluateJS("document.documentElement.style.height = '100vh'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        mainSession.flushApzRepaints()

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }

    @WithDisplay(height = SCREEN_HEIGHT, width = SCREEN_WIDTH)
    @Test
    fun backgroundAttachmentFixed() {
        val reference = getComparisonScreenshot(SCREEN_WIDTH, SCREEN_HEIGHT)

        val dynamicToolbarMaxHeight = SCREEN_HEIGHT / 2
        sessionRule.display?.run { setDynamicToolbarMaxHeight(dynamicToolbarMaxHeight) }

        // Set active since setVerticalClipping call affects only for forground tab.
        mainSession.setActive(true)

        mainSession.loadTestPath(BaseSessionTest.TOUCH_ACTION_HTML_PATH)
        mainSession.waitForPageStop()

        // Specify the root background-color to match the reference image color and specify
        // `background-attachment: fixed`.
        mainSession.evaluateJS("document.documentElement.style.background = 'rgb(0, 128, 0) fixed'")

        // Make the root element scrollable.
        mainSession.evaluateJS("document.documentElement.style.height = '100vh'")

        // Hide the vertical scrollbar.
        mainSession.evaluateJS("document.documentElement.style.scrollbarWidth = 'none'")

        mainSession.flushApzRepaints()

        // Simulate the dynamic toolbar being hidden by the scroll
        sessionRule.display?.run { setVerticalClipping(-dynamicToolbarMaxHeight) }

        mainSession.flushApzRepaints()

        sessionRule.display?.let {
            assertScreenshotResult(it.capturePixels(), reference)
        }
    }
}
