package com.jsjdesigns.poker

import com.jsjdesigns.poker.core.HandEvaluator
import com.jsjdesigns.poker.core.cardsOf
import com.jsjdesigns.poker.game.HandResult
import com.jsjdesigns.poker.game.PotAward
import com.jsjdesigns.poker.game.PotPayout
import com.jsjdesigns.poker.game.ShowdownHand
import com.jsjdesigns.poker.game.Street
import com.jsjdesigns.poker.game.TableSnapshot
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

class HandSummaryTest {

    private fun result(
        net: IntArray = intArrayOf(-8, 8, 0),
        winners: List<Int> = listOf(1),
        showdown: Boolean = true,
        pot: Int = 16,
        awards: List<PotAward>? = null,
    ): HandResult {
        val board = cardsOf("2c 7d 9s Jc 3h")
        val holes = listOf(cardsOf("Ah Ad"), cardsOf("Kh Kd"), cardsOf("Qh Qd"))
        return HandResult(
            board = board,
            net = net,
            wentToShowdown = showdown,
            potSize = pot,
            events = emptyList(),
            showdownHands = if (showdown) {
                winners.map { winner ->
                    val hole = holes[winner]
                    ShowdownHand(
                        seat = winner,
                        hole = hole.toList(),
                        score = HandEvaluator.evaluate(hole + board),
                    )
                }
            } else {
                emptyList()
            },
            potAwards = awards ?: listOf(
                PotAward(
                    amount = pot,
                    payouts = winners.mapIndexed { index, winner ->
                        PotPayout(
                            winner,
                            pot / winners.size + if (index < pot % winners.size) 1 else 0,
                        )
                    },
                ),
            ),
        )
    }

    @Test
    fun `summary names the winner and reports the hero's actual net`() {
        val summary = handSummaryFor(
            handNumber = 7,
            result = result(),
            seatNames = listOf("You", "Ada", "Bruno"),
        )

        assertEquals("Ada wins", summary.title)
        assertEquals("Showdown  •  Pot 16  •  You lost 8", summary.detail)
        assertEquals(7, summary.handNumber)
        assertEquals(cardsOf("2c 7d 9s Jc 3h").toList(), summary.board)
    }

    @Test
    fun `hero is identified by seat when the player uses a custom name`() {
        val summary = handSummaryFor(
            handNumber = 3,
            result = result(
                net = intArrayOf(5, -2, -3),
                winners = listOf(0),
                showdown = false,
                pot = 6,
            ),
            seatNames = listOf("Jay", "Ada", "Bruno"),
        )

        assertEquals("You win", summary.title)
        assertTrue(summary.heroWon)
        assertEquals("Uncontested  •  Pot 6  •  You won 5", summary.detail)
    }

    @Test
    fun `custom-named hero is labelled as you among several winners`() {
        val summary = handSummaryFor(
            handNumber = 9,
            result = result(
                net = intArrayOf(0, 12, -12),
                winners = listOf(0, 1),
                pot = 40,
            ),
            seatNames = listOf("Jay", "Ada", "Bruno"),
        )

        assertEquals("You & Ada win", summary.title)
        assertTrue(summary.heroWon)
        assertTrue(summary.detail.endsWith("You broke even"))
    }

    @Test
    fun `side pots name each award and the winning made hand`() {
        val board = cardsOf("2c 7d 9s Jc 3h")
        val aces = cardsOf("Ah Ad")
        val kings = cardsOf("Kh Kd")
        val result = HandResult(
            board = board,
            net = intArrayOf(100, 100, -200),
            wentToShowdown = true,
            potSize = 450,
            events = emptyList(),
            showdownHands = listOf(
                ShowdownHand(0, aces.toList(), HandEvaluator.evaluate(aces + board)),
                ShowdownHand(1, kings.toList(), HandEvaluator.evaluate(kings + board)),
            ),
            potAwards = listOf(
                PotAward(150, listOf(PotPayout(0, 150))),
                PotAward(300, listOf(PotPayout(1, 300))),
            ),
        )

        val summary = handSummaryFor(
            handNumber = 11,
            result = result,
            seatNames = listOf("Jay", "Ada", "Bruno"),
        )

        assertEquals("Main pot", summary.awards[0].label)
        assertEquals(150, summary.awards[0].amount)
        assertEquals("You", summary.awards[0].winners.single().label)
        assertEquals(150, summary.awards[0].winners.single().amountWon)
        assertEquals("Pair", summary.awards[0].winners.single().handDescription)
        assertEquals("Side pot 1", summary.awards[1].label)
        assertEquals("Ada", summary.awards[1].winners.single().label)
        assertEquals(kings.toList(), summary.awards[1].winners.single().hole)
    }

    @Test
    fun `showdown lists every public contender including hands that lost`() {
        val board = cardsOf("2c 7d 9s Jc 3h")
        val aces = cardsOf("Ah Ad")
        val kings = cardsOf("Kh Kd")
        val result = HandResult(
            board = board,
            net = intArrayOf(10, -10),
            wentToShowdown = true,
            potSize = 20,
            events = emptyList(),
            showdownHands = listOf(
                ShowdownHand(0, aces.toList(), HandEvaluator.evaluate(aces + board)),
                ShowdownHand(1, kings.toList(), HandEvaluator.evaluate(kings + board)),
            ),
            potAwards = listOf(PotAward(20, listOf(PotPayout(0, 20)))),
        )

        val summary = handSummaryFor(
            handNumber = 12,
            result = result,
            seatNames = listOf("Jay", "Ada"),
        )

        assertEquals(listOf("You", "Ada"), summary.showdownPlayers.map { it.label })
        assertEquals(listOf(20, 0), summary.showdownPlayers.map { it.amountWon })
        assertEquals(listOf("Pair", "Pair"), summary.showdownPlayers.map { it.handDescription })
        assertEquals(kings.toList(), summary.showdownPlayers[1].hole)
    }
}

class VisibleHandSummaryTest {

    private val summary = HandSummary(
        handNumber = 4,
        board = emptyList(),
        winnerLabels = listOf("Ada"),
        heroWon = false,
        potSize = 20,
        heroNet = -4,
        wentToShowdown = true,
        showdownPlayers = emptyList(),
        awards = emptyList(),
    )

    private fun snapshot(
        handNumber: Int = 4,
        phase: TableSnapshot.Phase = TableSnapshot.Phase.SHOWDOWN,
    ) = TableSnapshot(
        handNumber = handNumber,
        street = Street.RIVER,
        phase = phase,
        board = emptyList(),
        pot = 20,
        currentBet = 0,
        minRaiseSize = 2,
        button = 0,
        seats = emptyList(),
        toAct = null,
        toActToken = null,
        lastAction = null,
        actionNumber = 0,
    )

    @Test
    fun `summary is visible only on its own terminal frame`() {
        assertEquals(
            summary,
            UiState(snapshot = snapshot(), handSummary = summary).visibleHandSummary(),
        )
        assertNull(
            UiState(snapshot = snapshot(handNumber = 5), handSummary = summary).visibleHandSummary(),
        )
        assertNull(
            UiState(
                snapshot = snapshot(phase = TableSnapshot.Phase.BETTING),
                handSummary = summary,
            ).visibleHandSummary(),
        )
    }
}

@OptIn(ExperimentalCoroutinesApi::class)
class NextHandGateTest {

    @Test
    fun `a duplicate old tap cannot release a future hand`() = runTest {
        val gate = NextHandGate()
        gate.request(4)
        gate.awaitRequest(4)

        val next = async { gate.awaitRequest(5) }
        runCurrent()
        assertFalse(next.isCompleted)

        gate.request(4)
        runCurrent()
        assertFalse(next.isCompleted)

        gate.request(5)
        runCurrent()
        assertTrue(next.isCompleted)
    }
}
