package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.core.cardsOf
import com.jsjdesigns.poker.game.Action
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.DecisionOffer
import com.jsjdesigns.poker.game.Street
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 DecisionCoachTest {

    private fun offer(
        street: Street = Street.RIVER,
        hole: String = "Ah Qh",
        board: String = "2c 7d 9s Jc 3h",
        pot: Int = 80,
        toCall: Int = 20,
        canCheck: Boolean = false,
        canRaise: Boolean = true,
        activeOpponents: Int = 1,
        stack: Int = 200,
        eligiblePot: Int = pot,
        currentBet: Int = if (street == Street.PREFLOP) 2 else 0,
    ) = DecisionOffer(
        token = 91,
        handNumber = 7,
        street = street,
        seat = 0,
        hole = cardsOf(hole).toList(),
        board = cardsOf(board).toList(),
        pot = pot,
        toCall = toCall,
        minRaiseTo = 60,
        maxRaiseTo = 200,
        stack = stack,
        canCheck = canCheck,
        canRaise = canRaise,
        activeOpponents = activeOpponents,
        seatsActingAfter = 1,
        bigBlind = 2,
        eligiblePot = eligiblePot,
        currentBet = currentBet,
    )

    @Test
    fun `same public decision always produces the same review`() {
        val coach = DecisionCoach(equityIterations = 600)
        val decision = offer()
        val chosen = Action(ActionType.FOLD)

        assertEquals(coach.review(decision, chosen), coach.review(decision, chosen))
    }

    @Test
    fun `postflop trace is honest about equity and raw pot odds`() {
        val review = DecisionCoach(equityIterations = 800).review(
            offer(pot = 80, toCall = 20),
            Action(ActionType.CALL, 20),
        )
        val trace = review.trace

        assertNotNull(trace.estimatedEquity)
        assertEquals(0.20, trace.breakEvenEquity!!, 1e-12)
        assertEquals(trace.breakEvenEquity, trace.decisionThreshold)
        assertEquals("80:20", trace.potOdds)
        assertNull(trace.handStrengthPercentile)
        assertTrue("unknown random hand" in trace.reason)
        assertFalse(trace.mistakeApplied, "human disagreement is not a bot skill error")
    }

    @Test
    fun `preflop review uses chart fields and never fabricates equity`() {
        val review = DecisionCoach().review(
            offer(street = Street.PREFLOP, hole = "As Ad", board = ""),
            Action(ActionType.RAISE, 6),
        )
        val trace = review.trace

        assertNull(trace.estimatedEquity)
        assertNotNull(trace.handStrengthPercentile)
        assertNotNull(trace.preflopRangeThreshold)
        assertNull(trace.breakEvenEquity)
        assertNull(trace.decisionThreshold)
        assertNull(trace.potOdds)
    }

    @Test
    fun `big blind option is described as a raise rather than a bet`() {
        val review = DecisionCoach().review(
            offer(
                street = Street.PREFLOP,
                hole = "As Ad",
                board = "",
                toCall = 0,
                canCheck = true,
            ),
            Action(ActionType.RAISE, 60),
        )

        assertEquals(ActionType.RAISE, review.trace.intended.type)
        assertTrue(review.alignedWithBaseline)
    }

    @Test
    fun `preflop raise is identified by table bet level rather than amount owed`() {
        val coach = DecisionCoach()
        val unopened = coach.review(
            offer(street = Street.PREFLOP, board = "", toCall = 2, currentBet = 2),
            Action(ActionType.FOLD),
        )
        val facingMinimumRaise = coach.review(
            // The big blind has 2 invested, so a raise to 4 leaves only 2 owed.
            offer(street = Street.PREFLOP, board = "", toCall = 2, currentBet = 4),
            Action(ActionType.FOLD),
        )

        assertEquals(
            unopened.trace.preflopRangeThreshold!! * 0.45,
            facingMinimumRaise.trace.preflopRangeThreshold!!,
            1e-12,
        )
    }

    @Test
    fun `bet and raise spellings are the same aggressive action family`() {
        val review = DecisionCoach(equityIterations = 300).review(
            offer(
                hole = "As Ad",
                board = "Ah 7d 9s Jc 3h",
                toCall = 0,
                canCheck = true,
            ),
            // The app sends RAISE; Table sanitises it to BET when no bet exists.
            Action(ActionType.RAISE, 60),
        )

        assertEquals(ActionType.BET, review.trace.intended.type)
        assertTrue(review.alignedWithBaseline)
    }

    @Test
    fun `short all-in odds exclude chips in side pots the hero cannot win`() {
        val review = DecisionCoach(equityIterations = 300).review(
            offer(
                pot = 120,
                eligiblePot = 40,
                toCall = 100,
                stack = 20,
                canRaise = false,
            ),
            Action(ActionType.CALL, 20),
        )

        assertEquals(1.0 / 3.0, review.trace.breakEvenEquity!!, 1e-12)
        assertEquals("40:20", review.trace.potOdds)
    }
}
