import PokerCore
import TestKit

func tournamentTests(_ t: TestRun) {
    t.suite("tournament structure") { t in

        t.test("blinds go up on schedule") {
            let s = TournamentStructure.standard(startingStack: 10_000, handsPerLevel: 10)
            t.equal(s.level(atHand: 0).bigBlind, s.levels[0].bigBlind)
            t.equal(s.level(atHand: 9).bigBlind, s.levels[0].bigBlind)
            t.equal(s.level(atHand: 10).bigBlind, s.levels[1].bigBlind)
            t.greaterThan(s.levels[1].bigBlind, s.levels[0].bigBlind)
            t.greaterThan(s.levels[5].bigBlind, s.levels[4].bigBlind)
        }

        t.test("blinds never go backwards and stay round") {
            let s = TournamentStructure.standard()
            for (a, b) in zip(s.levels, s.levels.dropFirst()) {
                t.greaterThan(b.bigBlind, a.bigBlind)
                t.expect(b.smallBlind * 2 <= b.bigBlind + 1, "small blind should be about half")
            }
        }

        t.test("the schedule runs out of levels gracefully") {
            let s = TournamentStructure.standard(handsPerLevel: 5)
            t.equal(s.level(atHand: 100_000).bigBlind, s.levels.last!.bigBlind)
        }

        t.test("payouts sum to the whole prize pool") {
            for places in 1...9 {
                let ladder = TournamentStructure.payoutLadder(places: places)
                t.close(ladder.reduce(0, +), 1.0, 0.0001, "\(places) places")
                for (a, b) in zip(ladder, ladder.dropFirst()) {
                    t.greaterThan(a, b - 0.0001, "higher finishes pay more")
                }
            }
        }
    }

    t.suite("prize-money equity") { t in

        t.test("equal stacks split the prize pool equally") {
            let e = ICM.equity(stacks: [1000, 1000, 1000], payouts: [0.5, 0.3, 0.2])
            for share in e { t.close(share, 1.0 / 3.0, 0.001) }
        }

        t.test("everyone's equity sums to the prize pool") {
            let e = ICM.equity(stacks: [5000, 3000, 1500, 500], payouts: [0.5, 0.3, 0.2])
            t.close(e.reduce(0, +), 1.0, 0.001)
        }

        t.test("a bigger stack is worth more, but not proportionally") {
            let e = ICM.equity(stacks: [8000, 1000, 1000], payouts: [0.5, 0.3, 0.2])
            t.greaterThan(e[0], e[1])
            let chipShare = 8000.0 / 10000.0
            t.lessThan(e[0], chipShare, "doubling chips does not double prize equity")
            t.greaterThan(e[0], 0.4, "but the chip leader is clearly ahead")
        }

        t.test("chips are worth less as the ladder flattens") {
            // With winner-take-all, prize equity tracks chip share exactly.
            let winnerTakeAll = ICM.equity(stacks: [8000, 1000, 1000], payouts: [1.0])
            t.close(winnerTakeAll[0], 0.8, 0.001)
            // Spread the money around and the leader's edge shrinks.
            let spread = ICM.equity(stacks: [8000, 1000, 1000], payouts: [0.4, 0.35, 0.25])
            t.lessThan(spread[0], winnerTakeAll[0])
        }

        t.test("a player with no chips is worth nothing") {
            let e = ICM.equity(stacks: [5000, 5000, 0], payouts: [0.5, 0.3, 0.2])
            t.close(e[2], 0.0, 0.001)
        }
    }

    t.suite("running a tournament") { t in

        t.test("a nine-handed tournament plays down to one winner") {
            let structure = TournamentStructure.standard(startingStack: 3_000, handsPerLevel: 8, placesPaid: 3)
            let tourney = Tournament(structure: structure, playerIDs: (0..<9).map { "P\($0)" }, seed: 77)
            let totalChips = 9 * 3_000
            var hands = 0

            while let (seats, config) = tourney.nextHandSeats() {
                let e = HandEngine(seats: seats, config: config)
                var steps = 0
                var rng = SeededRandom(seed: config.seed &+ 1)
                while !e.isComplete, let legal = e.legalActions() {
                    let roll = rng.next() % 100
                    let action: PlayerAction
                    if roll < 45 && !legal.canCheck {
                        action = .fold
                    } else if roll < 90 {
                        action = legal.canCheck ? .check : .call
                    } else if legal.canBetOrRaise {
                        action = legal.canBet ? .bet(to: legal.minRaiseTo) : .raise(to: legal.minRaiseTo)
                    } else {
                        action = legal.canCheck ? .check : .call
                    }
                    try! e.apply(action)
                    steps += 1
                    precondition(steps < 400, "hand failed to terminate")
                }

                let ids = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
                tourney.recordHand(finalStacks: e.result!.finalStacks, seatIDs: ids)

                let inPlay = tourney.players.reduce(0) { $0 + $1.stack }
                t.equal(inPlay, totalChips, "chips left the tournament on hand \(hands)")

                hands += 1
                precondition(hands < 5_000, "tournament failed to finish")
            }

            t.expect(tourney.isComplete, "the tournament should be over")
            t.equal(tourney.alive.count, 1)
            let places = tourney.players.compactMap(\.finishedPlace).sorted()
            t.equal(places, Array(1...9), "every player got a distinct finishing place")
            t.close(tourney.payouts().values.reduce(0, +), 1.0, 0.0001, "the whole prize pool is paid out")
            t.greaterThan(hands, 20, "a tournament should take more than a handful of hands")
        }

        t.test("the same seed replays the same tournament") {
            func run() -> [String: Int] {
                let structure = TournamentStructure.standard(startingStack: 2_000, handsPerLevel: 6)
                let tourney = Tournament(structure: structure, playerIDs: (0..<6).map { "P\($0)" }, seed: 4242)
                var hands = 0
                while let (seats, config) = tourney.nextHandSeats() {
                    let e = HandEngine(seats: seats, config: config)
                    while !e.isComplete, let legal = e.legalActions() {
                        try! e.apply(legal.canCheck ? .check : .call)
                    }
                    let ids = Dictionary(uniqueKeysWithValues: seats.map { ($0.seat, $0.playerID) })
                    tourney.recordHand(finalStacks: e.result!.finalStacks, seatIDs: ids)
                    hands += 1
                    // A guard, not a formality: dead money handled wrongly once made this loop
                    // forever, with two short stacks refunded their own blinds every hand.
                    if hands > 3_000 {
                        t.expect(false, "tournament never ended — stacks \(tourney.alive.map { "\($0.id)=\($0.stack)" })")
                        break
                    }
                }
                return Dictionary(uniqueKeysWithValues: tourney.players.map { ($0.id, $0.finishedPlace ?? 0) })
            }
            t.equal(run(), run())
        }

        t.test("prize equity is available while the tournament is running") {
            let structure = TournamentStructure.standard(startingStack: 5_000, placesPaid: 3)
            let tourney = Tournament(structure: structure, playerIDs: ["a", "b", "c", "d"], seed: 9)
            let equity = tourney.currentEquity()
            t.equal(equity.count, 4)
            t.close(equity.values.reduce(0, +), 1.0, 0.001)
        }
    }
}
