package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.core.cardsOf
import com.jsjdesigns.poker.game.DecisionContext
import com.jsjdesigns.poker.game.Seat
import com.jsjdesigns.poker.game.Street
import kotlinx.coroutines.test.runTest
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class DecisionTraceTest {

    private fun context(bot: MathBot, street: Street, board: String): DecisionContext {
        val seat = Seat(0, "Expert", 200, bot)
        seat.hole = cardsOf("Ah Qh")
        return DecisionContext(
            street = street,
            seat = seat,
            board = cardsOf(board),
            pot = 80,
            toCall = 20,
            minRaiseTo = 60,
            maxRaiseTo = 200,
            activeOpponents = 1,
            seatsActingAfter = 1,
            bigBlind = 2,
            history = emptyList(),
            handNumber = 1,
            decisionToken = 1,
            bettingReopened = true,
            eligiblePot = 80,
            currentBet = if (street == Street.PREFLOP) 2 else 20,
        )
    }

    @Test
    fun `river does not receive a fictional implied-odds discount`() = runTest {
        val profile = BotProfile("E", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE)
        val flopBot = MathBot(profile, Random(77))
        val riverBot = MathBot(profile, Random(77))

        flopBot.act(context(flopBot, Street.FLOP, "2c 7d 9s"))
        riverBot.act(context(riverBot, Street.RIVER, "2c 7d 9s Jc 3h"))

        val flop = flopBot.lastTrace!!
        val river = riverBot.lastTrace!!
        assertEquals(
            requireNotNull(flop.decisionThreshold),
            requireNotNull(river.decisionThreshold),
            1e-12,
            "street alone must not apply a blanket discount to pot odds",
        )
        assertFalse(river.adjustments.any { "implied" in it.name.lowercase() })
    }

    @Test
    fun `trace records the rule that actually produced the action`() = runTest {
        val bot = MathBot(
            BotProfile("B", SkillLevel.BEGINNER, PlayStyle.TIGHT_AGGRESSIVE),
            Random(11),
        )
        bot.act(context(bot, Street.RIVER, "2c 7d 9s Jc 3h"))

        val trace = bot.lastTrace!!
        assertEquals(trace.intended != trace.chosen, trace.mistakeApplied)
        assertTrue(requireNotNull(trace.decisionThreshold) >= 0.0)
        assertNotNull(trace.estimatedEquity)
        assertNull(trace.handStrengthPercentile)
        assertNull(trace.preflopRangeThreshold)
        assertTrue(trace.adjustments.isNotEmpty())
        assertTrue("raw pot odds require" in trace.reason)
        assertTrue("adjusted decision threshold" in trace.reason)
    }

    @Test
    fun `preflop trace reports a percentile and never calls it equity`() = runTest {
        val bot = MathBot(
            BotProfile("E", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE),
            Random(17),
        )
        bot.act(context(bot, Street.PREFLOP, ""))

        val trace = bot.lastTrace!!
        assertNull(trace.estimatedEquity, "pre-flop does not run an equity simulation")
        assertNotNull(trace.handStrengthPercentile)
        assertTrue(trace.handStrengthPercentile in 0.0..1.0)
        assertNotNull(trace.preflopRangeThreshold)
        assertTrue(trace.preflopRangeThreshold in 0.0..1.0)
        assertNull(trace.breakEvenEquity, "pre-flop strategy is chart-based, not pot-odds based")
        assertNull(trace.decisionThreshold)
        assertNull(trace.potOdds)
    }

    @Test
    fun `position factors average to one at every table size`() {
        for (players in listOf(2, 4, 6, 9)) {
            for (inPositionDelta in listOf(0.45, -0.12)) {
                val inPosition = balancedPositionFactor(
                    activeOpponents = players - 1,
                    inPosition = true,
                    awareness = 1.0,
                    inPositionDelta = inPositionDelta,
                )
                val outOfPosition = balancedPositionFactor(
                    activeOpponents = players - 1,
                    inPosition = false,
                    awareness = 1.0,
                    inPositionDelta = inPositionDelta,
                )
                val weighted = (inPosition + (players - 1) * outOfPosition) / players
                assertEquals(
                    1.0,
                    weighted,
                    1e-12,
                    "$players-handed position adjustment must not alter average volume",
                )
            }
        }
    }

    @Test
    fun `bot pot odds exclude inaccessible side-pot chips`() = runTest {
        val bot = MathBot(
            BotProfile("E", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE),
            Random(19),
        )
        val seat = Seat(0, "Expert", 20, bot)
        seat.hole = cardsOf("As Ad")
        val ctx = DecisionContext(
            street = Street.RIVER,
            seat = seat,
            board = cardsOf("Ah 7d 9s Jc 3h"),
            pot = 120,
            toCall = 100,
            minRaiseTo = 220,
            maxRaiseTo = 20,
            activeOpponents = 2,
            seatsActingAfter = 1,
            bigBlind = 2,
            history = emptyList(),
            handNumber = 1,
            decisionToken = 1,
            bettingReopened = false,
            eligiblePot = 40,
            currentBet = 100,
        )

        bot.act(ctx)

        assertEquals(1.0 / 3.0, bot.lastTrace!!.breakEvenEquity!!, 1e-12)
        assertEquals("40:20", bot.lastTrace!!.potOdds)
        assertEquals(com.jsjdesigns.poker.game.ActionType.CALL, bot.lastTrace!!.intended.type)
        assertEquals(20, bot.lastTrace!!.intended.amount)
    }
}
