/*
 * This file is part of LibEuFin.
 * Copyright (C) 2023-2025 Taler Systems S.A.

 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.

 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.

 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

import io.ktor.http.*
import kotlinx.coroutines.*
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.bank.db.AccountDAO.AccountCreationResult
import tech.libeufin.bank.db.TanDAO.*
import tech.libeufin.common.*
import tech.libeufin.common.assertOk
import tech.libeufin.common.db.*
import tech.libeufin.common.json
import tech.libeufin.common.test.*
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNull

class DatabaseTest {
    
    // Testing the helper that creates the admin account.
    @Test
    fun createAdmin() = setup { db, ctx ->
        // Create admin account
        assertIs<AccountCreationResult.Success>(createAdminAccount(db, ctx))
        // Checking idempotency
        assertEquals(AccountCreationResult.UsernameReuse, createAdminAccount(db, ctx))
    }

    @Test
    fun tanChallenge() = bankSetup { db -> db.conn { conn ->
        val validityPeriod = Duration.ofHours(1)
        val retransmissionPeriod: Duration = Duration.ofMinutes(1)
        val retryCounter = 3

        suspend fun create(code: String, timestamp: Instant): UUID {
            return db.tan.new(
                hbody = Base32Crockford64B.rand(),
                salt = Base32Crockford16B.rand(),
                username = "customer", 
                op = Operation.withdrawal,
                code = code,
                timestamp = timestamp,
                retryCounter = retryCounter,
                validityPeriod = validityPeriod,
                tanChannel = TanChannel.sms,
                tanInfo = "+88"
            )
        }

        suspend fun markSent(id: UUID, timestamp: Instant) {
            db.tan.markSent(id, timestamp + retransmissionPeriod)
        }

        suspend fun send(id: UUID, code: String, timestamp: Instant): String? {
            return (db.tan.send(
                id,
                timestamp,
                10
            ) as? TanSendResult.Send)?.tanCode
        }
        
        val now = Instant.now()
        val expired = now + validityPeriod
        val retransmit = now + retransmissionPeriod

        // Check basic
        create("good-code", now).run {
            // Bad code
            assertEquals(TanSolveResult.BadCode, db.tan.solve(this, "bad-code", now))
            // Good code
            assertIs<TanSolveResult.Success>(db.tan.solve(this, "good-code", now))
            // Never resend a confirmed challenge
            assertEquals(TanSendResult.Solved, db.tan.send(this, now, 10))
            // Confirmed challenge always ok
            assertIs<TanSolveResult.Success>(db.tan.solve(this, "good-code", now))
        }

        // Check retry
        create("good-code", now).run {
            markSent(this, now)
            // Bad code
            repeat(retryCounter-1) {
                assertEquals(TanSolveResult.BadCode, db.tan.solve(this, "bad-code", now))
            }
            assertEquals(TanSolveResult.NoRetry, db.tan.solve(this, "bad-code", now))
            // Good code fail
            assertEquals(TanSolveResult.NoRetry, db.tan.solve(this, "good-code", now))
            // New code
            assertIs<TanSendResult.Success>(db.tan.send(this, now, 10))
        }

        // Check retransmission
        create("good-code", now).run {
            // Failed to send retransmit
            assertIs<TanSendResult.Send>(db.tan.send(this, now, 10))
            // Code successfully sent and still valid
            markSent(this, now)
            assertIs<TanSendResult.Success>(db.tan.send(this, now, 10))
            // Code is still valid but should be resent
            assertIs<TanSendResult.Send>(db.tan.send(this, retransmit, 10))
            // Good code fail because expired
            assertEquals(TanSolveResult.Expired, db.tan.solve(this, "good-code", expired))
            // No code because expired
            assertIs<TanSendResult.Send>(db.tan.send(this, retransmit, 10))
        }
    }}
}