import Foundation
import PokerCore
import PokerCoach
import PokerSession
import TestKit

/// The free allowance decides whether somebody may play, which makes it the one piece of arithmetic
/// in the app that can lock a paying customer out of a game they bought. Everything below is about
/// the edges: the hand that takes somebody to exactly the limit, the hand after it, what happens
/// when a subscription arrives late, which hands were never supposed to count, and what the tally
/// must refuse to forget.
///
/// Note what is NOT tested here: how many free hands there should be. That is a product decision
/// and the number lives in one place so it can move. What is tested is that the rule holds whatever
/// the number is — every count below is derived from `FreeTrial.freeHands` rather than written out,
/// so changing it cannot quietly turn these into tests of nothing.

private func hand(_ number: Int, kind: String? = nil) -> HandRecord {
    HandRecord(sessionID: "s1",
               sessionStarted: Date(timeIntervalSince1970: 1_780_000_000),
               playedAt: Date(timeIntervalSince1970: 1_780_000_000 + Double(number) * 60),
               handNumber: number,
               mode: "Tournament",
               bigBlind: 100,
               position: "BTN",
               holeCards: "Ah Kd",
               board: "",
               netChips: 0,
               wentToShowdown: false,
               sawFlop: false,
               putMoneyInPreflop: true,
               raisedPreflop: false,
               verdicts: [],
               kind: kind)
}

/// A tally with `count` ordinary hands already on it.
private func tally(played count: Int, store: TallyStore = InMemoryTally()) -> TrialTally {
    let t = TrialTally(store: store, key: "test.handsPlayed")
    for i in 0..<count { t.record(hand(i + 1)) }
    return t
}

func subscriptionTests(_ t: TestRun) {
    t.suite("the free allowance") { t in

        let limit = FreeTrial.freeHands

        t.test("a new player has the whole allowance") {
            t.equal(FreeTrial.entitlement(subscribed: false, handsPlayed: 0),
                    .trial(handsLeft: limit))
        }

        t.test("each ordinary hand spends one") {
            t.equal(tally(played: 3).entitlement(subscribed: false), .trial(handsLeft: limit - 3))
        }

        t.test("the last free hand is still a hand somebody may play") {
            let e = tally(played: limit - 1).entitlement(subscribed: false)
            t.equal(e, .trial(handsLeft: 1))
            t.expect(e.mayPlay, "one hand left has to mean one hand may be played")
        }

        t.test("the hand after the last one closes the table") {
            let e = tally(played: limit).entitlement(subscribed: false)
            t.equal(e, .lapsed)
            t.expect(!e.mayPlay, "the allowance is spent")
        }

        t.test("playing on past the limit does not owe anybody hands") {
            // Nothing stops a sitting finishing after the count is reached, and a negative
            // remainder would print as "-14 hands left" somewhere.
            t.equal(FreeTrial.handsLeft(played: limit + 40), 0)
            t.equal(tally(played: limit + 40).entitlement(subscribed: false), .lapsed)
        }

        t.test("a corrupted count cannot hand out free hands or negative ones") {
            // Whatever is in the store, the answer has to be inside the allowance.
            let store = InMemoryTally()
            store.set(-500, forKey: "test.handsPlayed")
            let t1 = TrialTally(store: store, key: "test.handsPlayed")
            t.equal(t1.handsPlayed, 0)
            t.equal(t1.entitlement(subscribed: false), .trial(handsLeft: limit))
        }

        t.test("the assessment counts like any other hand") {
            // Ken, 2026-09-11: "play 100 hands". The assessment used to be exempt as the
            // demonstration; at a hundred free hands that would have made the trial 140, and the
            // assessment is forty hands of the product whose report names the free habit.
            let t1 = TrialTally(store: InMemoryTally(), key: "test.handsPlayed")
            for i in 1...40 { t1.record(hand(i, kind: HandRecord.assessmentKind)) }
            t.equal(t1.handsPlayed, 40)
            t.equal(t1.entitlement(subscribed: false), .trial(handsLeft: limit - 40))
        }

        t.test("drills and re-tests do count") {
            // An uncounted drill is an unlimited free table for anybody who notices.
            let t1 = TrialTally(store: InMemoryTally(), key: "test.handsPlayed")
            for i in 1...5 { t1.record(hand(i, kind: HandRecord.drillKind)) }
            for i in 6...10 { t1.record(hand(i, kind: HandRecord.retestKind)) }
            t.equal(t1.handsPlayed, 10)
        }

        t.test("subscribing opens the table however many hands are behind you") {
            // Somebody who subscribes, lapses and comes back does not find their own history held
            // against them.
            let e = tally(played: limit * 5).entitlement(subscribed: true)
            t.equal(e, .subscribed)
            t.expect(e.mayPlay, "a subscriber plays")
        }

        t.test("nobody is ever locked out of hands they already played") {
            // Gating a game is a business decision; gating somebody's own record of their own play
            // is a different thing, and this app does not do it.
            for e in [Entitlement.subscribed, .trial(handsLeft: 3), .lapsed] {
                t.expect(e.maySeeHistory, "\(e) must still be able to read their own hands")
            }
        }
    }

    t.suite("the tally that survives") { t in

        t.test("the count outlives the object holding it") {
            // The whole point of a store: close the app, come back, still owe the same hands.
            let store = InMemoryTally()
            _ = tally(played: 7, store: store)
            let reopened = TrialTally(store: store, key: "test.handsPlayed")
            t.equal(reopened.handsPlayed, 7)
        }

        t.test("starting over does not reset the bill") {
            // `HistoryStore.clear` throws away every saved hand. This is the reason the count is
            // kept separately from them: start over resets your poker record, not your allowance.
            // If this test ever fails, somebody has wired `reset()` into the Start over button and
            // made the free tier unlimited.
            let store = InMemoryTally()
            let counted = tally(played: 30, store: store)

            let history = HistoryStore.inMemory()
            history.clear()

            t.equal(counted.handsPlayed, 30, "clearing the history must not touch the tally")
            t.equal(TrialTally(store: store, key: "test.handsPlayed").handsPlayed, 30)
        }

        t.test("the count only ever goes up") {
            let store = InMemoryTally()
            let counted = tally(played: 4, store: store)
            counted.record(hand(99, kind: HandRecord.assessmentKind))  // counts like any hand
            t.equal(counted.handsPlayed, 5)
            counted.record(hand(100))
            t.equal(counted.handsPlayed, 6)
        }

        t.test("two tallies over one store agree") {
            // Nothing caches. If the paywall and the table each hold their own tally they must not
            // disagree about how many hands are left.
            let store = InMemoryTally()
            let a = TrialTally(store: store, key: "test.handsPlayed")
            let b = TrialTally(store: store, key: "test.handsPlayed")
            a.record(hand(1))
            t.equal(b.handsPlayed, 1, "b must see the hand a counted")
        }

        t.test("reset exists for support, and works") {
            let counted = tally(played: 12)
            counted.reset()
            t.equal(counted.handsPlayed, 0)
        }
    }

    t.suite("warning before the allowance runs out") { t in

        t.test("a subscriber is never warned") {
            t.expect(!FreeTrial.shouldWarn(.subscribed))
        }

        t.test("nothing is said while the end is far off") {
            t.expect(!FreeTrial.shouldWarn(.trial(handsLeft: FreeTrial.warnWithHandsLeft + 1)))
        }

        t.test("the warning starts before the table closes, not at it") {
            // The whole point is that the paywall is not the first anybody hears of the price.
            t.expect(FreeTrial.shouldWarn(.trial(handsLeft: FreeTrial.warnWithHandsLeft)))
            t.expect(FreeTrial.shouldWarn(.trial(handsLeft: 1)))
        }

        t.test("once the table has closed the paywall is the message") {
            // A warning stacked on top of the thing it was warning about is noise.
            t.expect(!FreeTrial.shouldWarn(.lapsed))
        }

        t.test("the warning arrives while there are still hands to play") {
            t.greaterThan(FreeTrial.warnWithHandsLeft, 0,
                          "a warning with no hands left behind it is not a warning")
            t.lessThan(FreeTrial.warnWithHandsLeft, FreeTrial.freeHands,
                       "warning from the first hand is just a price tag on the front door")
        }
    }
}
