package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.game.Action
import com.jsjdesigns.poker.game.ActionType
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.math.abs
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertTrue

/**
 * Profiles must play like the name on the tin.
 *
 * This is the regression that was missing: a "Rock" was running at 41% VPIP
 * against a 12% label for several rounds, because the only checks were bb/100
 * figures and nothing asserted that a style actually produced its own behaviour.
 * A gradient can look perfectly healthy while every profile is misnamed.
 */
class ProfileBehaviourTest {

    /** Deals random hands and measures how often this profile enters the pot. */
    private suspend fun measureVpip(profile: BotProfile, hands: Int = 4000): Double {
        val random = Random(20260725)
        val bot = MathBot(profile, Random(99))
        var entered = 0

        repeat(hands) {
            val a = random.nextInt(52)
            var b = random.nextInt(52)
            while (b == a) b = random.nextInt(52)

            val seat = Seat(0, "P", 200, bot)
            seat.hole = intArrayOf(a, b)
            val ctx = DecisionContext(
                street = Street.PREFLOP,
                seat = seat,
                board = IntArray(0),
                pot = 3,
                toCall = 2,
                minRaiseTo = 4,
                maxRaiseTo = 200,
                activeOpponents = 5,
                // Six-handed: one seat per orbit is last to act. A 50/50 split
                // materially over-weighted the in-position widening branch.
                seatsActingAfter = if (it % 6 == 0) 0 else 2,
                bigBlind = 2,
                history = emptyList(),
                handNumber = it + 1,
                decisionToken = (it + 1).toLong(),
                bettingReopened = true,
                eligiblePot = 3,
                currentBet = 2,
            )
            val action = bot.act(ctx)
            if (action.type != ActionType.FOLD) entered++
        }
        return entered.toDouble() / hands
    }

    private suspend fun assertPlaysLikeLabel(style: PlayStyle, skill: SkillLevel, tolerance: Double = 0.06) {
        val vpip = measureVpip(BotProfile(style.label, skill, style))
        assertTrue(
            abs(vpip - style.looseness) <= tolerance,
            "${skill.label} ${style.label}: looseness ${style.looseness} but entered " +
                "${"%.1f".format(vpip * 100)}% of hands — the label should mean something",
        )
    }

    @Test
    fun `a rock plays like a rock at every skill level`() = runTest {
        // The original failure: BEGINNER error rate dragged this to 41%.
        for (skill in SkillLevel.entries) {
            assertPlaysLikeLabel(PlayStyle.ROCK, skill)
        }
    }

    @Test
    fun `a maniac plays like a maniac at every skill level`() = runTest {
        for (skill in SkillLevel.entries) {
            assertPlaysLikeLabel(PlayStyle.MANIAC, skill)
        }
    }

    @Test
    fun `every style is recognisable at beginner skill`() = runTest {
        // Beginner has the highest error rate, so this is where style is most at
        // risk of being drowned out.
        for (style in PlayStyle.ALL) {
            assertPlaysLikeLabel(style, SkillLevel.BEGINNER)
        }
    }

    @Test
    fun `styles stay ordered by looseness regardless of skill`() = runTest {
        for (skill in listOf(SkillLevel.BEGINNER, SkillLevel.EXPERT)) {
            val measured = PlayStyle.ALL.map { it to measureVpip(BotProfile(it.label, skill, it)) }
            val byLabel = measured.sortedBy { it.first.looseness }.map { it.first.label }
            val byBehaviour = measured.sortedBy { it.second }.map { it.first.label }
            assertTrue(
                byLabel == byBehaviour,
                "${skill.label}: styles ordered by looseness $byLabel but behave as $byBehaviour",
            )
        }
    }

    @Test
    fun `skill does not silently widen a tight range`() = runTest {
        val beginner = measureVpip(BotProfile("R", SkillLevel.BEGINNER, PlayStyle.ROCK))
        val expert = measureVpip(BotProfile("R", SkillLevel.EXPERT, PlayStyle.ROCK))
        assertTrue(
            abs(beginner - expert) < 0.06,
            "a beginner rock (${"%.1f".format(beginner * 100)}%) and an expert rock " +
                "(${"%.1f".format(expert * 100)}%) should both be recognisably tight",
        )
    }
}
