package com.tt.honeyDue.security

import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ApplicationProvider
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue

/**
 * P6 Stream T — Robolectric tests for [BiometricManager].
 *
 * We don't exercise the real [androidx.biometric.BiometricPrompt] system
 * UI (can't show a prompt in a unit test). Instead we inject a fake
 * [BiometricManager.Prompter] and drive the
 * [BiometricPrompt.AuthenticationCallback] callbacks directly — this
 * verifies our wrapper's result-mapping / strike-counting contract
 * without needing on-device biometric hardware.
 */
@RunWith(RobolectricTestRunner::class)
class BiometricManagerTest {

    private lateinit var activity: FragmentActivity

    @Before
    fun setUp() {
        // ApplicationProvider gives us a Context; Robolectric can build a
        // fragment-capable activity controller for testing FragmentActivity.
        activity = Robolectric
            .buildActivity(FragmentActivity::class.java)
            .create()
            .get()
    }

    // ---------- 1. canAuthenticate surfaces NO_HARDWARE ----------

    @Test
    fun canAuthenticate_returnsNoHardware_whenProbeSaysSo() {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { _ -> BiometricManager.Prompter { /* no-op */ } },
            availabilityProbe = { BiometricManager.Availability.NO_HARDWARE },
        )

        assertEquals(BiometricManager.Availability.NO_HARDWARE, mgr.canAuthenticate())
    }

    @Test
    fun canAuthenticate_returnsAvailable_whenProbeSaysSo() {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { _ -> BiometricManager.Prompter { } },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        assertEquals(BiometricManager.Availability.AVAILABLE, mgr.canAuthenticate())
    }

    // ---------- 2. Success path ----------

    @Test
    fun authenticate_returnsSuccess_whenCallbackFiresSucceeded() = runTest {
        var capturedCallback: BiometricPrompt.AuthenticationCallback? = null
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                capturedCallback = callback
                BiometricManager.Prompter { _ ->
                    // Simulate user succeeding.
                    callback.onAuthenticationSucceeded(mockk(relaxed = true))
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock", subtitle = "Verify identity")

        assertEquals(BiometricManager.Result.Success, result)
        assertEquals(0, mgr.currentFailureCount(), "success resets strike counter")
        // Sanity — the factory was actually invoked with our callback.
        assertTrue(capturedCallback != null)
    }

    // ---------- 3. Three-strike lockout ----------

    @Test
    fun threeConsecutiveFailures_nextAuthenticateReturnsTooManyAttempts() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { _ ->
                // Prompter is irrelevant here: we pre-seed the strike count
                // to simulate three prior onAuthenticationFailed hits, then
                // attempt one more call — it must short-circuit WITHOUT
                // calling the prompter at all.
                BiometricManager.Prompter { throw AssertionError("prompt must not be shown") }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )
        mgr.seedFailures(3)

        val result = mgr.authenticate(title = "Unlock")

        assertEquals(BiometricManager.Result.TooManyAttempts, result)
    }

    @Test
    fun failureCounter_incrementsAcrossMultipleFailedCallbacks() = runTest {
        // Verifies that onAuthenticationFailed increments the internal
        // counter even though it doesn't resume the coroutine. We simulate
        // 3 failures followed by a terminal ERROR_USER_CANCELED so the
        // suspend call actually resolves.
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationFailed()
                    callback.onAuthenticationFailed()
                    callback.onAuthenticationFailed()
                    callback.onAuthenticationError(
                        BiometricPrompt.ERROR_USER_CANCELED, "User canceled"
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")

        assertEquals(BiometricManager.Result.UserCanceled, result)
        assertEquals(
            3, mgr.currentFailureCount(),
            "three onAuthenticationFailed events should bump strike count to 3",
        )

        // A follow-up call must now short-circuit to TooManyAttempts.
        val secondAttempt = mgr.authenticate(title = "Unlock")
        assertEquals(BiometricManager.Result.TooManyAttempts, secondAttempt)
    }

    // ---------- 4. USER_CANCELED maps to UserCanceled ----------

    @Test
    fun onAuthenticationError_userCanceled_mapsToUserCanceled() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationError(
                        BiometricPrompt.ERROR_USER_CANCELED,
                        "User canceled",
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")
        assertEquals(BiometricManager.Result.UserCanceled, result)
    }

    @Test
    fun onAuthenticationError_negativeButton_mapsToUserCanceled() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationError(
                        BiometricPrompt.ERROR_NEGATIVE_BUTTON,
                        "Cancel",
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")
        assertEquals(BiometricManager.Result.UserCanceled, result)
    }

    // ---------- 5. Hardware-absent error maps to NoHardware ----------

    @Test
    fun onAuthenticationError_hwNotPresent_mapsToNoHardware() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationError(
                        BiometricPrompt.ERROR_HW_NOT_PRESENT,
                        "No hardware",
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")
        assertEquals(BiometricManager.Result.NoHardware, result)
    }

    // ---------- 6. Lockout error maps to TooManyAttempts ----------

    @Test
    fun onAuthenticationError_lockout_mapsToTooManyAttemptsAndSaturatesCounter() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationError(
                        BiometricPrompt.ERROR_LOCKOUT,
                        "Too many attempts",
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")
        assertEquals(BiometricManager.Result.TooManyAttempts, result)
        assertEquals(BiometricManager.MAX_FAILURES, mgr.currentFailureCount())
    }

    // ---------- 7. Other errors map to Result.Error with code + message ----------

    @Test
    fun onAuthenticationError_unknownError_mapsToResultError() = runTest {
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    callback.onAuthenticationError(
                        /* code = */ 9999,
                        /* msg  = */ "Something went wrong",
                    )
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )

        val result = mgr.authenticate(title = "Unlock")
        val err = assertIs<BiometricManager.Result.Error>(result)
        assertEquals(9999, err.code)
        assertEquals("Something went wrong", err.message)
    }

    // ---------- 8. reset() clears the strike counter ----------

    @Test
    fun reset_clearsFailureCounter_allowsFuturePromptsAgain() = runTest {
        var promptsShown = 0
        val mgr = BiometricManager(
            activity = activity,
            promptFactory = { callback ->
                BiometricManager.Prompter { _ ->
                    promptsShown++
                    callback.onAuthenticationSucceeded(mockk(relaxed = true))
                }
            },
            availabilityProbe = { BiometricManager.Availability.AVAILABLE },
        )
        mgr.seedFailures(BiometricManager.MAX_FAILURES)

        // Before reset — locked out.
        assertEquals(BiometricManager.Result.TooManyAttempts, mgr.authenticate("Unlock"))
        assertEquals(0, promptsShown, "locked-out call must NOT show the prompt")

        mgr.reset()

        // After reset — prompt is allowed and resolves with success.
        val afterReset = mgr.authenticate("Unlock")
        assertEquals(BiometricManager.Result.Success, afterReset)
        assertEquals(1, promptsShown)
    }
}
