package dev.privacyllc.period.core.security

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The PIN is never stored, and the three outcomes are three for a reason.
 *
 * These run on the JVM in milliseconds because [MacProvider] is the only part
 * that needs hardware. The iteration counts here are deliberately tiny — the
 * cost of PBKDF2 is a product decision measured on a device, not something a
 * unit test should pay for on every run.
 */
class PinVerifierTest {

    private val cheap = 1_000
    private fun pin(s: String) = s.toCharArray()

    private fun verifier(macs: MacProvider = FakeMacProvider()) = PinVerifier(macs)

    @Test fun `the correct PIN verifies`() {
        val v = verifier()
        val record = v.enroll(pin("246813"), cheap)
        assertTrue(v.verify(pin("246813"), record) is PinCheck.Correct)
    }

    @Test fun `a wrong PIN does not`() {
        val v = verifier()
        val record = v.enroll(pin("246813"), cheap)
        assertEquals(PinCheck.Wrong, v.verify(pin("246814"), record))
    }

    /** The point of the whole design: what is written down is not the PIN. */
    @Test fun `the stored record contains neither the PIN nor anything derived from it alone`() {
        val record = verifier().enroll(pin("246813"), cheap)
        val encoded = record.encode()
        assertFalse("the PIN itself is in the record", encoded.contains("246813"))
        // A record made with the same PIN and a different salt must differ, or
        // the salt is not doing its job and two users with the same PIN would
        // store the same bytes.
        val again = verifier().enroll(pin("246813"), cheap)
        assertFalse("two enrolments produced identical records", record.mac.contentEquals(again.mac))
    }

    /**
     * Without the Keystore key the record is unusable — which is the property
     * that makes a six-digit PIN safe, since a million candidates is nothing to
     * an attacker who can compute the hash themselves.
     */
    @Test fun `a different key cannot verify a record made with another`() {
        val recorded = PinVerifier(FakeMacProvider(seed = 0x11)).enroll(pin("246813"), cheap)

        // The second key must actually exist, or this asserts KeyUnavailable —
        // a different and much weaker claim than "the wrong key gives the wrong
        // answer", which is the property being tested.
        val otherMacs = FakeMacProvider(seed = 0x22).apply { ensureKey() }

        assertEquals(PinCheck.Wrong, PinVerifier(otherMacs).verify(pin("246813"), recorded))
    }

    /**
     * The cruellest failure this feature could produce, and why it is its own
     * branch: a missing key means no PIN can ever match, and calling that
     * "wrong" would send someone into a growing backoff typing a PIN that was
     * always correct.
     */
    @Test fun `a missing key is reported as unavailable and never as a wrong PIN`() {
        val macs = FakeMacProvider()
        val v = PinVerifier(macs)
        val record = v.enroll(pin("246813"), cheap)

        macs.deleteKey()

        assertEquals(PinCheck.KeyUnavailable, v.verify(pin("246813"), record))
    }

    @Test fun `enrolling creates the key exactly once`() {
        val macs = FakeMacProvider()
        val v = PinVerifier(macs)
        v.enroll(pin("111111"), cheap)
        v.enroll(pin("222222"), cheap)
        assertEquals(2, macs.ensureCalls)
        assertTrue("ensureKey must be idempotent, not re-minting", macs.hasKey())
    }

    /**
     * Raising the cost is only possible in the instant after a correct PIN,
     * because that is the only time the PIN exists in memory.
     */
    @Test fun `a cheaper record is re-derived on the next successful unlock`() {
        val v = verifier()
        val old = v.enroll(pin("246813"), cheap)

        val outcome = v.verify(pin("246813"), old) as PinCheck.Correct

        assertNotNull("a below-target record should be rehashed", outcome.rehashed)
        assertEquals(PinVerifier.TARGET_ITERATIONS, outcome.rehashed!!.iterations)
        assertTrue("the new record must still verify", v.verify(pin("246813"), outcome.rehashed!!) is PinCheck.Correct)
    }

    @Test fun `a record already at target cost is not re-derived`() {
        val v = verifier()
        val current = v.enroll(pin("246813"), PinVerifier.TARGET_ITERATIONS)
        val outcome = v.verify(pin("246813"), current) as PinCheck.Correct
        assertNull(outcome.rehashed)
    }

    /**
     * The same Keystore key MACs the lockout counter under tag 0x02. Without
     * the leading tag, a value produced in one context could be presented as
     * the other.
     */
    @Test fun `the verifier tag differs from the backoff tag`() {
        assertFalse(PinVerifier.TAG_VERIFIER == PinVerifier.TAG_BACKOFF)

        val macs = FakeMacProvider().apply { ensureKey() }
        val payload = ByteArray(8) { it.toByte() }
        val asVerifier = macs.mac(byteArrayOf(PinVerifier.TAG_VERIFIER) + payload)
        val asBackoff = macs.mac(byteArrayOf(PinVerifier.TAG_BACKOFF) + payload)
        assertFalse("the same bytes MAC identically under both tags", asVerifier.contentEquals(asBackoff))
    }
}
