import PokerCore
import PokerAI
import TestKit

/// The two betting structures the cash apps add. No-limit is covered by "betting rules"; every
/// number below was worked out by hand from the rule before the code was asked.
func structureTests(_ t: TestRun) {

    t.suite("pot-limit betting") { t in

        // The most you may raise BY is the pot as it stands after your call, so the most you may
        // raise TO is the current bet plus that pot. At 25/50 that makes the first raise 175
        // (3.5 big blinds) and a re-raise of a pot raise 600 (12 big blinds).
        let pl = BlindLevel(smallBlind: 25, bigBlind: 50)
        func pot(_ stacks: [Int], button: Int = 0, level: BlindLevel = pl) -> HandEngine {
            engine(stacks: stacks, button: button, level: level, structure: .potLimit)
        }

        t.test("the first raise may be three and a half big blinds") {
            let e = pot([5000, 5000, 5000, 5000])
            let legal = e.legalActions()!
            t.equal(legal.seat, 3, "under the gun")
            t.equal(legal.maxRaiseTo, 175, "50 to call, then 75 + 50 = 125 more: raise to 175")
            t.equal(legal.minRaiseTo, 100)
            t.equal(legal.allInTo, 5000)
            t.expect(!legal.maxIsAllIn)
        }

        t.test("re-raising a pot raise is twelve big blinds") {
            let e = pot([5000, 5000, 5000, 5000])
            try! e.apply(.raise(to: 175))
            let legal = e.legalActions()!
            t.equal(legal.callCost, 175)
            t.equal(legal.maxRaiseTo, 600, "175 to call makes the pot 425; 425 more on top: 600")
            try! e.apply(.raise(to: 600))
            let next = e.legalActions()!
            t.equal(next.callCost, 575)
            t.equal(next.maxRaiseTo, 2025, "call 575 into 850 makes 1425; 1425 more: 2025")
        }

        t.test("a big blind posted short does not lower the price for everyone else") {
            let e = pot([5000, 5000, 30])
            let legal = e.legalActions()!
            t.equal(legal.seat, 0)
            t.equal(legal.callCost, 50, "the whole big blind, not the short one")
            t.equal(legal.minRaiseTo, 100, "a raise is to two big blinds, whatever the short seat posted")
            try! e.apply(.call)
            try! e.apply(.call)
            let pots = PotBuilder.build(from: e.seats, dead: 0)
            t.equal(pots.map(\.amount), [90, 40], "30 from each of three, then 20 more from the two who could")
            t.expect(!pots[1].eligible.contains(2), "the short seat cannot win chips it did not match")
        }

        t.test("raising above the pot is refused, calling and potting are not") {
            let e = pot([5000, 5000, 5000, 5000])
            do {
                try e.apply(.raise(to: 176))
                t.expect(false, "176 is one chip over the pot and should have been refused")
            } catch {
                t.expect("\(error)".contains("pot limit"), "the refusal names the pot limit: \(error)")
            }
            try! e.apply(.raise(to: 175))
            t.equal(e.currentBet, 175)
        }

        t.test("an opening bet after the flop is the size of the pot") {
            let e = pot([5000, 5000, 5000])
            try! e.apply(.call); try! e.apply(.call); try! e.apply(.check)
            t.equal(e.street, .flop)
            t.equal(e.pot, 150)
            let legal = e.legalActions()!
            t.expect(legal.canBet)
            t.equal(legal.maxRaiseTo, 150, "a bet of the pot")
            t.equal(legal.minRaiseTo, 50)
            try! e.apply(.bet(to: 150))
            let raise = e.legalActions()!
            t.equal(raise.maxRaiseTo, 600, "call 150 into 300 makes 450; 450 more: 600")
        }

        t.test("antes count as part of the pot") {
            let level = BlindLevel(smallBlind: 25, bigBlind: 50, ante: .bigBlindAnte(50))
            let e = pot([5000, 5000, 5000, 5000], level: level)
            t.equal(e.pot, 125)
            t.equal(e.legalActions()!.maxRaiseTo, 225, "the ante is in the middle, so the raise is bigger")
        }

        t.test("a stack shorter than the pot may still go all in") {
            let e = pot([5000, 5000, 5000, 120])
            let legal = e.legalActions()!
            t.equal(legal.seat, 3)
            t.equal(legal.allInTo, 120)
            t.equal(legal.maxRaiseTo, 120, "less than the 175 the pot allows")
            t.expect(legal.maxIsAllIn)
            try! e.apply(.raise(to: 120))
            t.equal(e.seats[3].status, .allIn)
        }

        t.test("a raise over a short all-in is capped by the pot that call makes") {
            let e = pot([5000, 5000, 5000, 120])
            try! e.apply(.raise(to: 120))
            let legal = e.legalActions()!
            t.equal(legal.callCost, 120)
            t.equal(legal.maxRaiseTo, 435, "call 120 into 195 makes 315; 315 more: 435")
            t.equal(legal.minRaiseTo, 190, "120 was a raise of 70 over the blind, a full raise, so the minimum is 70 more again")
        }

        t.test("heads-up the button may make it three big blinds") {
            let e = pot([5000, 5000])
            let legal = e.legalActions()!
            t.equal(legal.seat, 0, "the button posts the small blind and acts first")
            t.equal(legal.callCost, 25)
            t.equal(legal.maxRaiseTo, 150, "25 to call makes 100; 100 more: 150")
        }

        t.test("the maximum is always at least the minimum and never above the stack") {
            var rng = SeededRandom(seed: 3)
            for seed in 1...40 {
                let stacks = (0..<6).map { _ in Int(200 + rng.next() % 3000) }
                let e = HandEngine(seats: makeSeats(stacks),
                                   config: HandConfig(level: pl, buttonSeat: seed % 6, seed: UInt64(seed), structure: .potLimit))
                playOut(e) { legal, _ in
                    if legal.canBetOrRaise {
                        t.expect(legal.maxRaiseTo >= legal.minRaiseTo, "max \(legal.maxRaiseTo) below min \(legal.minRaiseTo)")
                        t.expect(legal.maxRaiseTo <= legal.allInTo, "the cap never exceeds the stack")
                    }
                    if rng.next() % 3 == 0, legal.canBetOrRaise {
                        return legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo)
                    }
                    return legal.canCheck ? .check : .call
                }
                t.equal(e.result!.finalStacks.values.reduce(0, +), stacks.reduce(0, +), "chips conserved")
            }
        }
    }

    t.suite("fixed-limit betting") { t in

        // At 50/100 the small bet is 100 (before the flop and on it) and the big bet 200 (turn
        // and river). A bet and three raises cap a round — 100, 200, 300, 400 before the flop,
        // where the blind is the first bet — unless only two players are left in the hand.
        let fl = BlindLevel(smallBlind: 50, bigBlind: 100)
        func fixed(_ stacks: [Int], button: Int = 0, level: BlindLevel = fl) -> HandEngine {
            engine(stacks: stacks, button: button, level: level, structure: .fixedLimit)
        }
        func toFlop(_ e: HandEngine) {
            // Everyone calls, the big blind checks.
            while e.street == .preflop, let legal = e.legalActions() {
                try! e.apply(legal.canCheck ? .check : .call)
            }
        }

        t.test("there is one size, and it is the big blind before the flop") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            let legal = e.legalActions()!
            t.equal(legal.seat, 3)
            t.equal(legal.callCost, 100)
            t.equal(legal.minRaiseTo, 200)
            t.equal(legal.maxRaiseTo, 200, "a raise is to exactly two bets")
            t.expect(legal.sizeIsFixed)
            t.expect(!legal.capped)
            t.equal(legal.allInTo, 10_000)
        }

        t.test("a raise of any other size is refused") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            for wrong in [150, 250, 400] {
                do {
                    try e.apply(.raise(to: wrong))
                    t.expect(false, "\(wrong) is not the fixed size and should have been refused")
                } catch {
                    t.expect("\(error)".contains("200"), "the refusal names the one legal size: \(error)")
                }
            }
            try! e.apply(.raise(to: 200))
            t.equal(e.currentBet, 200)
        }

        t.test("the blind, a raise, a re-raise and a cap: 100, 200, 300, 400, then nobody may raise") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            try! e.apply(.raise(to: 200))                     // seat 3
            t.equal(e.legalActions()!.maxRaiseTo, 300, "seat 0 may make it three bets")
            try! e.apply(.raise(to: 300))                     // seat 0
            t.equal(e.legalActions()!.maxRaiseTo, 400, "seat 1 may cap it at four")
            try! e.apply(.raise(to: 400))                     // seat 1, the cap
            let capped = e.legalActions()!                    // seat 2
            t.equal(capped.seat, 2)
            t.expect(capped.capped, "four bets are in: the round is capped")
            t.expect(!capped.canRaise)
            t.expect(capped.canCall)
            t.equal(capped.callCost, 300)
            do {
                try e.apply(.raise(to: 500))
                t.expect(false, "a fifth bet should have been refused")
            } catch {
                t.expect("\(error)".contains("capped"), "the refusal says why: \(error)")
            }
        }

        t.test("heads-up there is no cap") {
            let e = fixed([10_000, 10_000])
            var level = 200
            for _ in 0..<7 {                                    // well past four bets
                let legal = e.legalActions()!
                t.expect(legal.canRaise, "raising is still open at \(level)")
                t.expect(!legal.capped)
                t.equal(legal.maxRaiseTo, level)
                try! e.apply(.raise(to: level))
                level += 100
            }
        }

        t.test("the cap lifts the moment only two players are left in the hand") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            try! e.apply(.raise(to: 200))                     // seat 3
            try! e.apply(.raise(to: 300))                     // seat 0
            try! e.apply(.fold)                               // seat 1
            try! e.apply(.fold)                               // seat 2
            try! e.apply(.raise(to: 400))                     // seat 3, the fourth bet
            let legal = e.legalActions()!                     // seat 0, heads-up now
            t.expect(legal.canRaise, "two left in the hand, so the cap does not apply")
            t.expect(!legal.capped)
            t.equal(legal.maxRaiseTo, 500)
        }

        t.test("a short all-in under the size of a bet is offered, and does not count towards the cap") {
            let e = fixed([10_000, 10_000, 10_000, 130])
            let short = e.legalActions()!
            t.equal(short.seat, 3)
            t.equal(short.maxRaiseTo, 130, "all it has")
            t.equal(short.minRaiseTo, 130)
            t.expect(short.maxIsAllIn)
            try! e.apply(.raise(to: 130))
            let next = e.legalActions()!                      // seat 0
            t.equal(next.callCost, 130)
            t.equal(next.maxRaiseTo, 200, "the next full level is still two bets, not 230")
            try! e.apply(.raise(to: 200))
            try! e.apply(.raise(to: 300))
            let fourth = e.legalActions()!                    // seat 2
            t.equal(fourth.maxRaiseTo, 400, "the short all-in was not a bet, so 400 is still the cap, not 300")
            try! e.apply(.raise(to: 400))
            t.expect(e.legalActions()!.capped)
        }

        t.test("a stack that covers the bet raises exactly the bet and keeps the rest") {
            // There is no such thing as an all-in for more than the fixed size: a 250 stack
            // makes it 200 and has 50 behind, however much it would like to shove.
            let e = fixed([10_000, 10_000, 10_000, 250])
            let legal = e.legalActions()!
            t.equal(legal.maxRaiseTo, 200)
            t.expect(!legal.maxIsAllIn)
            do {
                try e.apply(.raise(to: 250))
                t.expect(false, "250 is not a fixed-limit size and should have been refused")
            } catch {
                t.expect("\(error)".contains("200"), "\(error)")
            }
            try! e.apply(.raise(to: 200))
            t.equal(e.seats[3].stack, 50)
            t.equal(e.legalActions()!.maxRaiseTo, 300, "the next level is three bets")
        }

        t.test("the bet doubles on the turn and stays doubled on the river") {
            let e = fixed([10_000, 10_000, 10_000])
            toFlop(e)
            t.equal(e.street, .flop)
            t.equal(e.legalActions()!.maxRaiseTo, 100, "small bet on the flop")
            try! e.apply(.check); try! e.apply(.check); try! e.apply(.check)
            t.equal(e.street, .turn)
            let turn = e.legalActions()!
            t.expect(turn.canBet)
            t.equal(turn.maxRaiseTo, 200, "big bet on the turn")
            try! e.apply(.bet(to: 200))
            t.equal(e.legalActions()!.maxRaiseTo, 400, "a raise on the turn is two big bets")
            try! e.apply(.call); try! e.apply(.call)
            t.equal(e.street, .river)
            t.equal(e.legalActions()!.maxRaiseTo, 200, "big bet on the river")
        }

        t.test("the flop caps at four bets too: 100, 200, 300, 400") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            toFlop(e)
            try! e.apply(.bet(to: 100)); try! e.apply(.raise(to: 200))
            try! e.apply(.raise(to: 300)); try! e.apply(.raise(to: 400))
            let legal = e.legalActions()!
            t.expect(legal.capped)
            t.expect(!legal.canRaise)
        }

        t.test("the big blind keeps its option to raise when everyone just calls") {
            let e = fixed([10_000, 10_000, 10_000, 10_000])
            try! e.apply(.call); try! e.apply(.call); try! e.apply(.call)   // seats 3, 0, 1
            let bb = e.legalActions()!
            t.equal(bb.seat, 2)
            t.expect(bb.canCheck)
            t.expect(bb.canRaise)
            t.equal(bb.maxRaiseTo, 200)
        }

        t.test("an ante is dead money and changes no bet size") {
            let level = BlindLevel(smallBlind: 50, bigBlind: 100, ante: .perPlayer(25))
            let e = fixed([10_000, 10_000, 10_000, 10_000], level: level)
            let legal = e.legalActions()!
            t.equal(legal.callCost, 100)
            t.equal(legal.maxRaiseTo, 200)
        }

        t.test("chips are neither created nor destroyed at fixed-limit") {
            var rng = SeededRandom(seed: 88)
            for hand in 0..<40 {
                let stacks = (0..<5).map { _ in 150 + Int(rng.next() % 2000) }
                let e = HandEngine(seats: makeSeats(stacks),
                                   config: HandConfig(level: fl, buttonSeat: hand % 5, seed: rng.next(),
                                                      settlement: .allInEquity(trials: 60), structure: .fixedLimit))
                playOut(e) { legal, _ in
                    if legal.canBetOrRaise {
                        t.expect(legal.minRaiseTo == legal.maxRaiseTo, "one size at fixed-limit")
                        t.expect(legal.maxRaiseTo <= legal.allInTo)
                    }
                    if legal.canBetOrRaise, rng.next() % 2 == 0 {
                        return legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo)
                    }
                    return legal.canCheck ? .check : .call
                }
                t.equal(e.result!.finalStacks.values.reduce(0, +), stacks.reduce(0, +), "hand \(hand)")
            }
        }

        t.test("the opponents play legal fixed-limit and legal pot-limit") {
            // Every action a bot produces goes straight to the engine, which throws on anything
            // illegal. `BotTable` would have hidden a throw behind a passive fallback.
            for structure in [BettingStructure.fixedLimit, .potLimit] {
                var bots = Personality.allCases.prefix(5).enumerated().map { i, style in
                    Bot(id: "b\(i)", personality: style, skill: 30 + i * 12, seed: UInt64(i + 1))
                }
                var illegal = 0
                var decisions = 0
                for hand in 0..<120 {
                    let seats = (0..<5).map { SeatState(seat: $0, playerID: "b\($0)", stack: 6_000) }
                    let e = HandEngine(seats: seats,
                                       config: HandConfig(level: fl, buttonSeat: hand % 5, seed: UInt64(hand + 7),
                                                          structure: structure))
                    var steps = 0
                    while !e.isComplete, let seat = e.actor, let view = e.view(for: seat) {
                        let action = bots[seat].decide(view, models: nil)
                        decisions += 1
                        do { try e.apply(action) } catch { illegal += 1; try! e.apply(view.legal.canCheck ? .check : .fold) }
                        steps += 1
                        precondition(steps < 400)
                    }
                }
                t.equal(illegal, 0, "\(structure.name): \(illegal) illegal actions in \(decisions)")
                t.greaterThan(decisions, 1000, "\(structure.name): enough decisions to mean anything")
            }
        }
    }
}
