import Foundation
import PokerCore
import PokerAI
import PokerCoach
import PokerSession
import TestKit

/// Plays a cash session hand by hand, choosing the player's action each time.
private func playCash(_ session: TableSession, hands: Int, choose: (LegalActions, TableSession) -> PlayerAction) {
    var guardRail = 0
    while session.isPlaying, session.handNumber <= hands {
        guardRail += 1
        precondition(guardRail < 60_000, "session failed to progress")
        if let legal = session.legal {
            session.act(choose(legal, session))
        } else if session.engine?.isComplete == true {
            if session.handNumber >= hands { return }
            session.continueAfterReview()
        } else if session.opponentToAct {
            session.stepOpponent()
        } else {
            return
        }
    }
}

private func cashSession(seed: UInt64 = 7, setup: CashSetup = .standard,
                         history: HistoryStore = .inMemory()) -> TableSession {
    let session = TableSession(seed: seed, history: history, variant: .pokerTutorCash)
    session.cashSetup = setup
    return session
}

func cashTableTests(_ t: TestRun) {
    t.suite("a cash table") { t in

        t.test("everybody sits down with a hundred big blinds at the chosen stakes, and nothing else moves") {
            let setup = CashSetup(stakes: BlindLevel(smallBlind: 100, bigBlind: 200), structure: .noLimit)
            let session = cashSession(setup: setup)
            session.start(.cash)
            t.equal(session.mode, .cash)
            t.equal(session.level, setup.stakes)
            t.equal(session.level.ante, AnteStyle.none, "no antes at a cash table")
            for seat in session.engine!.seats {
                t.equal(seat.stack + seat.committedThisRound + seat.antePaid, 20_000, "\(seat.playerID) sat down with 100 big blinds")
            }
            playCash(session, hands: 30) { legal, _ in legal.canCheck ? .check : .fold }
            t.equal(session.level, setup.stakes, "the blinds are where they were set thirty hands later")
            t.equal(session.engine!.config.level.ante, AnteStyle.none)
            t.greaterThan(session.handNumber, 29)
        }

        t.test("the table plays the structure the setup asked for") {
            for structure in BettingStructure.allCases {
                let session = cashSession(setup: CashSetup(stakes: CashSetup.stakesOnOffer[1], structure: structure))
                session.start(.cash)
                t.equal(session.engine?.config.structure, structure)
                t.equal(session.bettingStructure, structure)
                playCash(session, hands: 3) { legal, _ in legal.canCheck ? .check : .call }
                t.equal(session.history.hands.last?.structure, structure.rawValue, "the hand says how it was limited")
            }
        }

        t.test("stacks carry from hand to hand instead of being topped back up") {
            let session = cashSession()
            session.start(.cash)
            // Fold everything: the blinds drain the player's stack a little at a time.
            playCash(session, hands: 40) { legal, _ in legal.canCheck ? .check : .fold }
            let hero = session.seat(session.heroSeat)!
            t.lessThan(hero.stack + hero.committedThisRound, session.cashSetup.buyIn,
                       "forty hands of blinds have come off the stack and stayed off")
            t.equal(session.rebuys["You"] ?? 0, 0, "nobody rebuys while they still have chips")
        }

        t.test("a player who busts buys back in for a hundred big blinds, and the rebuy is counted") {
            let session = cashSession(seed: 3)
            session.start(.cash)
            // Move in every hand: the player busts sooner or later, and so do some opponents.
            playCash(session, hands: 400) { legal, _ in
                legal.canBetOrRaise ? (legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo))
                                    : (legal.canCall ? .call : .check)
            }
            let mine = session.rebuys["You"] ?? 0
            t.greaterThan(mine, 0, "moving in for 400 hands busts at least once")
            // The buy-back happens as the next hand is dealt, so deal it.
            if session.engine?.isComplete == true { session.continueAfterReview() }
            let hero = session.seat(session.heroSeat)!
            t.greaterThan(hero.stack + hero.committedThisRound, 0, "and the player is still at the table with chips")
            // The hand after each bust starts with exactly the buy-in in front of the player —
            // a hundred big blinds, not a top-up to whatever was lost.
            let hands = session.history.hands
            var rebuysSeen = 0
            for hand in hands {
                let now = hand.rebuys ?? 0
                if now > rebuysSeen, let me = hand.players?.first(where: { $0.isHero }) {
                    t.equal(me.startingStack, session.cashSetup.buyIn,
                            "hand \(hand.handNumber) after a bust should start with the buy-in")
                }
                rebuysSeen = max(rebuysSeen, now)
            }
            t.equal(session.history.hands.last?.rebuys, mine, "the hand carries the count")
            let recorded = session.history.hands.map { $0.rebuys ?? 0 }
            t.expect(recorded == recorded.sorted(), "the count written down never goes down")
        }

        t.test("an opponent who busts twice leaves, and somebody new sits down") {
            let session = cashSession(seed: 3)
            session.start(.cash)
            let original = Set(session.seatedIDs)
            playCash(session, hands: 400) { legal, _ in
                legal.canBetOrRaise ? (legal.canBet ? .bet(to: legal.maxRaiseTo) : .raise(to: legal.maxRaiseTo))
                                    : (legal.canCall ? .call : .check)
            }
            let now = Set(session.seatedIDs)
            t.equal(now.count, original.count, "the table stays full")
            t.expect(now != original, "at least one seat changed hands over 400 all-in hands")
            let newcomers = now.subtracting(original)
            for name in newcomers {
                t.expect(session.opponents[name] != nil, "\(name) is somebody the coach can read")
                t.expect(!original.contains(name), "a new name, not a returning one")
            }
            t.expect(now.contains("You"), "the player never leaves this way")
        }

        t.test("cashing out reports what the hands say, and nothing the hands do not") {
            let history = HistoryStore.inMemory()
            let session = cashSession(seed: 5, setup: CashSetup(stakes: CashSetup.stakesOnOffer[0], structure: .potLimit),
                                      history: history)
            session.start(.cash)
            playCash(session, hands: 25) { legal, _ in legal.canCheck ? .check : .call }
            let played = history.hands.filter { $0.sessionID == session.sessionID }
            session.leave()
            let result = session.lastCashResult!
            t.equal(result.hands, played.count)
            t.equal(result.netChips, played.reduce(0) { $0 + $1.netChips })
            t.close(result.netBB, played.reduce(0.0) { $0 + $1.netBB }, 0.001)
            t.equal(result.setup.structure, .potLimit)
            t.close(result.bbPer100, result.netBB / Double(result.hands) * 100, 0.001)
            t.expect(!session.isPlaying)
        }

        t.test("a cash table never judges a stack as a push-or-fold spot") {
            // Twelve big blinds at a cash table is somebody who has lost some of a buy-in, not
            // somebody the blinds are about to swallow. Until Phase 2 says what to say, the
            // coach says nothing about it — and a tournament table still does.
            let cash = cashSession()
            t.expect(!cash.coach.judgesShortStacks)
            let tournament = TableSession(seed: 7, history: .inMemory(), variant: .pokerTutor)
            t.expect(tournament.coach.judgesShortStacks)
        }

        t.test("a tournament hand is written down exactly as before") {
            let session = TableSession(seed: 9, history: .inMemory())
            session.start(.tournament)
            playCash(session, hands: 3) { legal, _ in legal.canCheck ? .check : .fold }
            let hand = session.history.hands.last!
            t.equal(hand.structure, nil, "no structure field on a tournament hand")
            t.equal(hand.rebuys, nil)
            t.equal(session.engine?.config.structure ?? .noLimit, .noLimit)
        }
    }
}
