/* 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/. */

package org.mozilla.fenix.helpers

import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.test.core.app.ApplicationProvider
import org.junit.rules.ExternalResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import java.util.Date
import kotlin.random.Random

private const val mockProviderName = LocationManager.GPS_PROVIDER

/**
 * Rule that sets up a mock location provider that can inject location samples
 * straight to the device that the test is running on.
 *
 * Credit to the mapbox team
 * https://github.com/mapbox/mapbox-navigation-android/blob/87fab7ea1152b29533ee121eaf6c05bc202adf02/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/MockLocationUpdatesRule.kt
 *
 */
class MockLocationUpdatesRule : ExternalResource() {
    private val appContext = (ApplicationProvider.getApplicationContext() as Context)
    val latitude = Random.nextDouble(-90.0, 90.0)
    val longitude = Random.nextDouble(-180.0, 180.0)

    private val locationManager: LocationManager by lazy {
        (appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager)
    }

    override fun before() {
        /* ADB command to enable the mock location setting on the device.
         * Will not be turned back off due to limitations on knowing its initial state.
         */
        mDevice.executeShellCommand(
            "appops set " +
                appContext.packageName +
                " android:mock_location allow",
        )

        // To mock locations we need a location provider, so we generate and set it here.
        try {
            locationManager.addTestProvider(
                mockProviderName,
                false,
                false,
                false,
                false,
                true,
                true,
                true,
                3,
                2,
            )
        } catch (ex: Exception) {
            // unstable
            Log.w("MockLocationUpdatesRule", "addTestProvider failed")
        }
        locationManager.setTestProviderEnabled(mockProviderName, true)
    }

    // Cleaning up the location provider after the test.
    override fun after() {
        locationManager.setTestProviderEnabled(mockProviderName, false)
        locationManager.removeTestProvider(mockProviderName)
    }

    /**
     * Generate a valid mock location data and set with the help of a test provider.
     *
     * @param modifyLocation optional callback for modifying the constructed location before setting it.
     */
    fun setMockLocation(modifyLocation: (Location.() -> Unit)? = null) {
        check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            "MockLocationUpdatesRule is supported only on Android devices " +
                "running version >= Build.VERSION_CODES.M"
        }

        val location = Location(mockProviderName)
        location.time = Date().time
        location.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
        location.accuracy = 5f
        location.altitude = 0.0
        location.bearing = 0f
        location.speed = 5f
        location.latitude = latitude
        location.longitude = longitude
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            location.verticalAccuracyMeters = 5f
            location.bearingAccuracyDegrees = 5f
            location.speedAccuracyMetersPerSecond = 5f
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            location.elapsedRealtimeUncertaintyNanos = 0.0
        }

        modifyLocation?.let {
            location.apply(it)
        }

        locationManager.setTestProviderLocation(mockProviderName, location)
    }
}
