package dev.privacyllc.period.core.security

import android.os.Build
import android.security.keystore.KeyInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.security.KeyStore
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory

/**
 * The half that cannot run on the JVM.
 *
 * `KeyStore.getInstance("AndroidKeyStore")` throws `AndroidKeyStore not found`
 * under Robolectric — verified, not assumed — so the key's real properties can
 * only be checked on a device. Everything else about the lock is covered by the
 * JVM tests in this module, which is why this file is short.
 *
 * Must be run on **`PeriodMinSdk26`** as well as a modern image: API 26 is where
 * `PBKDF2WithHmacSHA256` first exists, so it is the one algorithm choice in this
 * feature with no margin at all.
 */
@RunWith(AndroidJUnit4::class)
class KeystoreVerifierTest {

    private val alias = "period.applock.test.v1"
    private lateinit var macs: AndroidKeyStoreMacProvider

    private fun store(): KeyStore =
        KeyStore.getInstance(AndroidKeyStoreMacProvider.ANDROID_KEYSTORE).apply { load(null) }

    @Before fun setUp() {
        // Without this, a device that has already run the un-mutated build has
        // the key under this alias, ensureKey() returns early, and the test
        // passes over a spec it never checked.
        runCatching { store().deleteEntry(alias) }
        macs = AndroidKeyStoreMacProvider(alias)
    }

    @After fun tearDown() {
        runCatching { store().deleteEntry(alias) }
    }

    @Test fun theAlgorithmThisFeatureNeedsExistsAtThisApiLevel() {
        // API 26 exactly. A NoSuchAlgorithmException here would mean the lock
        // cannot be set on the oldest supported device, which is a shipped
        // feature that does not work rather than one that degrades.
        val factory = SecretKeyFactory.getInstance(PinVerifier.PBKDF2)
        assertEquals(PinVerifier.PBKDF2, factory.algorithm)
    }

    @Test fun theKeyIsCreatedOnceAndSurvivesReuse() {
        assertFalse(macs.hasKey())
        macs.ensureKey()
        assertTrue(macs.hasKey())

        val first = macs.mac("payload".toByteArray())

        // A second provider over the same alias is what a process restart looks
        // like from here.
        val reopened = AndroidKeyStoreMacProvider(alias)
        reopened.ensureKey()
        assertTrue(
            "the key must survive, or every stored record becomes unverifiable",
            first.contentEquals(reopened.mac("payload".toByteArray())),
        )
    }

    /**
     * The single most important assertion in the feature.
     *
     * A key bound to user authentication is destroyed when the device passcode
     * changes or the screen lock is removed. With no recovery path, that is the
     * user's entire history gone because they changed their phone's PIN.
     */
    @Test fun theKeyIsNotBoundToUserAuthentication() {
        macs.ensureKey()
        val key = store().getKey(alias, null) as SecretKey

        assertNull("an AndroidKeyStore secret key must never be exportable", key.encoded)

        val info = SecretKeyFactory
            .getInstance(key.algorithm, AndroidKeyStoreMacProvider.ANDROID_KEYSTORE)
            .getKeySpec(key, KeyInfo::class.java) as KeyInfo

        assertEquals(AndroidKeyStoreMacProvider.KEY_BITS, info.keySize)
        assertFalse(
            "a key requiring user authentication dies with a passcode change, " +
                "and under no-recovery that is the history gone",
            info.isUserAuthenticationRequired,
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            assertFalse(info.isInvalidatedByBiometricEnrollment)
        }
    }

    /** A missing key must take the "this device cannot check" branch, not "wrong PIN". */
    @Test fun aDeletedKeyIsReportedAsUnavailableRatherThanWrong() {
        val verifier = PinVerifier(macs)
        val record = verifier.enroll("2468".toCharArray(), iterations = 1_000)
        assertTrue(verifier.verify("2468".toCharArray(), record) is PinCheck.Correct)

        macs.deleteKey()

        assertEquals(PinCheck.KeyUnavailable, verifier.verify("2468".toCharArray(), record))
    }

    @Test fun theRealKeyProducesADifferentMacPerTag() {
        macs.ensureKey()
        val payload = ByteArray(8) { it.toByte() }
        assertFalse(
            macs.mac(byteArrayOf(PinVerifier.TAG_VERIFIER) + payload)
                .contentEquals(macs.mac(byteArrayOf(PinVerifier.TAG_BACKOFF) + payload)),
        )
    }
}
