package com.jsjdesigns.poker.game

import com.jsjdesigns.poker.core.StackedDeck
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
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

private class Folder : PlayerAgent {
    override suspend fun act(ctx: DecisionContext): Action =
        if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.FOLD)
}

private class Caller : PlayerAgent {
    override suspend fun act(ctx: DecisionContext): Action =
        if (ctx.canCheck) Action(ActionType.CHECK) else Action(ActionType.CALL, ctx.toCall)
}

/** Opens once so that someone downstream actually has a bet to fold to. */
private class Raiser(private val to: Int) : PlayerAgent {
    private var raised = false
    override suspend fun act(ctx: DecisionContext): Action = when {
        !raised && ctx.canRaise -> { raised = true; Action(ActionType.RAISE, to) }
        ctx.canCheck -> Action(ActionType.CHECK)
        else -> Action(ActionType.CALL, ctx.toCall)
    }
}

class SnapshotAndHumanAgentTest {

    private fun context(seat: Seat) = DecisionContext(
        street = Street.PREFLOP,
        seat = seat,
        board = IntArray(0),
        pot = 15,
        toCall = 10,
        minRaiseTo = 20,
        maxRaiseTo = 500,
        activeOpponents = 1,
        seatsActingAfter = 0,
        bigBlind = 10,
        history = emptyList(),
        handNumber = 1,
        bettingReopened = true,
    )

    // ---------- snapshots ----------

    @Test
    fun `snapshots are published for the deal, every action, and the finish`() = runTest {
        val seen = mutableListOf<TableSnapshot>()
        val seats = listOf(
            Seat(0, "A", 500, Caller()),
            Seat(1, "B", 500, Caller()),
        )
        Table(
            seats, 5, 10, Random(1),
            StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
            observer = { seen += it },
        ).playHand()

        assertTrue(seen.isNotEmpty(), "the UI must receive something to render")
        assertEquals(TableSnapshot.Phase.DEALT, seen.first().phase, "first snapshot is the deal")
        assertTrue(
            seen.any { it.phase == TableSnapshot.Phase.BETTING && it.toAct != null },
            "some snapshot must name whose turn it is",
        )
        assertTrue(
            seen.last().phase == TableSnapshot.Phase.SHOWDOWN ||
                seen.last().phase == TableSnapshot.Phase.COMPLETE,
            "the hand must end on a terminal phase, was ${seen.last().phase}",
        )
    }

    @Test
    fun `a snapshot carries the board and pot as the hand progresses`() = runTest {
        val seen = mutableListOf<TableSnapshot>()
        val seats = listOf(Seat(0, "A", 500, Caller()), Seat(1, "B", 500, Caller()))
        Table(
            seats, 5, 10, Random(1),
            StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
            observer = { seen += it },
        ).playHand()

        assertEquals(0, seen.first().board.size, "no board on the deal")
        assertEquals(5, seen.last().board.size, "full board by the end")
        assertTrue(seen.last().pot > 0)
        assertTrue(seen.all { it.handNumber == 1 })
    }

    @Test
    fun `masking hides other players hole cards until showdown`() = runTest {
        val seen = mutableListOf<TableSnapshot>()
        val seats = listOf(Seat(0, "A", 500, Caller()), Seat(1, "B", 500, Caller()))
        Table(
            seats, 5, 10, Random(1),
            StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
            observer = { seen += it },
        ).playHand()

        val duringPlay = seen.first { it.phase == TableSnapshot.Phase.DEALT }.maskedFor(viewer = 0)
        assertNotNull(duringPlay.seats[0].hole, "a player always sees their own cards")
        assertNull(duringPlay.seats[1].hole, "an opponent's cards must be hidden")

        // Both called to the river, so both are revealed at showdown.
        val end = seen.last().maskedFor(viewer = 0)
        assertNotNull(end.seats[1].hole, "cards turned up at showdown stay visible")
    }

    @Test
    fun `folded hands are never revealed`() = runTest {
        val seen = mutableListOf<TableSnapshot>()
        // The raiser gives the folder something to fold to; a checker faces nothing
        // heads-up, and sanitise() turns a pointless FOLD into a CHECK.
        val seats = listOf(Seat(0, "A", 500, Raiser(to = 40)), Seat(1, "B", 500, Folder()))
        val result = Table(
            seats, 5, 10, Random(1),
            StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
            observer = { seen += it },
        ).playHand()

        assertFalse(result.wentToShowdown, "the hand must actually end in a fold")
        val end = seen.last().maskedFor(viewer = 0)
        val folded = end.seats.first { it.folded }
        assertNull(folded.hole, "a folded hand must stay face down")
    }

    // ---------- human input ----------

    @Test
    fun `submit resolves the pending decision`() = runTest {
        val human = HumanAgent()
        val seat = Seat(0, "You", 500, human)
        val decision = async { human.act(context(seat)) }

        while (!human.isAwaitingInput) yield()
        assertNotNull(human.awaiting, "the UI must be able to see what is on offer")
        assertTrue(human.submit(Action(ActionType.CALL, 10)))

        assertEquals(ActionType.CALL, decision.await().type)
        assertFalse(human.isAwaitingInput, "the wait must clear once answered")
    }

    @Test
    fun `submitting when nothing is pending is harmless`() = runTest {
        val human = HumanAgent()
        assertFalse(human.submit(Action(ActionType.FOLD)), "a stale tap must not crash")
    }

    @Test
    fun `cancelling the hand releases a human waiting for input`() = runTest {
        val human = HumanAgent()
        val seat = Seat(0, "You", 500, human)
        val job = launch { human.act(context(seat)) }

        while (!human.isAwaitingInput) yield()
        job.cancel()
        job.join()

        assertFalse(
            human.isAwaitingInput,
            "cancellation must clear the pending wait, not strand it forever",
        )
        // The agent is reusable afterwards.
        val next = async { human.act(context(seat)) }
        while (!human.isAwaitingInput) yield()
        assertTrue(human.submit(Action(ActionType.FOLD)))
        assertEquals(ActionType.FOLD, next.await().type)
    }

    @Test
    fun `a human can play a full hand against a bot`() = runTest {
        val human = HumanAgent()
        val seen = mutableListOf<TableSnapshot>()
        val seats = listOf(Seat(0, "You", 500, human), Seat(1, "Bot", 500, Caller()))
        val table = Table(
            seats, 5, 10, Random(1),
            StackedDeck.of(listOf("Ah Ad", "Kh Kd"), "2c 7d 9s Jc 3h"),
            observer = { seen += it },
        )

        val hand = async { table.playHand() }
        // Fold at the first opportunity.
        while (!human.isAwaitingInput) yield()
        human.submit(Action(ActionType.FOLD))

        val result = hand.await()
        assertEquals(0, result.net.sum(), "chips conserved through a human-driven hand")
        assertTrue(seen.isNotEmpty())
        assertEquals(listOf(1), result.winners, "the bot wins once the human folds")
    }
}
