/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

@file:Suppress("TooManyFunctions")

package org.mozilla.fenix.ui.robots

import android.graphics.Bitmap
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withHint
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import mozilla.components.browser.state.state.searchEngines
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.MatcherHelper.assertCheckedItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.checkedItemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.withBitmapDrawable
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.utils.Settings

/**
 * Implementation of Robot Pattern for the home screen menu.
 */
class HomeScreenRobot {
    val privateSessionMessage =
        "$appName clears your search and browsing history from private tabs when you close them" +
            " or quit the app. While this doesn’t make you anonymous to websites or your internet" +
            " service provider, it makes it easier to keep what you do online private from anyone" +
            " else who uses this device."

    fun verifyNavigationToolbar() = assertItemWithResIdExists(navigationToolbar)
    fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
    fun verifyHomeScreen() = assertItemWithResIdExists(homeScreen)

    fun verifyPrivateBrowsingHomeScreen() {
        verifyHomeScreenAppBarItems()
        assertItemContainingTextExists(itemContainingText(privateSessionMessage))
        verifyCommonMythsLink()
        verifyNavigationToolbarItems()
    }

    fun verifyHomeScreenAppBarItems() =
        assertItemWithResIdExists(homeScreen, privateBrowsingButton, homepageWordmark)

    fun verifyHomeScreenWelcomeItems() =
        assertItemContainingTextExists(welcomeHeader, welcomeSubHeader)

    fun verifyChooseYourThemeCard(
        isDarkThemeChecked: Boolean,
        isLightThemeChecked: Boolean,
        isAutomaticThemeChecked: Boolean,
    ) {
        scrollToElementByText(getStringResource(R.string.onboarding_theme_picker_header))
        assertItemContainingTextExists(
            chooseThemeHeader,
            chooseThemeText,
            darkThemeDescription,
            lightThemeDescription,
        )
        assertCheckedItemWithResIdExists(
            darkThemeToggle(isDarkThemeChecked),
            lightThemeToggle(isLightThemeChecked),
            automaticThemeToggle(isAutomaticThemeChecked),
        )
        assertItemWithResIdAndDescriptionExists(automaticThemeDescription)
    }

    fun clickLightThemeButton() =
        itemWithResId("$packageName:id/theme_light_radio_button").click()

    fun clickDarkThemeButton() =
        itemWithResId("$packageName:id/theme_dark_radio_button").click()

    fun clickAutomaticThemeButton() =
        itemWithResId("$packageName:id/theme_automatic_radio_button").click()

    fun verifyToolbarPlacementCard(isBottomChecked: Boolean, isTopChecked: Boolean) {
        scrollToElementByText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
        assertItemContainingTextExists(toolbarPlacementHeader, toolbarPlacementDescription)
        assertCheckedItemWithResIdExists(
            toolbarPlacementBottomRadioButton(isBottomChecked),
            toolbarPlacementTopRadioButton(isTopChecked),
        )
        assertItemWithResIdExists(
            toolbarPlacementBottomImage,
            toolbarPlacementBottomTitle,
            toolbarPlacementTopImage,
            toolbarPlacementTopTitle,
        )
    }

    fun clickTopToolbarPlacementButton() =
        itemWithResId("$packageName:id/toolbar_top_radio_button").click()

    fun clickBottomToolbarPlacementButton() =
        itemWithResId("$packageName:id/toolbar_bottom_radio_button").click()

    fun verifySignInToSyncCard() {
        scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header))
        assertItemContainingTextExists(startSyncHeader, startSyncDescription)
        assertItemWithResIdExists(signInButton)
    }

    fun verifyPrivacyProtectionCard(settings: Settings, isStandardChecked: Boolean, isStrictChecked: Boolean) {
        scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
        assertItemContainingTextExists(privacyProtectionHeader, privacyProtectionDescription(settings))
        assertCheckedItemWithResIdExists(
            standardTrackingProtectionToggle(isStandardChecked),
            strictTrackingProtectionToggle(isStrictChecked),
        )
    }

    fun clickStandardTrackingProtectionButton() =
        itemWithResId("$packageName:id/tracking_protection_standard_option").click()

    fun clickStrictTrackingProtectionButton() =
        itemWithResId("$packageName:id/tracking_protection_strict_default").click()

    fun verifyPrivacyNoticeCard() {
        scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_read_button))
        assertItemContainingTextExists(privacyNoticeHeader, privacyNoticeDescription)
        assertItemWithResIdExists(privacyNoticeButton)
    }

    fun verifyStartBrowsingSection() {
        scrollToElementByText(getStringResource(R.string.onboarding_finish))
        assertItemWithResIdExists(startBrowsingButton)
        assertItemContainingTextExists(conclusionHeader)
    }

    fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") {
        assertItemWithResIdExists(navigationToolbar, menuButton)
        assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
    }

    fun verifyHomePrivateBrowsingButton() = assertItemWithResIdExists(privateBrowsingButton)
    fun verifyHomeMenuButton() = assertItemWithResIdExists(menuButton)
    fun verifyTabButton() = assertTabButton()
    fun verifyCollectionsHeader() = assertCollectionsHeader()
    fun verifyNoCollectionsText() = assertNoCollectionsText()
    fun verifyHomeWordmark() = assertItemWithResIdExists(homepageWordmark)
    fun verifyHomeComponent() = assertHomeComponent()
    fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
    fun verifyTabCounter(numberOfOpenTabs: String) =
        assertItemWithResIdAndTextExists(tabCounter(numberOfOpenTabs))
    fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)

    fun verifyWallpaperImageApplied(isEnabled: Boolean) {
        if (isEnabled) {
            assertTrue(
                mDevice.findObject(
                    UiSelector().resourceId("$packageName:id/wallpaperImageView"),
                ).waitForExists(waitingTimeShort),
            )
        } else {
            assertFalse(
                mDevice.findObject(
                    UiSelector().resourceId("$packageName:id/wallpaperImageView"),
                ).waitForExists(waitingTimeShort),
            )
        }

        mDevice.findObject(UiSelector())
    }

    // First Run elements
    fun verifyWelcomeHeader() = assertItemContainingTextExists(welcomeHeader)
    fun verifyAccountsSignInButton() {
        scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header))
        assertItemWithResIdExists(signInButton)
    }
    fun verifyStartBrowsingButton() {
        scrollToElementByText(getStringResource(R.string.onboarding_finish))
        assertItemWithResIdExists(startBrowsingButton)
    }

    // Upgrading users onboarding dialog
    fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) {
        testRule.also {
            it.onNodeWithText(getStringResource(R.string.onboarding_home_welcome_title_2))
                .assertIsDisplayed()

            it.onNodeWithText(getStringResource(R.string.onboarding_home_welcome_description))
                .assertIsDisplayed()

            it.onNodeWithText(getStringResource(R.string.onboarding_home_get_started_button))
                .assertIsDisplayed()
        }
    }

    fun clickGetStartedButton(testRule: ComposeTestRule) =
        testRule.onNodeWithText(getStringResource(R.string.onboarding_home_get_started_button)).performClick()

    fun verifyUpgradingUserOnboardingSecondScreen(testRule: ComposeTestRule) {
        testRule.also {
            it.onNodeWithText(getStringResource(R.string.onboarding_home_sync_title_3))
                .assertIsDisplayed()

            it.onNodeWithText(getStringResource(R.string.onboarding_home_sync_description))
                .assertIsDisplayed()

            it.onNodeWithText(getStringResource(R.string.onboarding_home_sign_in_button))
                .assertIsDisplayed()

            it.onNodeWithText(getStringResource(R.string.onboarding_home_skip_button))
                .assertIsDisplayed()
        }
    }

    fun clickSkipButton(testRule: ComposeTestRule) =
        testRule
            .onNodeWithText(getStringResource(R.string.onboarding_home_skip_button))
            .performClick()

    fun verifyCommonMythsLink() =
        assertItemContainingTextExists(itemContainingText(getStringResource(R.string.private_browsing_common_myths)))

    fun verifyExistingTopSitesList() = assertExistingTopSitesList()
    fun verifyNotExistingTopSitesList(title: String) = assertNotExistingTopSitesList(title)
    fun verifySponsoredShortcutDoesNotExist(sponsoredShortcutTitle: String, position: Int) =
        assertFalse(
            mDevice.findObject(
                UiSelector()
                    .resourceId("$packageName:id/top_site_item")
                    .index(position - 1),
            ).getChild(
                UiSelector()
                    .textContains(sponsoredShortcutTitle),
            ).waitForExists(waitingTimeShort),
        )
    fun verifyNotExistingSponsoredTopSitesList() = assertSponsoredTopSitesNotDisplayed()
    fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
    fun verifySponsoredShortcutDetails(sponsoredShortcutTitle: String, position: Int) {
        assertSponsoredShortcutLogoIsDisplayed(position)
        assertSponsoredShortcutTitle(sponsoredShortcutTitle, position)
        assertSponsoredSubtitleIsDisplayed(position)
    }
    fun verifyTopSiteContextMenuItems() = assertTopSiteContextMenuItems()

    fun verifyJumpBackInSectionIsDisplayed() {
        scrollToElementByText(getStringResource(R.string.recent_tabs_header))
        assertTrue(jumpBackInSection().waitForExists(waitingTime))
    }
    fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed()
    fun verifyJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) =
        assertJumpBackInItemTitle(testRule, itemTitle)
    fun verifyJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) =
        assertJumpBackInItemWithUrl(testRule, itemUrl)
    fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton()
    fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed()
    fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed()
    fun verifyRecentBookmarksSectionIsDisplayed() = assertRecentBookmarksSectionIsDisplayed()
    fun verifyRecentBookmarksSectionIsNotDisplayed() = assertRecentBookmarksSectionIsNotDisplayed()
    fun verifyPocketSectionIsDisplayed() = assertPocketSectionIsDisplayed()
    fun verifyPocketSectionIsNotDisplayed() = assertPocketSectionIsNotDisplayed()

    fun verifyRecentlyVisitedSearchGroupDisplayed(shouldBeDisplayed: Boolean, searchTerm: String, groupSize: Int) {
        // checks if the search group exists in the Recently visited section
        if (shouldBeDisplayed) {
            scrollToElementByText("Recently visited")
            assertTrue(
                mDevice.findObject(UiSelector().text(searchTerm))
                    .getFromParent(UiSelector().text("$groupSize sites"))
                    .waitForExists(waitingTimeShort),
            )
        } else {
            assertTrue(
                mDevice.findObject(UiSelector().text(searchTerm))
                    .getFromParent(UiSelector().text("$groupSize sites"))
                    .waitUntilGone(waitingTimeShort),
            )
        }
    }

    // Collections elements
    fun verifyCollectionIsDisplayed(title: String, collectionExists: Boolean = true) {
        if (collectionExists) {
            assertTrue(mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime))
        } else {
            assertTrue(mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime))
        }
    }

    fun togglePrivateBrowsingModeOnOff() {
        onView(ViewMatchers.withResourceName("privateBrowsingButton"))
            .perform(click())
    }

    fun swipeToBottom() = onView(withId(R.id.homeLayout)).perform(ViewActions.swipeUp())

    fun swipeToTop() =
        onView(withId(R.id.sessionControlRecyclerView)).perform(ViewActions.swipeDown())

    fun verifySnackBarText(expectedText: String) {
        mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
    }

    fun clickFirefoxLogo() = homepageWordmark.click()

    fun verifyThoughtProvokingStories(enabled: Boolean) {
        if (enabled) {
            scrollToElementByText(getStringResource(R.string.pocket_stories_header_1))
            assertTrue(
                mDevice.findObject(
                    UiSelector()
                        .textContains(
                            getStringResource(R.string.pocket_stories_header_1),
                        ),
                ).waitForExists(waitingTime),
            )
        } else {
            homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
            assertFalse(
                mDevice.findObject(
                    UiSelector()
                        .textContains(
                            getStringResource(R.string.pocket_stories_header_1),
                        ),
                ).waitForExists(waitingTimeShort),
            )
        }
    }

    fun scrollToPocketProvokingStories() =
        scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))

    fun swipePocketProvokingStories() {
        UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList()
            .swipeLeft(3)
    }

    fun verifyPocketRecommendedStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
        composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
        positions.forEach {
            composeTestRule.onNodeWithTag("pocket.stories")
                .onChildAt(it - 1)
                .assert(hasTestTag("pocket.recommended.story"))
        }
    }

    fun verifyPocketSponsoredStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
        composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
        positions.forEach {
            composeTestRule.onNodeWithTag("pocket.stories")
                .onChildAt(it - 1)
                .assert(hasTestTag("pocket.sponsored.story"))
        }
    }

    fun verifyDiscoverMoreStoriesButton(composeTestRule: ComposeTestRule, position: Int) {
        composeTestRule.onNodeWithTag("pocket.stories")
            .assertIsDisplayed()
            .onChildAt(position - 1)
            .assert(hasTestTag("pocket.discover.more.story"))
    }

    fun verifyStoriesByTopic(enabled: Boolean) {
        if (enabled) {
            scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))
            assertTrue(
                mDevice.findObject(
                    UiSelector()
                        .textContains(
                            getStringResource(R.string.pocket_stories_categories_header),
                        ),
                ).waitForExists(waitingTime),
            )
        } else {
            homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
            assertFalse(
                mDevice.findObject(
                    UiSelector()
                        .textContains(
                            getStringResource(R.string.pocket_stories_categories_header),
                        ),
                ).waitForExists(waitingTimeShort),
            )
        }
    }

    fun verifyStoriesByTopicItems() =
        assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.categories")).childCount > 1)

    fun verifyStoriesByTopicItemState(composeTestRule: ComposeTestRule, isSelected: Boolean, position: Int) {
        homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))

        if (isSelected) {
            composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
            storyByTopicItem(composeTestRule, position).assertIsSelected()
        } else {
            composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
            storyByTopicItem(composeTestRule, position).assertIsNotSelected()
        }
    }

    fun clickStoriesByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
        storyByTopicItem(composeTestRule, position).performClick()

    fun verifyPoweredByPocket(rule: ComposeTestRule) {
        homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
        rule.onNodeWithTag("pocket.header.title", true).assertIsDisplayed()
        rule.onNodeWithTag("pocket.header.subtitle", true).assertIsDisplayed()
    }

    fun verifyCustomizeHomepageButton(enabled: Boolean) {
        if (enabled) {
            scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1))
            assertTrue(
                mDevice.findObject(
                    UiSelector()
                        .textContains("Customize homepage"),
                ).waitForExists(waitingTime),
            )
        } else {
            homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
            assertFalse(
                mDevice.findObject(
                    UiSelector()
                        .textContains("Customize homepage"),
                ).waitForExists(waitingTimeShort),
            )
        }
    }

    fun verifyJumpBackInMessage() {
        assertTrue(
            mDevice.findObject(
                UiSelector().text(
                    getStringResource(R.string.onboarding_home_screen_jump_back_contextual_hint_2),
                ),
            ).waitForExists(waitingTime),
        )
    }

    fun getProvokingStoryPublisher(position: Int): String {
        val publisher = mDevice.findObject(
            UiSelector()
                .className("android.view.View")
                .index(position - 1),
        ).getChild(
            UiSelector()
                .className("android.widget.TextView")
                .index(1),
        ).text

        return publisher
    }

    fun verifyToolbarPosition(defaultPosition: Boolean) {
        onView(withId(R.id.toolbarLayout))
            .check(
                if (defaultPosition) {
                    isPartiallyBelow(withId(R.id.sessionControlRecyclerView))
                } else {
                    isCompletelyAbove(withId(R.id.homeAppBar))
                },
            )
    }
    fun verifyNimbusMessageCard(title: String, text: String, action: String) {
        val textView = UiSelector()
            .className(ComposeView::class.java)
            .className(View::class.java)
            .className(TextView::class.java)
        assertTrue(
            mDevice.findObject(textView.textContains(title)).waitForExists(waitingTime),
        )
        assertTrue(
            mDevice.findObject(textView.textContains(text)).waitForExists(waitingTime),
        )
        assertTrue(
            mDevice.findObject(textView.textContains(action)).waitForExists(waitingTime),
        )
    }

    class Transition {

        fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
            mDevice.findObject(
                UiSelector().descriptionContains("open tab. Tap to switch tabs."),
            ).waitForExists(waitingTime)

            tabsCounter().click()
            mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")))

            TabDrawerRobot().interact()
            return TabDrawerRobot.Transition()
        }

        fun openComposeTabDrawer(composeTestRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
            mDevice.waitForIdle(waitingTime)
            onView(withId(R.id.tab_button)).click()
            composeTestRule.onNodeWithTag(TabsTrayTestTag.tabsTray).assertExists()

            ComposeTabDrawerRobot(composeTestRule).interact()
            return ComposeTabDrawerRobot.Transition(composeTestRule)
        }

        fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
            // Issue: https://github.com/mozilla-mobile/fenix/issues/21578
            try {
                mDevice.waitNotNull(
                    Until.findObject(By.res("$packageName:id/menuButton")),
                    waitingTime,
                )
            } catch (e: AssertionError) {
                mDevice.pressBack()
            } finally {
                threeDotButton().perform(click())
            }

            ThreeDotMenuMainRobot().interact()
            return ThreeDotMenuMainRobot.Transition()
        }

        fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
            navigationToolbar.waitForExists(waitingTime)
            navigationToolbar.click()
            mDevice.waitForIdle()

            SearchRobot().interact()
            return SearchRobot.Transition()
        }

        fun dismissOnboarding() {
            openThreeDotMenu { }.openSettings { }.goBack { }
        }

        fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
            startBrowsingButton.click()

            SearchRobot().interact()
            return SearchRobot.Transition()
        }

        fun clickUpgradingUserOnboardingSignInButton(
            testRule: ComposeTestRule,
            interact: SyncSignInRobot.() -> Unit,
        ): SyncSignInRobot.Transition {
            testRule.onNodeWithText("Sign in").performClick()

            SyncSignInRobot().interact()
            return SyncSignInRobot.Transition()
        }

        fun togglePrivateBrowsingMode() {
            mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
                .waitForExists(
                    waitingTime,
                )
            privateBrowsingButton.click()
        }

        fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
            // Loop to press the PB icon for 5 times to display the Add the Private Browsing Shortcut CFR
            for (i in 1..5) {
                mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
                    .waitForExists(
                        waitingTime,
                    )

                privateBrowsingButton.click()
            }

            AddToHomeScreenRobot().interact()
            return AddToHomeScreenRobot.Transition()
        }

        fun pressBack() {
            onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
        }

        fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
            mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
                .waitForExists(waitingTime)
            navigationToolbar.click()

            NavigationToolbarRobot().interact()
            return NavigationToolbarRobot.Transition()
        }

        fun openContextMenuOnTopSitesWithTitle(
            title: String,
            interact: HomeScreenRobot.() -> Unit,
        ): Transition {
            onView(withId(R.id.top_sites_list)).perform(
                actionOnItem<RecyclerView.ViewHolder>(
                    hasDescendant(withText(title)),
                    ViewActions.longClick(),
                ),
            )

            HomeScreenRobot().interact()
            return Transition()
        }

        fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition {
            sponsoredShortcut(sponsoredShortcutTitle).click(LONG_CLICK_DURATION)

            HomeScreenRobot().interact()
            return Transition()
        }

        fun openTopSiteTabWithTitle(
            title: String,
            interact: BrowserRobot.() -> Unit,
        ): BrowserRobot.Transition {
            onView(withId(R.id.top_sites_list)).perform(
                actionOnItem<RecyclerView.ViewHolder>(hasDescendant(withText(title)), click()),
            )

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun openSponsoredShortcut(sponsoredShortcutTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            sponsoredShortcut(sponsoredShortcutTitle).click()

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun renameTopSite(title: String, interact: HomeScreenRobot.() -> Unit): Transition {
            onView(withText("Rename"))
                .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
                .perform(click())
            onView(Matchers.allOf(withId(R.id.top_site_title), instanceOf(EditText::class.java)))
                .perform(ViewActions.replaceText(title))
            onView(withId(android.R.id.button1)).perform((click()))

            HomeScreenRobot().interact()
            return Transition()
        }

        fun removeTopSite(interact: HomeScreenRobot.() -> Unit): Transition {
            onView(withText("Remove"))
                .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
                .perform(click())

            HomeScreenRobot().interact()
            return Transition()
        }

        fun deleteTopSiteFromHistory(interact: HomeScreenRobot.() -> Unit): Transition {
            mDevice.findObject(
                UiSelector().resourceId("$packageName:id/simple_text"),
            ).waitForExists(waitingTime)
            deleteFromHistory.click()

            HomeScreenRobot().interact()
            return Transition()
        }

        fun openTopSiteInPrivateTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            onView(withText("Open in private tab"))
                .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
                .perform(click())

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickSponsorsAndPrivacyButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            sponsorsAndPrivacyButton.waitForExists(waitingTime)
            sponsorsAndPrivacyButton.clickAndWaitForNewWindow(waitingTime)

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
            sponsoredShortcutsSettingsButton.waitForExists(waitingTime)
            sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime)

            SettingsSubMenuHomepageRobot().interact()
            return SettingsSubMenuHomepageRobot.Transition()
        }

        fun openCommonMythsLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            mDevice.findObject(
                UiSelector()
                    .textContains(
                        getStringResource(R.string.private_browsing_common_myths),
                    ),
            ).also { it.click() }

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickSaveTabsToCollectionButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
            scrollToElementByText(getStringResource(R.string.no_collections_description2))
            saveTabsToCollectionButton().click()

            TabDrawerRobot().interact()
            return TabDrawerRobot.Transition()
        }

        fun expandCollection(title: String, interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
            assertItemContainingTextExists(itemContainingText(title))
            itemContainingText(title).clickAndWaitForNewWindow(waitingTimeShort)
            assertItemWithDescriptionExists(itemWithDescription(getStringResource(R.string.remove_tab_from_collection)))

            CollectionRobot().interact()
            return CollectionRobot.Transition()
        }

        fun openRecentlyVisitedSearchGroupHistoryList(title: String, interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
            scrollToElementByText("Recently visited")
            val searchGroup = mDevice.findObject(UiSelector().text(title))
            searchGroup.waitForExists(waitingTimeShort)
            searchGroup.click()

            HistoryRobot().interact()
            return HistoryRobot.Transition()
        }

        fun openCustomizeHomepage(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
            homeScreenList().scrollToEnd(LISTS_MAXSWIPES)
            mDevice.findObject(
                UiSelector()
                    .textContains(
                        "Customize homepage",
                    ),
            ).clickAndWaitForNewWindow(waitingTime)

            SettingsSubMenuHomepageRobot().interact()
            return SettingsSubMenuHomepageRobot.Transition()
        }

        fun clickJumpBackInShowAllButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
            mDevice
                .findObject(
                    UiSelector()
                        .textContains(getStringResource(R.string.recent_tabs_show_all)),
                ).clickAndWaitForNewWindow(waitingTime)

            TabDrawerRobot().interact()
            return TabDrawerRobot.Transition()
        }

        fun clickJumpBackInItemWithTitle(itemTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            mDevice
                .findObject(
                    UiSelector()
                        .resourceId("recent.tab.title")
                        .textContains(itemTitle),
                ).clickAndWaitForNewWindow(waitingTime)

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickPocketStoryItem(publisher: String, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            mDevice.findObject(
                UiSelector()
                    .className("android.view.View")
                    .index(position - 1),
            ).getChild(
                UiSelector()
                    .className("android.widget.TextView")
                    .index(1)
                    .textContains(publisher),
            ).clickAndWaitForNewWindow(waitingTime)

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickPocketDiscoverMoreButton(composeTestRule: ComposeTestRule, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            composeTestRule.onNodeWithTag("pocket.stories")
                .assertIsDisplayed()
                .onChildAt(position - 1)
                .assert(hasTestTag("pocket.discover.more.story"))
                .performClick()

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickPocketLearnMoreLink(composeTestRule: ComposeTestRule, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
            composeTestRule.onNodeWithTag("pocket.header.subtitle", true).performClick()

            BrowserRobot().interact()
            return BrowserRobot.Transition()
        }

        fun clickSignInButton(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
            signInButton.clickAndWaitForNewWindow(waitingTimeShort)

            SyncSignInRobot().interact()
            return SyncSignInRobot.Transition()
        }

        fun clickPrivacyNoticeButton(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transition {
            privacyNoticeButton.clickAndWaitForNewWindow(waitingTimeShort)

            CustomTabRobot().interact()
            return CustomTabRobot.Transition()
        }
    }
}

fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
    HomeScreenRobot().interact()
    return HomeScreenRobot.Transition()
}

private fun homeScreenList() =
    UiScrollable(
        UiSelector()
            .resourceId("$packageName:id/sessionControlRecyclerView")
            .scrollable(true),
    ).setAsVerticalList()

private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
    Assert.assertEquals(
        isExpectedToBeVisible,
        mDevice
            .executeShellCommand("dumpsys input_method | grep mInputShown")
            .contains("mInputShown=true"),
    )

private fun assertFocusedNavigationToolbar() =
    onView(allOf(withHint("Search or enter address")))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

private fun assertTabButton() =
    onView(allOf(withId(R.id.tab_button), isDisplayed()))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

private fun assertCollectionsHeader() =
    onView(allOf(withText("Collections")))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

private fun assertNoCollectionsText() =
    onView(
        withText(
            containsString(
                "Collect the things that matter to you.\n" +
                    "Group together similar searches, sites, and tabs for quick access later.",
            ),
        ),
    ).check(matches(isDisplayed()))

private fun assertHomeComponent() =
    onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

private fun threeDotButton() = onView(allOf(withId(R.id.menuButton)))

private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: String) {
    onView(withId(R.id.search_engine_icon))
        .check(matches(withBitmapDrawable(searchEngineIcon, searchEngineName)))
}

private fun getSearchEngine(searchEngineName: String) =
    appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName }

private fun verifySearchEngineIcon(searchEngineName: String) {
    val defaultSearchEngine = getSearchEngine(searchEngineName)
        ?: throw AssertionError("No search engine with name $searchEngineName")
    verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name)
}

private fun collectionTitle(title: String, rule: ComposeTestRule) =
    rule.onNode(hasText(title))

private fun assertExistingTopSitesList() =
    onView(allOf(withId(R.id.top_sites_list)))
        .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))

private fun assertExistingTopSitesTabs(title: String) {
    mDevice.findObject(
        UiSelector()
            .resourceId("$packageName:id/top_site_title")
            .textContains(title),
    ).waitForExists(waitingTime)

    onView(allOf(withId(R.id.top_sites_list)))
        .check(matches(hasDescendant(withText(title))))
        .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}

private fun assertSponsoredShortcutLogoIsDisplayed(position: Int) =
    assertTrue(
        mDevice.findObject(
            UiSelector()
                .resourceId("$packageName:id/top_site_item")
                .index(position - 1),
        ).getChild(
            UiSelector()
                .resourceId("$packageName:id/favicon_card"),
        ).waitForExists(waitingTime),
    )

private fun assertSponsoredSubtitleIsDisplayed(position: Int) =
    assertTrue(
        mDevice.findObject(
            UiSelector()
                .resourceId("$packageName:id/top_site_item")
                .index(position - 1),
        ).getChild(
            UiSelector()
                .resourceId("$packageName:id/top_site_subtitle"),
        ).waitForExists(waitingTime),
    )

private fun assertSponsoredShortcutTitle(sponsoredShortcutTitle: String, position: Int) =
    assertTrue(
        mDevice.findObject(
            UiSelector()
                .resourceId("$packageName:id/top_site_item")
                .index(position - 1),
        ).getChild(
            UiSelector()
                .textContains(sponsoredShortcutTitle),
        ).waitForExists(waitingTime),
    )

private fun assertNotExistingTopSitesList(title: String) {
    mDevice.findObject(UiSelector().text(title)).waitUntilGone(waitingTime)

    assertFalse(
        mDevice.findObject(
            UiSelector()
                .resourceId("$packageName:id/top_site_title")
                .textContains(title),
        ).waitForExists(waitingTimeShort),
    )
}

private fun assertSponsoredTopSitesNotDisplayed() {
    assertFalse(
        mDevice.findObject(
            UiSelector()
                .resourceId("$packageName:id/top_site_subtitle")
                .textContains(getStringResource(R.string.top_sites_sponsored_label)),
        ).waitForExists(waitingTimeShort),
    )
}

private fun assertTopSiteContextMenuItems() {
    mDevice.waitNotNull(
        findObject(By.text("Open in private tab")),
        waitingTime,
    )
    mDevice.waitNotNull(
        findObject(By.text("Remove")),
        waitingTime,
    )
}

private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort))

private fun assertJumpBackInItemTitle(testRule: ComposeTestRule, itemTitle: String) =
    testRule.onNodeWithTag("recent.tab.title", useUnmergedTree = true).assert(hasText(itemTitle))

private fun assertJumpBackInItemWithUrl(testRule: ComposeTestRule, itemUrl: String) =
    testRule.onNodeWithTag("recent.tab.url", useUnmergedTree = true).assert(hasText(itemUrl))

private fun assertJumpBackInShowAllButton() =
    assertTrue(
        mDevice
            .findObject(
                UiSelector()
                    .textContains(getStringResource(R.string.recent_tabs_show_all)),
            ).waitForExists(waitingTime),
    )

private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime))

private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTimeShort))

private fun assertRecentBookmarksSectionIsDisplayed() =
    assertTrue(recentBookmarksSection().waitForExists(waitingTime))

private fun assertRecentBookmarksSectionIsNotDisplayed() =
    assertFalse(recentBookmarksSection().waitForExists(waitingTimeShort))

private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitForExists(waitingTime))

private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTimeShort))

private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))

private fun tabsCounter() = onView(withId(R.id.tab_button))

private fun jumpBackInSection() =
    mDevice.findObject(UiSelector().textContains(getStringResource(R.string.recent_tabs_header)))

private fun recentlyVisitedSection() =
    mDevice.findObject(UiSelector().textContains(getStringResource(R.string.history_metadata_header_2)))

private fun recentBookmarksSection() =
    mDevice.findObject(UiSelector().textContains(getStringResource(R.string.recently_saved_title)))

private fun pocketSection() =
    mDevice.findObject(UiSelector().textContains(getStringResource(R.string.pocket_stories_header_1)))

private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
    mDevice.findObject(
        By
            .res("$packageName:id/top_site_title")
            .textContains(sponsoredShortcutTitle),
    )

private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
    composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)

private fun privacyProtectionDescription(settings: Settings): UiObject {
    val isTCPPublic = settings.enabledTotalCookieProtectionCFR
    val descriptionText = when (isTCPPublic) {
        true -> R.string.onboarding_tracking_protection_description
        false -> R.string.onboarding_tracking_protection_description_old
    }

    return itemContainingText(getStringResource(descriptionText))
}

private val homeScreen =
    itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton =
    itemWithResId("$packageName:id/privateBrowsingButton")
private val homepageWordmark =
    itemWithResId("$packageName:id/wordmark")
private val welcomeHeader = itemContainingText(getStringResource(R.string.onboarding_header_2))
private val welcomeSubHeader =
    itemContainingText(getStringResource(R.string.onboarding_message))
private val chooseThemeHeader =
    itemContainingText(getStringResource(R.string.onboarding_theme_picker_header))
private val chooseThemeText =
    itemContainingText(getStringResource(R.string.onboarding_theme_picker_description_2))
private val darkThemeDescription =
    itemContainingText(getStringResource(R.string.onboarding_theme_dark_title))
private val lightThemeDescription =
    itemContainingText(getStringResource(R.string.onboarding_theme_light_title))
private val automaticThemeDescription =
    itemWithResIdAndDescription(
        "$packageName:id/clickable_region_automatic",
        "${getStringResource(R.string.onboarding_theme_automatic_title)} ${getStringResource(R.string.onboarding_theme_automatic_summary)}",
    )
private fun darkThemeToggle(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/theme_dark_radio_button", isChecked)
private fun lightThemeToggle(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/theme_light_radio_button", isChecked)
private fun automaticThemeToggle(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/theme_automatic_radio_button", isChecked)
private val toolbarPlacementHeader =
    itemContainingText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
private val toolbarPlacementDescription =
    itemContainingText(getStringResource(R.string.onboarding_toolbar_placement_description))
private fun toolbarPlacementBottomRadioButton(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/toolbar_bottom_radio_button", isChecked)
private fun toolbarPlacementTopRadioButton(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/toolbar_top_radio_button", isChecked)
private val toolbarPlacementBottomImage =
    itemWithResId("$packageName:id/toolbar_bottom_image")
private val toolbarPlacementBottomTitle =
    itemWithResId("$packageName:id/toolbar_bottom_title")
private val toolbarPlacementTopTitle =
    itemWithResId("$packageName:id/toolbar_top_title")
private val toolbarPlacementTopImage =
    itemWithResId("$packageName:id/toolbar_top_image")
private val startSyncHeader =
    itemContainingText(getStringResource(R.string.onboarding_account_sign_in_header))
private val startSyncDescription =
    itemContainingText(getStringResource(R.string.onboarding_manual_sign_in_description))
private val signInButton =
    itemWithResId("$packageName:id/fxa_sign_in_button")
private val privacyProtectionHeader =
    itemContainingText(getStringResource(R.string.onboarding_tracking_protection_header))
private fun standardTrackingProtectionToggle(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/tracking_protection_standard_option", isChecked)
private fun strictTrackingProtectionToggle(isChecked: Boolean) =
    checkedItemWithResId("$packageName:id/tracking_protection_strict_default", isChecked)
private val privacyNoticeHeader =
    itemContainingText(getStringResource(R.string.onboarding_privacy_notice_header_1))
private val privacyNoticeDescription =
    itemContainingText(getStringResource(R.string.onboarding_privacy_notice_description))
private val privacyNoticeButton =
    itemWithResId("$packageName:id/read_button")
private val startBrowsingButton =
    itemWithResId("$packageName:id/finish_button")
private val conclusionHeader =
    itemContainingText(getStringResource(R.string.onboarding_conclusion_header))
private val navigationToolbar =
    itemWithResId("$packageName:id/toolbar")
private val menuButton =
    itemWithResId("$packageName:id/menuButton")
private fun tabCounter(numberOfOpenTabs: String) =
    itemWithResIdAndText("$packageName:id/counter_text", numberOfOpenTabs)

val deleteFromHistory =
    onView(
        allOf(
            withId(R.id.simple_text),
            withText(R.string.delete_from_history),
        ),
    ).inRoot(RootMatchers.isPlatformPopup())

private val recentlyVisitedList =
    UiScrollable(
        UiSelector()
            .className("android.widget.HorizontalScrollView"),
    ).setAsHorizontalList()

private val sponsoredShortcutsSettingsButton =
    mDevice
        .findObject(
            UiSelector()
                .textContains(getStringResource(R.string.top_sites_menu_settings))
                .resourceId("$packageName:id/simple_text"),
        )

private val sponsorsAndPrivacyButton =
    mDevice
        .findObject(
            UiSelector()
                .textContains(getStringResource(R.string.top_sites_menu_sponsor_privacy))
                .resourceId("$packageName:id/simple_text"),
        )
