/* 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.os.Build
import android.widget.TextView
import androidx.core.content.pm.PackageInfoCompat
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewAssertion
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.isVisibleForUser
import org.mozilla.fenix.settings.SupportUtils
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.Calendar
import java.util.Date

/**
 * Implementation of Robot Pattern for the settings search sub menu.
 */
class SettingsSubMenuAboutRobot {

    fun verifyAboutFirefoxPreview() = assertFirefoxPreviewPage()

    class Transition {
        val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

        fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
            goBackButton().perform(click())

            SettingsRobot().interact()
            return SettingsRobot.Transition()
        }
    }
}

private fun assertFirefoxPreviewPage() {
    assertVersionNumber()
    assertProductCompany()
    assertCurrentTimestamp()
    verifyListElements()
}

private fun navigateBackToAboutPage(itemToInteract: () -> Unit) {
    browserScreen {
    }.openTabDrawer {
        closeTab()
    }

    homeScreen {
    }.openThreeDotMenu {
    }.openSettings {
    }.openAboutFirefoxPreview {
        itemToInteract()
    }
}

private fun verifyListElements() {
    assertWhatIsNewInFirefoxPreview()
    navigateBackToAboutPage(::assertSupport)
    navigateBackToAboutPage(::assertPrivacyNotice)
    navigateBackToAboutPage(::assertKnowYourRights)
    navigateBackToAboutPage(::assertLicensingInformation)
    navigateBackToAboutPage(::assertLibrariesUsed)
}

private fun assertVersionNumber() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
    val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()

    val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n"
    val componentsVersion =
        "${mozilla.components.Build.version}, ${mozilla.components.Build.gitHash}"
    val geckoVersion =
        org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION + "-" + org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
    val asVersion = mozilla.components.Build.applicationServicesVersion

    onView(withId(R.id.about_text))
        .check(matches(withText(containsString(buildNVersion))))
        .check(matches(withText(containsString(componentsVersion))))
        .check(matches(withText(containsString(geckoVersion))))
        .check(matches(withText(containsString(asVersion))))
}

private fun assertProductCompany() {
    onView(withId(R.id.about_content))
        .check(matches(withText(containsString("Firefox Preview is produced by Mozilla."))))
}

private fun assertCurrentTimestamp() {
    onView(withId(R.id.build_date))
        // Currently UI tests run against debug builds, which display a hard-coded string 'debug build'
        // instead of the date. See https://github.com/mozilla-mobile/fenix/pull/10812#issuecomment-633746833
        .check(matches(withText(containsString("debug build"))))
        // This assertion should be valid for non-debug build types.
        // .check(BuildDateAssertion.isDisplayedDateAccurate())
}

private fun assertWhatIsNewInFirefoxPreview() {

    if (!onView(withText("What’s new in Firefox Preview")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("What’s new in Firefox Preview"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    // Commenting out since the Text to verify in the web site seems to be different now
    /*
    TestHelper.verifyUrl(
         SupportUtils.SumoTopic.WHATS_NEW.topicStr,
         "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
         R.id.mozac_browser_toolbar_url_view
    )*/

    Espresso.pressBack()
}

private fun assertSupport() {
    if (!onView(withText("Support")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("Support"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    TestHelper.verifyUrl(
        "support.mozilla.org",
        "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
        R.id.mozac_browser_toolbar_url_view
    )

    Espresso.pressBack()
}

private fun assertPrivacyNotice() {
    if (!onView(withText("Privacy notice")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("Privacy notice"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    TestHelper.verifyUrl(
        "/privacy/firefox",
        "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
        R.id.mozac_browser_toolbar_url_view
    )

    Espresso.pressBack()
}

private fun assertKnowYourRights() {
    if (!onView(withText("Know your rights")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("Know your rights"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    TestHelper.verifyUrl(
        SupportUtils.SumoTopic.YOUR_RIGHTS.topicStr,
        "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
        R.id.mozac_browser_toolbar_url_view
    )

    Espresso.pressBack()
}

private fun assertLicensingInformation() {
    if (!onView(withText("Licensing information")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("Licensing information"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    TestHelper.verifyUrl(
        "about:license",
        "org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
        R.id.mozac_browser_toolbar_url_view
    )

    Espresso.pressBack()
}

private fun assertLibrariesUsed() {
    if (!onView(withText("Libraries that we use")).isVisibleForUser()) {
        onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
    }

    onView(withText("Libraries that we use"))
        .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .perform(click())

    onView(withId(R.id.action_bar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries")))))
    Espresso.pressBack()
}

private fun goBackButton() =
    onView(CoreMatchers.allOf(withContentDescription("Navigate up")))

class BuildDateAssertion {
    // When the app is built on firebase, there are times where the BuildDate is off by a few seconds or a few minutes.
    // To compensate for that slight discrepancy, this assertion was added to see if the Build Date shown
    // is within a reasonable amount of time from when the app was built.
    companion object {
        // this pattern represents the following date format: "Monday 12/30 @ 6:49 PM"
        private const val DATE_PATTERN = "EEEE M/d @ h:m a"
        //
        private const val NUM_OF_HOURS = 1

        fun isDisplayedDateAccurate(): ViewAssertion {
            return ViewAssertion { view, noViewFoundException ->
                if (noViewFoundException != null) throw noViewFoundException

                val textFromView = (view as TextView).text
                    ?: throw AssertionError("This view is not of type TextView")

                verifyDateIsWithinRange(textFromView.toString(), NUM_OF_HOURS)
            }
        }

        private fun verifyDateIsWithinRange(dateText: String, hours: Int) {
            // This assertion checks whether has defined a range of tim
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
                val simpleDateFormat = SimpleDateFormat(DATE_PATTERN)
                val date = simpleDateFormat.parse(dateText)
                if (date == null || !date.isWithinRangeOf(hours)) {
                    throw AssertionError("The build date is not within Range.")
                }
            } else {
                val textviewDate = getLocalDateTimeFromString(dateText)
                val buildConfigDate = getLocalDateTimeFromString(BuildConfig.BUILD_DATE)

                if (!buildConfigDate.isEqual(textviewDate) &&
                    !textviewDate.isWithinRangeOf(hours, buildConfigDate)
                ) {
                    throw AssertionError("$textviewDate is not equal to the date within the build config: $buildConfigDate, and are not within a reasonable amount of time from each other.")
                }
            }
        }

        private fun Date.isWithinRangeOf(hours: Int): Boolean {
            // To determine the date range, the maxDate is retrieved by adding the variable hours to the calendar.
            // Since the calendar will represent the maxDate at this time, to retrieve the minDate the variable hours is multipled by negative 2 and added to the calendar
            // This will result in the maxDate being equal to the original Date + hours, and minDate being equal to original Date - hours

            val calendar = Calendar.getInstance()
            val currentYear = calendar.get(Calendar.YEAR)
            calendar.time = this
            calendar.set(Calendar.YEAR, currentYear)
            val updatedDate = calendar.time

            calendar.add(Calendar.HOUR_OF_DAY, hours)
            val maxDate = calendar.time
            calendar.add(
                Calendar.HOUR_OF_DAY,
                hours * -2
            ) // Gets the minDate by subtracting from maxDate
            val minDate = calendar.time
            return updatedDate.after(minDate) && updatedDate.before(maxDate)
        }

        private fun LocalDateTime.isWithinRangeOf(
            hours: Int,
            baselineDate: LocalDateTime
        ): Boolean {
            val upperBound = baselineDate.plusHours(hours.toLong())
            val lowerBound = baselineDate.minusHours(hours.toLong())
            val currentDate = this
            return currentDate.isAfter(lowerBound) && currentDate.isBefore(upperBound)
        }

        private fun getLocalDateTimeFromString(buildDate: String): LocalDateTime {
            val dateFormatter = DateTimeFormatterBuilder().appendPattern(DATE_PATTERN)
                .parseDefaulting(ChronoField.YEAR, LocalDateTime.now().year.toLong())
                .toFormatter()
            return LocalDateTime.parse(buildDate, dateFormatter)
        }
    }
}
