package com.jsjdesigns.poker.bot

import com.jsjdesigns.poker.game.Action
import com.jsjdesigns.poker.game.ActionType
import com.jsjdesigns.poker.game.HandEvent
import com.jsjdesigns.poker.game.Street
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class OpponentModelTest {

    private fun bet(seat: Int) =
        HandEvent(Street.FLOP, seat, "P$seat", Action(ActionType.BET, 10))

    private fun call(seat: Int) =
        HandEvent(Street.FLOP, seat, "P$seat", Action(ActionType.CALL, 10))

    /**
     * The regression Codex found: history is cleared each hand, so a size
     * comparison cannot detect a hand boundary. Consume 3 events in hand 1, then
     * first observe hand 2 when it already holds 4 — a `4 < 3` check is false, so
     * the first three events of hand 2 would be silently dropped.
     */
    @Test
    fun `events are not skipped when the next hand is first observed late`() {
        val model = OpponentModel()

        model.observe(listOf(bet(1), call(2), call(3)), handNumber = 1)
        assertEquals(1, model.actionsObserved(1))
        assertEquals(1, model.actionsObserved(2))
        assertEquals(1, model.actionsObserved(3))

        // New hand, already four events deep before we look.
        model.observe(listOf(bet(1), call(2), call(3), bet(1)), handNumber = 2)

        assertEquals(3, model.actionsObserved(1), "both bets in hand 2 plus hand 1's must count")
        assertEquals(2, model.actionsObserved(2), "hand 2's first events must not be skipped")
        assertEquals(2, model.actionsObserved(3), "hand 2's first events must not be skipped")
    }

    @Test
    fun `repeated observation within a hand is idempotent`() {
        val model = OpponentModel()
        val history = mutableListOf(bet(1))

        model.observe(history, handNumber = 1)
        model.observe(history, handNumber = 1)
        model.observe(history, handNumber = 1)
        assertEquals(1, model.actionsObserved(1), "re-observing must not double count")

        history += call(1)
        model.observe(history, handNumber = 1)
        assertEquals(2, model.actionsObserved(1), "growth within a hand is picked up")
    }

    @Test
    fun `aggression rate reflects bet and raise share once sampled`() {
        val model = OpponentModel()
        // 30 actions for seat 1: 24 bets, 6 calls.
        val history = ArrayList<HandEvent>()
        repeat(24) { history += bet(1) }
        repeat(6) { history += call(1) }
        model.observe(history, handNumber = 1)

        assertEquals(30, model.actionsObserved(1))
        assertEquals(0.8, model.aggressionRate(1), 0.0001)
    }

    @Test
    fun `unsampled opponents report a neutral read`() {
        val model = OpponentModel()
        model.observe(listOf(bet(1), bet(1)), handNumber = 1)
        assertEquals(
            OpponentModel.NEUTRAL_AGGRESSION, model.aggressionRate(1),
            "too few actions to form a read",
        )
        assertEquals(
            OpponentModel.NEUTRAL_AGGRESSION, model.aggressionRate(9),
            "never seen at all",
        )
        assertTrue(model.actionsObserved(9) == 0)
    }

    /**
     * This is only a broad sanity bound. The actual prior is validated by the
     * controlled simulator style calibration against a neutral TAG profile.
     */
    @Test
    fun `the neutral baseline reflects a realistic bet-raise share`() {
        assertTrue(
            OpponentModel.NEUTRAL_AGGRESSION in 0.15..0.35,
            "a plausible neutral bet/raise share, was ${OpponentModel.NEUTRAL_AGGRESSION}",
        )
    }
}
