package com.github.nullptroma.wallenc.usecases

import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.Duration
import java.time.Instant
import javax.crypto.spec.SecretKeySpec

class TwoFaTotpTest {

    @Test
    fun buildTwoFaCodeStateMatchesJavaOtpForKnownSecret() {
        val secretBase32 = "JBSWY3DPEHPK3PXP"
        val token = TwoFaTokenRecord(
            id = "1",
            issuer = "Test",
            account = "user",
            secret = secretBase32,
            digits = 6,
            periodSeconds = 30,
            algorithm = "SHA1",
            notes = null,
        )
        val nowMillis = 1_700_000_000_000L
        val state = buildTwoFaCodeState(token, nowMillis)
        assertNotNull(state)
        val expected = generateReferenceTotp(secretBase32, nowMillis, 6, 30, "HmacSHA1")
        assertEquals(expected, state!!.code)
        assertTrue(state.secondsUntilRefresh in 1..30)
    }

    @Test
    fun totpPeriodProgressIsContinuousWithinPeriod() {
        val period = 30
        val periodStartMillis = 1_700_000_100_000L // epoch sec кратна period
        assertEquals(0f, totpPeriodProgress(periodStartMillis, period), 0.001f)
        assertEquals(0.5f, totpPeriodProgress(periodStartMillis + 15_000L, period), 0.001f)
        assertEquals(29f / 30f, totpPeriodProgress(periodStartMillis + 29_000L, period), 0.001f)
    }

    @Test
    fun totpSecondsUntilRefreshCountsDownWithinPeriod() {
        val period = 30
        val t0 = 1_700_000_100_000L
        assertEquals(30, totpSecondsUntilRefresh(t0, period))
        assertEquals(15, totpSecondsUntilRefresh(t0 + 15_000L, period))
        assertEquals(1, totpSecondsUntilRefresh(t0 + 29_000L, period))
    }

    @Test
    fun buildTwoFaCodeStateReturnsNullForInvalidSecret() {
        val token = TwoFaTokenRecord(
            id = "1",
            issuer = "Test",
            account = "user",
            secret = "!!!not-base32!!!",
            digits = 6,
            periodSeconds = 30,
            algorithm = "SHA1",
            notes = null,
        )
        assertEquals(null, buildTwoFaCodeState(token, System.currentTimeMillis()))
    }

    private fun generateReferenceTotp(
        secretBase32: String,
        nowMillis: Long,
        digits: Int,
        periodSeconds: Int,
        macAlgorithm: String,
    ): String {
        val key = decodeBase32(secretBase32)
        val generator = TimeBasedOneTimePasswordGenerator(
            Duration.ofSeconds(periodSeconds.toLong()),
            digits,
            macAlgorithm,
        )
        val otp = generator.generateOneTimePassword(
            SecretKeySpec(key, "RAW"),
            Instant.ofEpochMilli(nowMillis),
        )
        return otp.toString().padStart(digits, '0')
    }

    private fun decodeBase32(input: String): ByteArray {
        val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
        val clean = input.uppercase().replace(" ", "").replace("=", "")
        var buffer = 0
        var bitsLeft = 0
        val out = ArrayList<Byte>()
        for (ch in clean) {
            val value = alphabet.indexOf(ch)
            buffer = (buffer shl 5) or value
            bitsLeft += 5
            if (bitsLeft >= 8) {
                out.add(((buffer shr (bitsLeft - 8)) and 0xFF).toByte())
                bitsLeft -= 8
            }
        }
        return out.toByteArray()
    }
}
