import PokerCore
import TestKit

private func heroEquity(_ hero: String, vs villain: String, board: String = "", trials: Int = 40_000, seed: UInt64 = 7) -> Double {
    var rng = SeededRandom(seed: seed)
    return EquityCalculator.equity(
        hero: [Card].parse(hero),
        opponents: [[Card].parse(villain)],
        board: [Card].parse(board),
        trials: trials,
        rng: &rng
    ).equity
}

func equityTests(_ t: TestRun) {
    t.suite("equity") { t in

        // Published all-in preflop equities. Tolerance is wide enough that sampling noise
        // can't fail the suite, tight enough to catch a real error.
        t.test("known preflop matchups") {
            t.close(heroEquity("As Ah", vs: "Kc Kd"), 0.812, 0.015, "pair over pair")
            t.close(heroEquity("As Ks", vs: "Qc Qd"), 0.460, 0.015, "suited overcards vs a pair")
            t.close(heroEquity("As Ah", vs: "Kc Qd"), 0.870, 0.015, "pair vs two undercards")
            t.close(heroEquity("Ac Ah", vs: "7c 2d"), 0.877, 0.015, "aces vs the worst hand")
            t.close(heroEquity("Js Ts", vs: "Ac Kd"), 0.415, 0.015, "the flip that isn't a flip")
            t.close(heroEquity("8c 8d", vs: "As Kh"), 0.545, 0.015, "small pair vs two overs")
        }

        t.test("identical hands split") {
            t.close(heroEquity("As Ks", vs: "Ah Kh"), 0.50, 0.02)
        }

        t.test("equity falls as opponents are added") {
            var rng = SeededRandom(seed: 11)
            let hero = [Card].parse("Ac Ad")
            var previous = 1.0
            for count in 1...5 {
                let e = EquityCalculator.equity(hero: hero, opponentCount: count, trials: 20_000, rng: &rng).equity
                t.lessThan(e, previous, "aces should win less often against \(count) opponents")
                previous = e
            }
            t.greaterThan(previous, 0.35, "aces should still be ahead five-handed")
        }

        t.test("drawing dead is zero") {
            t.close(heroEquity("7c 2d", vs: "Ks Kd", board: "Kc Kh 9s 9d 4c", trials: 500), 0.0, 0.0001)
        }

        t.test("the nuts is one") {
            t.close(heroEquity("As Ks", vs: "2c 2d", board: "Qs Js Ts 4h 3d", trials: 500), 1.0, 0.0001)
        }

        t.test("a chopped board is a tie") {
            // Both play the board: broadway with no flush possible.
            t.close(heroEquity("2c 3d", vs: "4c 5d", board: "As Kh Qd Jc Ts", trials: 500), 0.5, 0.0001)
        }

        t.test("same seed gives the same answer") {
            t.equal(heroEquity("As Ah", vs: "Kc Kd", trials: 5_000, seed: 99),
                    heroEquity("As Ah", vs: "Kc Kd", trials: 5_000, seed: 99))
        }

        t.test("win, tie and lose sum to one") {
            var rng = SeededRandom(seed: 3)
            let r = EquityCalculator.equity(hero: [Card].parse("Ah Kd"), opponentCount: 3, trials: 5_000, rng: &rng)
            t.close(r.win + r.tie + r.lose, 1.0, 0.0001)
        }
    }
}

func deckTests(_ t: TestRun) {
    t.suite("deck and shuffle") { t in

        t.test("shuffle is deterministic per seed") {
            var a = SeededRandom(seed: 12345)
            var b = SeededRandom(seed: 12345)
            var c = SeededRandom(seed: 54321)
            var deckA = Deck(rng: &a), deckB = Deck(rng: &b), deckC = Deck(rng: &c)
            let orderA = deckA.deal(52)
            t.equal(orderA, deckB.deal(52), "same seed, same deck")
            t.notEqual(deckC.deal(52), orderA, "different seed, different deck")
        }

        t.test("the deck holds every card exactly once") {
            var rng = SeededRandom(seed: 8)
            var deck = Deck(rng: &rng)
            t.equal(deck.remaining, 52)
            let dealt = deck.deal(52)
            t.equal(Set(dealt).count, 52)
            t.equal(deck.remaining, 0)
        }

        t.test("a stacked deck deals in order, then the rest") {
            var deck = Deck(stacked: [Card].parse("As Ks Qs"))
            t.equal(deck.deal(3).shorthand, "As Ks Qs")
            t.equal(deck.remaining, 49)
        }

        t.test("the shuffle is not biased toward the top") {
            var counts = [Int](repeating: 0, count: 52)
            for seed in 0..<5_200 {
                var rng = SeededRandom(seed: UInt64(seed))
                var deck = Deck(rng: &rng)
                counts[Int(deck.deal().index)] += 1
            }
            let low = counts.min() ?? 0, high = counts.max() ?? 0
            t.greaterThan(low, 55, "some card almost never lands on top")
            t.lessThan(high, 155, "some card lands on top too often")
        }
    }
}
