import PokerCore
import PokerAI
import PokerSession
import TestKit

private func legal(min: Int, max: Int, canRaise: Bool = true) -> LegalActions {
    LegalActions(seat: 0, canFold: true, canCheck: false, canCall: true, callCost: 100,
                 callIsAllIn: false, canBet: false, canRaise: canRaise,
                 minRaiseTo: min, maxRaiseTo: max)
}

func chipTrayTests(_ t: TestRun) {
    t.suite("betting with chips") { t in

        t.test("the chips are the ones you can afford") {
            let deep = ChipTray(minimum: 100, maximum: 10_000)
            t.equal(deep.offered.map(\.value), [25, 50, 100, 500, 1_000, 5_000, 10_000])

            let short = ChipTray(minimum: 100, maximum: 600)
            t.equal(short.offered.map(\.value), [25, 50, 100, 500],
                    "a chip bigger than the stack is a button that can never be pressed")

            let tiny = ChipTray(minimum: 20, maximum: 30)
            t.equal(tiny.offered.map(\.value), [25])
        }

        t.test("counting a bet out") {
            // The example that started this: two hundreds and a fifty is 250.
            let tray = ChipTray(minimum: 100, maximum: 10_000)
            var amount = 0
            amount = tray.adding(.hundred, to: amount)
            amount = tray.adding(.hundred, to: amount)
            amount = tray.adding(.fifty, to: amount)
            t.equal(amount, 250)
            t.expect(tray.isLegal(amount))
        }

        t.test("you cannot bet more than you have") {
            let tray = ChipTray(minimum: 100, maximum: 1_200)
            t.expect(tray.canAdd(.thousand, to: 0))
            t.expect(!tray.canAdd(.thousand, to: 500), "1,500 out of a 1,200 stack")
            t.equal(tray.adding(.thousand, to: 500), 500, "an impossible chip must change nothing")
            t.expect(tray.canAdd(.hundred, to: 1_100))
            t.expect(!tray.canAdd(.hundred, to: 1_150))
            t.expect(!tray.isLegal(1_300))
        }

        t.test("a bet under the minimum is not ready yet") {
            let tray = ChipTray(minimum: 300, maximum: 10_000)
            t.expect(!tray.isLegal(250))
            t.equal(tray.shortfall(250), 50)
            t.equal(tray.shortfall(275), 25)
            t.expect(tray.shortfall(300) == nil, "exactly the minimum is a legal bet")
            t.expect(tray.shortfall(9_000) == nil)
            t.expect(tray.isLegal(300))
        }

        t.test("counting out the all in, and the odd minimum") {
            let tray = ChipTray(minimum: 100, maximum: 6_575)
            let all = tray.quickest(tray.maximum)
            t.equal(tray.total(all), 6_575, "the all in has to be exact")
            t.expect(tray.isLegal(tray.total(all)))
            t.equal(all.count, 5, "5,000 + 1,000 + 500 + 50 + 25 is the shortest way to 6,575")

            // A minimum raise that chips cannot hit exactly must round up to a legal bet, never
            // down to an illegal one.
            let awkward = ChipTray(minimum: 137, maximum: 10_000)
            let counted = awkward.quickest(awkward.minimum)
            t.expect(awkward.total(counted) >= 137, "counted out less than the minimum raise")
            t.expect(awkward.isLegal(awkward.total(counted)))
            t.equal(awkward.total(counted), 150)
        }

        t.test("counting out never exceeds the stack") {
            let tray = ChipTray(minimum: 90, maximum: 90)
            let counted = tray.quickest(90)
            t.expect(tray.total(counted) <= 90, "counted out more chips than the player has")
            for chip in Chip.allCases {
                t.expect(!tray.canAdd(chip, to: 90), "\(chip.label) fitted into a full stack")
            }
        }

        t.test("chips read the way they are said") {
            t.equal(Chip.twentyFive.label, "25")
            t.equal(Chip.hundred.label, "100")
            t.equal(Chip.thousand.label, "1K")
            t.equal(Chip.fiveThousand.label, "5K")
            t.equal(Chip.tenThousand.label, "10K")
        }

        t.test("there is no tray when raising is not allowed") {
            t.expect(ChipTray(legal: legal(min: 0, max: 0, canRaise: false)) == nil)
            t.expect(ChipTray(legal: legal(min: 200, max: 4_000)) != nil)
        }

        t.test("every blind level is payable in the chips on the table") {
            let ladder = TournamentStructure.blindLadder
            t.equal(ladder.first?.small, 25)
            t.equal(ladder.first?.big, 50)
            t.equal(Array(ladder.prefix(8).map(\.big)),
                    [50, 100, 200, 400, 1_000, 2_000, 4_000, 8_000],
                    "the published ladder")

            let smallest = Chip.twentyFive.value
            for level in ladder {
                t.equal(level.small % smallest, 0, "a \(level.small) small blind cannot be paid in chips")
                t.equal(level.big % smallest, 0, "a \(level.big) big blind cannot be paid in chips")
                t.equal(level.big, level.small * 2, "the big blind is two small blinds")
            }
            for (a, b) in zip(ladder, ladder.dropFirst()) {
                t.lessThan(a.big, b.big, "the blinds must always go up")
            }

            // And the same for the ante, which is charged as a big blind.
            let structure = TournamentStructure.standard()
            for level in structure.levels {
                if case .bigBlindAnte(let ante) = level.ante {
                    t.equal(ante % smallest, 0, "a \(ante) ante cannot be paid in chips")
                }
            }
        }

        t.test("nobody at the table bets in chips that do not exist") {
            // The point of the whole exercise: an opponent must never push out an amount the
            // player could not have counted out themselves. Checked at the moment of the decision,
            // because an all in that nobody calls is refunded afterwards and stops looking like one.
            let smallest = Chip.twentyFive.value
            var checked = 0
            var allIns = 0

            for seed in [3, 19, 77] as [UInt64] {
                let structure = TournamentStructure.standard()
                let ids = (0..<9).map { "P\($0)" }
                let tournament = Tournament(structure: structure, playerIDs: ids, seed: seed)
                let bots = ids.map { Bot(id: $0, personality: .solidRegular, skill: 55, seed: seed &+ 7) }
                let table = BotTable(bots: bots, seed: seed)

                var hands = 0
                while let (seats, config) = tournament.nextHandSeats(), hands < 120 {
                    let engine = HandEngine(seats: seats, config: config)

                    while !engine.isComplete, let seat = engine.actor {
                        let shove = engine.view(for: seat)?.legal.maxRaiseTo ?? 0
                        guard let action = table.step(engine) else { break }
                        switch action {
                        case .bet(let to), .raise(let to):
                            if to == shove {
                                // A stack is whatever it is, so an all in can land anywhere.
                                allIns += 1
                            } else {
                                t.equal(to % smallest, 0,
                                        "a bet of \(to) at \(config.level.bigBlind) blinds cannot be made from chips")
                                checked += 1
                            }
                        default: break
                        }
                    }
                    table.observeFinished(engine)

                    let seatIDs = Dictionary(uniqueKeysWithValues: engine.seats.map { ($0.seat, $0.playerID) })
                    tournament.recordHand(finalStacks: engine.result!.finalStacks, seatIDs: seatIDs)
                    hands += 1
                }
            }
            t.expect(checked > 200, "only \(checked) ordinary opponent bets were checked")
            t.expect(allIns > 0, "no all ins happened, so that exemption went untested")
        }

        t.test("a real hand always offers a way to make a legal bet") {
            let session = TableSession(seed: 88)
            session.pacedByCaller = true
            session.start(.tournament)

            var spots = 0
            var guardRail = 0
            while session.isPlaying, session.handNumber <= 40, spots < 25 {
                guardRail += 1
                precondition(guardRail < 40_000)
                if session.stepOpponent() != nil { continue }
                guard let legal = session.legal else {
                    if session.review != nil, session.engine?.isComplete == true {
                        if session.handNumber >= 40 { break }
                        session.continueAfterReview()
                        continue
                    }
                    break
                }

                if legal.canBetOrRaise {
                    guard let tray = session.chipTray else {
                        t.expect(false, "no chips offered in a spot that allows raising")
                        break
                    }
                    t.expect(!tray.offered.isEmpty, "a raise was legal but no chip was small enough")
                    // Whatever the spot, the player must be able to build something legal — the
                    // minimum raise at the very least.
                    let counted = tray.total(tray.quickest(tray.minimum))
                    t.expect(tray.isLegal(counted),
                             "cannot count out a legal bet: min \(tray.minimum), max \(tray.maximum)")
                    spots += 1
                } else {
                    t.expect(session.chipTray == nil, "chips offered when raising was not allowed")
                }
                session.act(legal.canCheck ? .check : .fold)
            }
            t.expect(spots > 0, "never reached a spot where raising was possible")
        }
    }
}
