import Foundation
import PokerCore
import PokerCoach
import PokerSession

/// Every sentence the screens write for a person to read.
///
/// It is gathered here for one reason: until it was, no test could read a word of it. The plain
/// English rule was enforced across the coach, the leak catalogue, the assessment and the drills —
/// every layer with a testable string on it — and stopped dead at the screens, which is where most
/// of the writing actually lives. A commit whose whole purpose was removing one poker word left
/// that same word sitting in a `Text(...)` two files away, and nothing noticed, because nothing
/// could look. `PokerTests` walks `Copy.prose` now, so a poker word typed into a screen fails the
/// suite in the same breath as one typed into a leak write-up.
///
/// **What is in here and what is not.** The prose is: sentences, paragraphs, the subtitle under a
/// panel heading, the note that explains why a number is missing. The things left where they were
/// are buttons ("Next hand", "Start again"), column headings ("SEAT", "HANDS"), and anything that
/// is really a number with a word beside it ("\(n) hands"). Those are labels rather than writing —
/// there is no sentence in them to get wrong, and moving them here would bury the writing in a list
/// of nouns. The limit is worth knowing: a poker word used as a *button label* is not covered by
/// the suite, and would have to be caught by reading.
///
/// Sentences that need a number in them are functions. Each one appears in `prose` filled in with a
/// plausible value, because the test reads the finished sentence rather than the format string.
public enum Copy {

    // MARK: - Home

    public static let appTagline = "Play, and find out what your game is costing you."

    /// Both of these name the hands behind the number, because the ladder prints a rating of its
    /// own — over the hands at the table you are on now — and the two are visible at once.
    public static let ratingSettling =
        "Every hand you have ever played, and still settling — play more to firm it up."
    public static let ratingSettled =
        "Every hand you have ever played, rated by what your mistakes cost."
    /// Before there is a single hand to rate. The app has never seen this person play, so it says
    /// so rather than drawing a 50 in inch-high type beside "Solid amateur" — which is the same
    /// overclaim the statistics panel three inches away already refuses to make, where a nought
    /// with no hands behind it is printed as a dash on the grounds that nought reads as a result
    /// somebody earned. Being called a solid amateur off nothing is not a result either.
    public static let ratingUnknown =
        "Nothing to rate yet. Play a hand and this fills in — a rating off no hands would be a "
        + "number about nobody."

    /// Which of the three the rating card and the sidebar are entitled to say. Written once because
    /// both of them show the same number and used to disagree about when it means anything.
    public static func ratingSubtitle(handsBehindIt: Int, settled: Bool) -> String {
        guard handsBehindIt > 0 else { return ratingUnknown }
        return settled ? ratingSettled : ratingSettling
    }

    public static let tournamentOffer = "blinds climb, nine players, three paid"

    // MARK: The cash app

    public static let cashSitDownTitle = "Sit down"
    public static let cashSitDownButton = "Sit down"
    public static let cashStakesLabel = "Blinds"
    public static let cashStructureLabel = "How much you can bet"
    /// The one-line offer under the button: "25/50, no-limit, 100 big blinds to sit down with."
    public static func cashOfferLine(stakes: String, structure: String, buyInBB: Int) -> String {
        "\(stakes), \(structure), \(buyInBB) big blinds to sit down with."
    }
    /// What each structure means, in a sentence a person can pick from.
    public static func structureName(_ structure: BettingStructure) -> String {
        switch structure {
        case .fixedLimit: return "Fixed-limit"
        case .potLimit: return "Pot-limit"
        case .noLimit: return "No-limit"
        }
    }
    public static func structureExplained(_ structure: BettingStructure) -> String {
        switch structure {
        case .fixedLimit: return "Every bet is one set size, and a round stops after a bet and three raises."
        case .potLimit: return "You can bet anything up to what is in the pot."
        case .noLimit: return "You can bet anything up to everything you have."
        }
    }
    public static let cashHowItWorks =
        "The blinds stay where you set them. Everybody sits down with a hundred big blinds and "
        + "keeps what they win from hand to hand; anyone who loses the lot buys back in for the "
        + "same again. Leave whenever you like — that is cashing out."
    /// "Last session: won 12 big blinds over 84 hands, 14 for every 100. Bought back in once."
    public static func cashResultLine(netBB: Double, hands: Int, per100: Double, rebuys: Int) -> String {
        let won = netBB >= 0 ? "won" : "lost"
        let amount = String(format: "%.0f", abs(netBB))
        let rate = String(format: "%+.0f", per100)
        var line = "Last session: \(won) \(amount) big blinds over \(Self.hands(hands)), \(rate) for every 100."
        if rebuys == 1 { line += " Bought back in once." }
        if rebuys > 1 { line += " Bought back in \(Self.times(rebuys))." }
        return line
    }
    public static let cashResultNone = "Nothing played yet. What you win and lose is counted in big blinds, the way cash players count it."
    /// Fixed-limit: the buttons carry the one size a bet has.
    public static func fixedBetTitle(_ amount: Int) -> String { "Bet \(amount)" }
    public static func fixedRaiseTitle(_ to: Int) -> String { "Raise to \(to)" }
    public static let bettingCapped = "Capped — no more raising this round"
    public static let cashStructureReadout = "Limit"

    public static let assessmentOffer =
        "A measured sitting rather than a game. Each hand puts you in whichever situation I know "
        + "least about, and at the end you get a starting rating and the first list of what your "
        + "game is actually costing you."

    public static func assessmentLength(_ hands: Int) -> String {
        "About \(hands) hands, roughly twenty-five minutes."
    }

    public static func assessmentStoppedAt(_ hands: Int) -> String {
        "You stopped \(Copy.hands(hands)) in. It carries on from there."
    }

    /// The starting rating, named as what it is: the number from that one sitting, which never
    /// moves. Ken read this card's 54 and the rating card's number as the same thing and asked why
    /// one of them did not update — so each card says which hands it is counting and that the
    /// other one exists.
    public static func assessmentResult(rating: Int, label: String, low: Int, high: Int,
                                        hands: Int) -> String {
        "Your starting rating from those \(Copy.hands(hands)): \(rating) — \(label). Those hands "
            + "support somewhere between \(low) and \(high). This card does not change; the "
            + "rating card below follows every hand since."
    }

    /// When the sitting was played, for the card's subtitle.
    public static func assessmentPlayedOn(_ date: Date) -> String {
        "Played on \(Copy.dayAndMonth(date))."
    }

    /// The report's title the one time it opens by itself, at the end of the sitting. Being taken
    /// off the felt without a word read as being thrown out of the game; this says why it happened.
    public static func assessmentJustFinishedTitle(hands: Int) -> String {
        "Your \(hands) hands are done — here is where your game is now"
    }

    /// "10 September", the way a person says a date.
    public static func dayAndMonth(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.dateFormat = "d MMMM"
        return formatter.string(from: date)
    }

    /// What the rating is made of, in the units the arithmetic actually uses. The number on its own
    /// read as a verdict from nowhere; this is the figure it is worked out from.
    public static func ratingCostLine(costPer100: Double) -> String {
        "Your mistakes cost about \(Int(costPer100.rounded())) big blinds for every 100 hands."
    }

    /// A zero needs a word beside it, or it reads as "you have no rating".
    public static func ratingLabel(value: Int, label: String) -> String {
        value == 0 ? "\(label) — the bottom of the scale" : label
    }

    /// The hands the rating is worked out from, saying what is left out of it: drill and re-check
    /// hands are practice at one situation with you watching for it, and are kept out on purpose.
    public static func ratingHandsWatched(_ n: Int) -> String {
        "\(Copy.hands(n)) watched (practice hands are left out)"
    }

    /// The three hand counts on Home are three different things, and this is the one that adds
    /// them up: everything ever saved, split into hands that count towards the rating and hands
    /// that were practice — drills and re-checks. Assessment hands count as played.
    public static func handsSavedTitle(saved: Int, played: Int, practice: Int) -> String {
        practice == 0
            ? "\(Copy.hands(saved)) saved"
            : "\(Copy.hands(saved)) saved — \(played) played, \(practice) practice"
    }

    public static let focusNone = "Nothing confirmed yet."
    public static let focusWorst = "Your most expensive habit right now."
    public static let focusEmpty =
        "Play some hands and the coach will tell you which mistakes are habits rather than "
        + "accidents. It will not name one until it has seen enough of the same spot to be sure."

    /// The line over the panel that reports the sitting on the table right now.
    ///
    /// Built from the hands written down for this sitting with the arranged ones left out, which
    /// is what every other screen in the app counts. The panel used to read the coach's own memory
    /// instead, and that is not the sitting: it is everything since the app opened, so a
    /// tournament, an assessment and two hundred drill hands were being added together, priced per
    /// hundred and reported as one evening's poker.
    public static func sittingLine(hands: Int, flagged: Int) -> String {
        guard hands > 0 else { return "Nothing played this sitting yet." }
        let played = "\(hands) hand\(hands == 1 ? "" : "s") this sitting"
        // Under the floor the cost per hundred is a dash, and a dash with nothing said about it
        // reads as a fault in the app rather than as a refusal.
        let thin = hands < GameAnalysis.costFloor
            ? " Too few hands yet to spread a cost over a hundred of them."
            : ""
        guard flagged > 0 else { return "\(played), nothing worth flagging.\(thin)" }
        return "\(played). \(flagged) decision\(flagged == 1 ? "" : "s") cost you money.\(thin)"
    }

    public static let handsSavedSubtitle = "Kept from every sitting you have ever played."
    public static let handsSavedBlurb =
        "Most of what there is to learn here lives in the pattern across all of them rather than in "
        + "any single hand. My game reads the lot and tells you where the money actually goes."

    // MARK: - The hand review

    public static let nothingToFlag =
        "The coach had nothing to flag in this hand. It only speaks when it is confident a decision "
        + "cost you money — silence here means the decisions were reasonable, not that it was not "
        + "watching."
    public static let nothingToFlagShort = "The coach had nothing to flag in this hand."
    public static let quietOnCloseSpots = "The coach stays quiet on close spots by design."

    /// What a hand says about itself when the coach kept quiet and the scorecard still counted
    /// something in it.
    ///
    /// The bar for speaking is higher than the bar for counting, deliberately: a third of a big
    /// blind is never worth stopping somebody over and is ruinous forty times an hour. That gap is
    /// where the app used to contradict itself. A player whose named habit was a cheap one — too
    /// many hands, limping — opened the Hands list to check the coach's work and read "clean"
    /// against the very hands the habit was counted from. The rule about staying quiet at the table
    /// stands; what stopped is the screens afterwards asserting there was nothing there.
    ///
    /// It says nothing about where those decisions are drawn, because it is used on two screens
    /// that draw them differently — the hand detail marks each one, the review at the table does
    /// not — and a sentence pointing at something that is only sometimes there is worse than one
    /// that does not point.
    public static func nothingFlaggedButCounted(_ decisions: Int) -> String {
        let subject = decisions == 1
            ? "One decision was"
            : "\(decisions) decisions were"
        return "Nothing here was worth stopping you over. \(subject) still counted towards a habit "
            + "on your My game list."
    }

    /// One such decision, said where the player is reading about it.
    public static func countedTowards(_ leak: String) -> String {
        "Too small to stop you over, and counted all the same. It is one of the decisions behind "
            + "“\(leak)” on your My game list."
    }
    /// The same, where the habit it belongs to is one this build no longer recognises — an old hand
    /// whose leak has since been renamed. The decision still counted; there is no name to give it.
    public static let countedTowardsUnnamed =
        "Too small to stop you over, and counted all the same. It is one of the decisions behind a "
        + "habit on your My game list."

    /// Which of three things a per-hand screen says about one decision.
    ///
    /// The rule lives here, in one place, because three screens ask it — the Hands list, the hand
    /// detail and the replay — and they were each asking it in their own words. Two of the three
    /// asked whether the coach spoke, which is a different question from whether the decision
    /// counted, and answered the second question with the first one's answer.
    public enum DecisionNote: Equatable {
        /// The coach spoke about it at the table. Its own write-up says what it is.
        case flagged
        /// The coach kept quiet and the scorecard counted it towards a habit.
        case counted
        /// Close enough that calling it a mistake would be guessing.
        case tooClose
    }

    public static func decisionNote(spoke: Bool, countsTowardsLeak: Bool) -> DecisionNote {
        if spoke { return .flagged }
        return countsTowardsLeak ? .counted : .tooClose
    }

    /// The badge beside a hand in the list. Three states rather than two: a hand can be one the
    /// coach kept quiet on and still be one the scorecard counted a habit from, and "clean" is a
    /// claim about the second question rather than the first.
    public static func handBadge(flagged: Int, counted: Int) -> String {
        if flagged > 0 { return "\(flagged) flagged" }
        return counted > 0 ? "\(counted) counted" : "clean"
    }

    /// The line above the decisions in the hand detail. Nil where the coach flagged something —
    /// there, the write-ups underneath say it better than a summary would.
    public static func handSummary(flagged: Int, counted: Int) -> String? {
        guard flagged == 0 else { return nil }
        return counted == 0 ? nothingToFlagShort : nothingFlaggedButCounted(counted)
    }

    // MARK: - My game

    /// The line under the heading, which sits directly above the scope bar — so it says what is
    /// kept and leaves what is counted to the control that answers it. It used to end "every hand
    /// you have ever played is counted here", one line above a bar that can be set to a single
    /// sitting, and the subtitle further down said the opposite and was the one telling the truth.
    public static let myGameIntro =
        "This is the part worth coming back to. One hand tells you almost nothing — there is too "
        + "much luck in a single hand for it to mean much. A few hundred will tell you things about "
        + "how you play that you did not know, and every hand you play is kept, so this only gets "
        + "clearer. Pick which of them you are looking at below."

    public static let costingYouSubtitle =
        "Your weaknesses, worst first, ranked by what they have actually cost. Counted from every "
        + "hand above, so it follows whichever sitting you are looking at and is still here tomorrow."

    public static let tooSoonSubtitle =
        "Heading the wrong way, on too few chances to be sure. Named so you know what is being kept "
        + "an eye on — not findings."

    public static let everythingMeasuredSubtitle = "The full list, including what you are doing well."

    public static let numbersMeasured = "This is measured, not estimated."
    public static func numbersAcross(_ sittings: Int) -> String { "Across \(sittings) sittings." }

    /// The seat rows, explained without a sentence that stops being true at the end of a
    /// tournament.
    ///
    /// It used to say "seat 1 puts in the small blind", which is right at any table of three or
    /// more and wrong two-handed, where the dealer pays the small blind and the other seat is
    /// numbered 1. Every tournament finishes two-handed, so the table below produces those rows on
    /// the way past. The highest number being the dealer holds at every size, so that is what the
    /// sentence leans on now.
    public static let bySeatSubtitle =
        "Where the money comes from. The highest number is the dealer and acts last, and the seats "
        + "that pay before the cards come out lose money by definition; the late numbers are where "
        + "it should be earned back. Pick a row to see where it sits."

    public static let whatItCostSubtitle = "The mistakes the coach was confident about, worst first."

    /// Why the habits and the rounds of betting in the same panel do not add up.
    ///
    /// How often a habit happens is counted over assessment hands and what it cost is not, which is
    /// right — being put in a situation on purpose does not change how often you get it right when
    /// you are in it, but it does change what the evening won or lost. Two hand sets an inch apart
    /// with nothing saying so is how a screen quietly stops being believed.
    ///
    /// This used to be drawn inside the branch that draws the round-by-round tiles, and those tiles
    /// are built from ordinary hands only — so on an assessment-only history there were no tiles,
    /// no note, and a list of habits made entirely of the hands the note exists to explain. It sits
    /// outside that branch now.
    public static func roundsLeaveOutArranged(_ hands: Int) -> String {
        "\(hands) arranged hand\(hands == 1 ? "" : "s") — an assessment — "
        + "\(hands == 1 ? "is" : "are") counted in the times above and left out of the money, here "
        + "and everywhere else on this screen. What a hand you were put into won or lost is not a "
        + "fair sample of anything; how often you got the decision right in it still is."
    }
    public static let worthAnotherLookSubtitle = "The two biggest pots in the hands you are looking at."

    public static let thinSeatRows = "Greyed rows have too few hands to read anything into yet."

    public static let noFlaggedHands =
        "Nothing flagged in these hands. The coach only speaks when it is confident a decision cost "
        + "you money."

    /// The line under the headline numbers. Which of the four is owed depends only on how many
    /// hands are behind them, so the choice sits here where a test can read it rather than in the
    /// view — where the empty one was written and then rendered inside a branch that only ran once
    /// there were hands, and so could never appear.
    ///
    /// `arranged` is what stops the fourth case reading as the first. Straight after a forty-hand
    /// assessment — the first thing anybody does on a fresh install — this panel counted nought
    /// hands, because arranged hands are kept out of every number in it, and then told the player
    /// "nothing has been played". They had just played forty.
    public static func statisticsFootnote(hands: Int, arranged: Int = 0, enough: Bool) -> String {
        if hands == 0 { return arranged > 0 ? statisticsAllArranged(arranged) : statisticsEmpty }
        return (enough ? statisticsSettled : statisticsThin(hands)) + " " + flaggedCostNote
    }

    public static let statisticsEmpty =
        "Nothing has been played in what you are looking at, so there is no win rate to work out. "
        + "The dashes above are there instead of noughts on purpose: nought big blinds per hundred "
        + "hands is a result somebody earned, and no hands at all is not one."
    public static func statisticsAllArranged(_ hands: Int) -> String {
        let played = hands == 1
            ? "The one hand you are looking at was arranged"
            : "All \(Copy.hands(hands)) you are looking at were arranged"
        return "\(played) rather than dealt: the seat you sat in and the chips in front of "
            + "everybody were chosen for you, so what they won or lost is no sort of win rate and "
            + "the dashes above say so. What those hands do say is in the habits further up."
    }
    public static func statisticsThin(_ hands: Int) -> String {
        "VPIP is how often you play a hand, PFR how often you raise first. Win rate over \(hands) "
        + "hands is still mostly luck — under a couple of hundred hands, trust the coach's verdicts "
        + "over the number above."
    }
    public static let statisticsSettled =
        "VPIP is how often you play a hand, PFR how often you raise first. A tight, aggressive "
        + "nine-handed game usually sits near 18 / 14."

    /// Why the money in this panel is smaller than the money at the top of the screen.
    ///
    /// Two figures in big blinds per hundred hands, a few inches apart, over different sets of
    /// decisions. This one is the mistakes the coach spoke about; the habits above also count the
    /// cheap decisions it stays quiet on, so their total is always the larger of the two, and the
    /// gap grows with exactly the habits the scorecard exists to catch.
    ///
    /// Written so it is true on a screen with no habits on it as well. "Their total is the bigger
    /// number" points at a total that is not always there.
    public static let flaggedCostNote =
        "The cost above counts only the decisions the coach spoke about. A habit named at the top "
        + "of this screen is priced over the small ones it stays quiet on as well, so that figure "
        + "is always the larger of the two."

    // MARK: - The scorecard

    public static let scorecardEmpty =
        "A leak is the same mistake in the same spot, often enough to be a habit rather than an "
        + "accident. Nothing is put on this list until there have been enough chances to be sure of "
        + "it, so an empty list is good news."

    /// Both halves are needed. The list draws a row for every habit in the catalogue, and the ones
    /// that have never come up used to get a short green bar of their own — the same picture as a
    /// habit measured and going well, directly above a sentence saying a short bar means the thing
    /// is working. Those rows have no bar at all now, and this says what that means.
    public static let barMarkNote =
        "The mark on each bar is the line past which this app will call something a habit. It is "
        + "set well clear of where a steady player actually sits, so that a bad run of cards is "
        + "never mistaken for one. A bar that stops short of the mark is a part of your game that "
        + "is working. A row with no bar at all has not come up yet, which is a different thing "
        + "from going well."

    public static let costNotKnown = "cost not known"

    // MARK: - The hand list

    public static let flaggedOnlyToggle = "Only hands the coach flagged"
    public static let noHandsYet = "No hands yet"
    public static let noHandsYetBlurb = "Sit down and play — every hand is kept, and stays here between visits."
    public static let nothingFlagged = "Nothing flagged"
    public static let nothingFlaggedBlurb =
        "Every hand you are looking at was played well enough that the coach kept quiet."

    public static let pickAHand = "Pick a hand"
    public static let pickAHandBlurb =
        "Every hand you have played, with what the coach thought at each decision — including the "
        + "ones it chose not to comment on. Hands played recently can be stepped through one action "
        + "at a time, forwards or back."

    public static let replayOffer = "one round of betting at a time, forwards or back"
    public static let noComment = "no comment"

    // MARK: - Playing a hand back

    public static let cannotReplay = "This hand cannot be played back"
    public static let cannotReplayBlurb =
        "It was saved before the app started writing down every action in a hand. The summary above "
        + "is everything that was kept. Hands you play from now on can be stepped through in full."

    public static func coachSaidNothing(about taken: String) -> String {
        "Nothing to say about this one. You chose to \(taken), which was close enough to the best "
        + "answer that calling it a mistake would be guessing."
    }

    // MARK: - The assessment

    public static func assessmentIntro(_ hands: Int) -> String {
        "You play \(hands) hands and I watch. At the end you get a starting rating and the first "
        + "honest list of what your game is costing you."
    }

    public static let everyHandIsChosen = "Every hand is chosen"
    public static let everyHandIsChosenBlurb =
        "I pick the seat you sit in and how many big blinds everybody has in front of them, and I "
        + "change them between hands. That is so I can put you in the situations I have not seen you "
        + "handle yet instead of waiting for the deal to get round to them. What each hand is "
        + "looking at I keep to myself until the end, because being told what is being measured "
        + "changes it — you get the whole list in the report."

    public static let cardsNotChosen = "The cards are not chosen"
    public static let cardsNotChosenBlurb =
        "Every hand comes off a fresh shuffle. I never give you a hand to make a point with, because "
        + "the moment I did, everything at the end of this would be worth nothing."

    public static let stopWhenever = "You can stop whenever you like"
    public static let stopWheneverBlurb =
        "Walking away costs nothing. The hands you played are kept, and picking it back up carries "
        + "on from where you left it."

    public static let noPretending = "It will not pretend to know more than it does"
    public static func noPretendingBlurb(_ hands: Int) -> String {
        "\(hands) hands is a short look at anybody. The report says what it could measure, what it "
        + "could not get near, and how far out its own numbers could be."
    }

    public static let assessmentFinishedTitle = "Where your game is now"
    public static let assessmentStoppedTitle = "As far as you got"
    public static let startingRating = "Your starting rating"
    public static func ratingRangeLine(low: Int, high: Int, hands: Int) -> String {
        "\(low) to \(high) is what \(Copy.hands(hands)) support"
    }
    public static let alreadyClear = "What is already clear"
    public static let whereToLookFirst = "Where I would look first"
    public static let whereToLookFirstNote =
        "Not proved. These happened often enough to be worth your attention and not often enough "
        + "for me to call them habits."
    public static let nothingToName = "Nothing to name"
    public static let whatIGotToLookAt = "What I got to look at"
    public static func decisionsAcrossHands(decisions: Int, hands: Int) -> String {
        "\(decisions) decisions across \(Copy.hands(hands))."
    }
    public static let whatIArranged = "What I arranged, and what I did not"

    // MARK: - The drills

    public static func drillTitle(_ leak: String) -> String { "Drill: \(leak)" }
    public static let checkingBackTitle = "Checking back"
    public static let checkingBackHint =
        "You cleared this one. This is the same situation again, now you have stopped thinking "
        + "about it."

    public static let sameSituation = "The same situation, over and over"
    public static func sameSituationBlurb(seat: String, depthInBB: Int, lean: String) -> String {
        "I put you in \(seat) with \(depthInBB) big blinds in front of everybody, \(lean), because "
        + "that is the arrangement most likely to hand you this decision. If it stops producing it, "
        + "I change the arrangement rather than waiting."
    }

    // MARK: Seat drills — where you lose most

    public static let whereYouLoseMost = "Where you lose most"
    public static let whereYouLoseMostSubtitle =
        "Which seat your decisions go wrong in most often, off every hand the deal put you in — "
        + "never the ones the app arranged."
    public static let seatDrillTitle = "Drill this seat"
    public static func drillSeatTitle(_ seat: String) -> String { "Drill \(seat)" }
    public static let seatDrillLocked = "Included with a subscription"
    public static let seatDrillHint =
        "Every hand from the one seat, until the decisions you make there go wrong no more often "
        + "than a steady player's do."
    public static let sameSeat = "The same seat, every hand"
    public static func sameSeatBlurb(seat: String, lean: String) -> String {
        "I put you in \(seat) every hand, with 100 big blinds in front of everybody, \(lean). "
        + "The seat is the whole of what is arranged; what comes at you from it is up to the deal."
    }
    public static let seatCounted = "Everything you decide there counts"
    public static let seatCountedBlurb =
        "A drill at a habit counts one situation. A drill at a seat counts every decision the "
        + "coach looks at from it, whatever the habit — that is what made the seat expensive."
    public static let seatDrillsHeading = "By seat"
    public static let seatDrillsFooter =
        "A seat drill sits you in one seat every hand. The seat to start with is the one My game "
        + "says you lose most from."
    public static func seatBannerLine(_ seat: String, watching: String?) -> String {
        guard let watching else { return "\(seat) every hand." }
        return "\(seat) every hand, watching for \(watching)."
    }

    public static let cardsStillNotChosen = "The cards are still not chosen"
    public static let cardsStillNotChosenBlurb =
        "Every hand comes off a fresh shuffle, exactly as it does at a normal table. I arrange the "
        + "situation and never the hand you are holding in it."

    public static let checkTakesAsLong = "It takes as long as it takes"
    public static func checkTakesAsLongBlurb(_ chances: Int) -> String {
        "\(chances) chances at it and then an answer, either way. A check that ran until it liked "
        + "the result would not be a check."
    }

    public static let drillEndsWhenStopped = "It ends when you have actually stopped"
    public static let drillEndsWhenStoppedBlurb =
        "Not after a set number of hands. It ends when you have had enough chances for the rate to "
        + "be worth reading at all, and that rate is counted with one slip always added in, so a "
        + "clean run cannot finish it early. Get one wrong and it takes a few more, which is the "
        + "whole point of it. If it will not clear, it says so rather than letting you off."

    public static let drillHandsKeptOut = "These hands are kept out of your numbers"
    public static let drillHandsKeptOutBlurb =
        "Practice at one situation, knowing it is coming, is not the same as how you play when it "
        + "turns up unannounced. So drill hands do not touch your win rate and they do not touch "
        + "your leaks. What counts is the check a few days from now, once you have stopped working "
        + "on it."

    public static let whatHappened = "What happened"
    public static func drillHandsAndChances(hands: Int, chances: Int) -> String {
        "\(Copy.hands(hands)), \(Copy.times(chances)) in the situation."
    }

    public static let willCheckBack = "I will check back"
    public static func inAboutDays(_ days: Int) -> String { "In about \(days) days." }
    public static let willCheckBackBlurb =
        "An evening of getting something right while you are watching for it is not the same as "
        + "having stopped doing it. So this goes in the diary rather than in the past: in a few "
        + "days, once you have stopped working on it, you get the same situation again, and that is "
        + "the one that decides whether it comes off your list. It will be waiting on the home "
        + "screen when it is due, however long the app is shut."

    /// What is owed after a check that held up, which is not the same sentence as after a drill.
    /// The gap has just doubled, and a screen that says "I will check once more" and then does not
    /// say when leaves the player with nothing to plan around.
    public static let checkHeldBlurb =
        "It has held up once, so the gap doubles before the next one. That is on purpose: the first "
        + "check asks whether the fix survived the week, the third asks whether it survived not "
        + "thinking about it at all. Nothing is owed from you in the meantime — it will be waiting "
        + "on the home screen when it is due, however long the app is shut."

    public static let backOnYourList = "Back on your list"
    public static let backOnYourListBlurb =
        "This is why the check exists. It is on the My game screen again, with the same write-up it "
        + "had before, and you can drill it again whenever you like — nothing about the first drill "
        + "was wasted."

    public static let whatToDoAboutIt = "What to do about it"
    public static let stoppingCostsNothing =
        "Nothing is lost by stopping here. The hands are written down and picking the drill back up "
        + "carries on from where you left it."

    public static let checkIsDue = "A check is due"
    public static func chancesAtTheSameSituation(_ chances: Int) -> String {
        "\(chances) chances at the same situation"
    }
    public static let drillHalfDone = "A drill left half done"
    // MARK: Practice

    public static let practiceTitle = "Practice"
    public static let practiceSubtitle =
        "Pick a situation and play nothing else until it stops needing to be worked out."
    public static let practicePanelSubtitle = "Choose a situation and drill it, whenever you like."
    public static let practiceBlurb =
        "The coach hands you drills once it has watched you long enough to be sure about a habit. "
        + "This is the other way in: anything on the list can be practised now, whether or not it "
        + "has ever been a problem for you."
    public static let practiceOpen = "Choose something to practise"
    public static let practiceSomethingElse = "Practise something else"
    public static let practiceStart = "Practise this"
    public static let practiceBeforeFlop = "Before the flop"
    public static let practiceOnTheFlop = "On the flop"
    public static let practiceLaterStreets = "Turn and river"
    public static let practiceFooter =
        "Hands played in a drill are kept apart from your real ones. They never move your rating, "
        + "and they are left out of everything on My game — an arranged table would flatter or "
        + "punish those numbers without either being true."

    /// A word for a drill somebody has already been through, or nothing at all where they have not.
    static func practiceMark(_ stage: DrillBook.Stage) -> String? {
        switch stage {
        case .notStarted: return nil
        case .running: return "In progress"
        case .due: return "Check due"
        case .holding: return "Cleared"
        case .reopened: return "Back on your list"
        case .stalled: return "Unfinished"
        }
    }

    public static let practiseOneThing = "Practise one thing"
    public static let practiseOneThingBlurb =
        "A set of hands that puts you in this one situation over and over, until you have got it "
        + "right often enough for that to mean something. Then I check back in a few days to see "
        + "whether it stuck."

    public static func drillHintLine(_ hint: String) -> String { "Drill: \(hint)" }

    // MARK: - The ladder

    public static func ladderIntro(_ tables: Int) -> String {
        "\(tables) tables, each one harder than the last — and what makes one harder than the next "
        + "is who is sitting at it. Beat a table over enough hands and you are moved up. Stop "
        + "beating it and you are moved back down, which is the only way the rung you are on means "
        + "anything at all."
    }

    public static func handsAtThisTable(played: Int, needed: Int) -> String {
        "\(played) of \(Copy.hands(needed)) at this table"
    }

    public static let nineSeatsBlurb =
        "Nine seats — you and eight of them. Seats 1 and 2 pay before the cards come out, 9 is the "
        + "dealer, and the whole ring shifts one seat every hand."

    public static func nextTableIs(_ tier: String) -> String { "Your next table is \(tier)." }
    public static func nextTableIsAgain(_ tier: String) -> String { "Your next table is \(tier) again." }

    public static let topOfTheLadder = "The top of the ladder"
    public static let whatItTakesToMoveUp = "What it takes to move up"
    public static let costingAtThisTable = "What is costing you at this table"
    public static let everyRung = "Every rung"
    public static func moveUpAt(_ rating: Int) -> String { "move up at \(rating)" }

    public static let sameEightPeople = "The same eight people, playing their styles better."
    public static func whoChanges(leaving: String, arriving: String) -> String {
        "Walking in there, \(leaving) get up and \(arriving) sit down — and they play a little "
        + "better than these ones do. Who is at the table is the bigger half of that by a long way."
    }
    public static let nobody = "nobody"

    // MARK: - The table picture

    public static let seatsPayBeforeCards =
        "Seats 1 and 2 put money in before anyone has seen a card — that is the chip. D is the "
        + "dealer, and it moves one seat along every hand, so everybody sits in every seat."

    // MARK: - Settings

    public static let coloursSubtitle = "Pick whichever you can read most easily."
    public static let textSizeSubtitle = "How big everything is written, the coach's write-ups most of all."
    public static let soundSubtitle = "Chips, knuckles on the felt, cards sliding away."
    public static let playTableSounds = "Play table sounds"

    /// "1 hand", "2 hands". Written once because a count of one turns up all over this app — the
    /// first hand of a sitting, a habit seen once, a rating built on a single hand — and every
    /// place that gets it wrong reads as software rather than as somebody talking to you.
    public static func hands(_ n: Int) -> String { n == 1 ? "1 hand" : "\(n) hands" }

    /// The same for the other things counted in ones here.
    public static func sittings(_ n: Int) -> String { n == 1 ? "1 sitting" : "\(n) sittings" }
    public static func times(_ n: Int) -> String { n == 1 ? "once" : "\(n) times" }
    public static func chances(_ n: Int) -> String { n == 1 ? "1 chance" : "\(n) chances" }

    // MARK: Who made it

    public static let madeByTitle = "The (Digital) Ad Lab"
    public static let madeBySubtitle = "Applied data science and AI, by Ken Rona."
    public static var madeByBlurb: String {
        "\(AppVariant.current.displayName) is one of the things built at The (Digital) Ad Lab, "
        + "alongside the Refacto newsletters and a handful of other tools. Everything it knows "
        + "about your game stays on this device: nothing is uploaded, there is no account to make, "
        + "and nobody else can see a hand you have played."
    }
    public static let visitAdLab = "Visit the Ad Lab"
    /// The About link, the page it opens and the privacy page all follow the product, so the
    /// cash app never sends anyone to the tournament app's pages (Apple checks the privacy URL).
    public static var readAboutPokerTutor: String { "About \(AppVariant.current.displayName)" }
    public static let adLabURL = "https://www.thedigitaladlab.com/"
    public static var pokerTutorPageURL: String { AppVariant.current.pageURL }
    public static var privacyURL: String { AppVariant.current.privacyURL }
    public static let privacyLink = "Privacy"
    public static func versionLine(_ version: String) -> String {
        // The LEGAL name, without the parentheses. "The (Digital) Ad Lab" is the brand and belongs
        // in `madeByTitle`; this line is an ownership claim and has to match the entity as it is
        // registered in Georgia, which is what Apple and the App Store will show.
        "\(AppVariant.current.displayName) \(version) — The Digital Ad Lab, LLC"
    }

    // MARK: The basics

    public static let basicsTitle = "The basics"
    public static func basicsSubtitle(_ count: Int) -> String {
        "\(count) things people who have played for years assume everybody knows."
    }
    public static let basicsPanelSubtitle =
        "The short answers nobody says out loud, starting with how big a bet is supposed to be."
    public static let basicsBlurb =
        "Most of what makes somebody hard to play against is not clever. It is a handful of things "
        + "they learned so long ago that they no longer know they know them, and nobody at a table "
        + "will ever explain any of it. This is that list, with the numbers worked through."
    public static let basicsOpen = "Read the basics"
    public static let workedThrough = "Worked through"
    public static let practiseThis = "Practise this"
    public static let basicsFooter =
        "None of these are laws. They are where a good player starts before they have a reason to "
        + "do something else, which is a different thing — and knowing which one you are departing "
        + "from is most of what being good at this means."

    public static func pokerWordsSubtitle(_ count: Int) -> String { "\(count) of them, in plain English." }
    public static let pokerWordsBlurb =
        "Anywhere the app has to use a poker word — the numbers on My game, the old seat names, "
        + "what the coach says about a hand — you can tap it and the meaning opens where you are "
        + "reading. The whole list is here too."

    public static let gettingStarted = "Getting started"
    public static let gettingStartedSubtitle = "The short walk-through you saw the first time."

    public static let startOver = "Start over"
    public static let startOverSubtitle = "Throw away every saved hand and let the coach meet you fresh."
    public static let nothingSavedYet = "There is nothing saved yet."
    /// What "Start over" actually costs, said in the panel as well as in the dialog.
    ///
    /// The rung you are on, the checks waiting on your drills and how far through an assessment you
    /// got are all counted back off the hands themselves — there is no second copy of any of them
    /// anywhere. That is what makes them survive the app being closed, and it is also what makes
    /// clearing the hands clear them. Saying "resets your rating" and stopping there described
    /// about a third of what the button does.
    public static func handsAcrossSittings(hands: Int, sittings: Int) -> String {
        "\(Copy.hands(hands)) across \(Copy.sittings(sittings)), kept since the first one. Clearing them also "
        + "resets your rating, puts you back at the first table on the ladder and cancels any drill "
        + "checks that were waiting, because all three are counted back off the hands themselves."
    }

    public static let whereHandsAreKept = "Where your hands are kept"
    public static let whereHandsAreKeptSubtitle = "They stay put — updating the app does not touch them."
    public static let whereHandsAreKeptBlurb =
        "Every hand is written down as it finishes and kept outside the app itself, so a new version "
        + "arrives without disturbing anything you have played. Resetting is the only thing that "
        + "clears them, and even then the old file is set aside rather than deleted."

    public static let resetTitle = "Reset history?"
    public static let resetWarning =
        "This clears every hand you have played and resets your rating. It also puts you back at "
        + "the first table on the ladder and cancels any drill checks that were waiting, because "
        + "both of those are counted back off the hands themselves. If you are at a table, you "
        + "will leave it. A copy of the old history is kept on disk in case this was a mistake."

    // MARK: - The glossary screen

    public static let pokerWordsTitle = "Poker words"
    public static func pokerWordsIntro(_ count: Int) -> String {
        "\(count) words, in plain English. Anywhere one of these turns up in the app you can tap it "
        + "and get the same answer without leaving what you were reading."
    }
    public static let noWordMatches =
        "No word here matches that. Every term the app uses is on this list, so it is worth trying a "
        + "shorter piece of it."

    // MARK: - The walk-through

    public struct TutorialPage: Sendable {
        public let icon: String
        public let title: String
        public let body: String
    }

    /// The tutorial for the game this product plays.
    public static var tutorial: [TutorialPage] { tutorial(for: AppVariant.current.format) }

    public static func tutorial(for format: AppVariant.Format,
                                structure: BettingStructure = .noLimit) -> [TutorialPage] {
        guard format == .cash else { return tournamentTutorial }
        // At fixed-limit there is no tray to count chips out of — the buttons carry the one size
        // a bet has — so the page about counting chips describes a screen the player never sees.
        return structure == .fixedLimit
            ? cashTutorial.filter { $0.title != chipPageTitle }
            : cashTutorial
    }
    static let chipPageTitle = "Bet by counting out chips"

    /// The cash app's tutorial: the same six things, with the first page saying what a cash table
    /// is, and one more page for the thing that is only true there — the session is yours to end.
    public static let cashTutorial: [TutorialPage] =
        [TutorialPage(icon: "suit.spade.fill",
                      title: "This is a coach, not a card game",
                      body: "You sit down at a cash table with a hundred big blinds against eight "
                          + "opponents, at the stakes and the kind of betting you choose, and "
                          + "afterwards someone tells you which of your decisions cost money. "
                          + "Winning the hand is not the point. Playing it well is — and those "
                          + "are different things more often than you would like. Pick "
                          + "fixed-limit and the coach judges that game, which is a different "
                          + "one: bets have one size, so the price to keep going is nearly "
                          + "always right, and the expensive mistakes are folds."),
         TutorialPage(icon: "banknote.fill",
                      title: "The session is yours to end",
                      body: "The blinds never move and nobody is knocked out. What you win stays "
                          + "in front of you, what you lose can be bought back, and you leave "
                          + "when you like. Your result is counted the way cash players count "
                          + "it: big blinds won or lost, and how many for every hundred hands.")]
        + tournamentTutorial.dropFirst()

    public static let tournamentTutorial: [TutorialPage] = [
        TutorialPage(icon: "suit.spade.fill",
                     title: "This is a coach, not a card game",
                     body: "You play real hands against eight opponents, and afterwards someone "
                         + "tells you which of your decisions cost money. Winning the hand is not "
                         + "the point. Playing it well is — and those are different things more "
                         + "often than you would like."),
        TutorialPage(icon: "hand.thumbsup.fill",
                     title: "Silence is good news",
                     body: "Most hands, nothing will be said. That is deliberate. The coach only "
                         + "speaks when it is confident a decision actually cost you something, "
                         + "because a coach that second-guesses everything is one you stop "
                         + "believing. A hand you played well gets a thumbs up and deals straight "
                         + "on."),
        TutorialPage(icon: "chart.bar.doc.horizontal",
                     title: "The real learning is in My game",
                     body: "One hand tells you almost nothing — poker has too much luck in it for "
                         + "that. Fifty hands start to show a pattern, and a few hundred will tell "
                         + "you things about how you play that you did not know. Every hand is "
                         + "kept, so that picture only gets clearer. Go and look at it often; it is "
                         + "the part of this app that changes how you play."),
        TutorialPage(icon: "circle.grid.3x3.fill",
                     title: "Bet by counting out chips",
                     body: "Pick chips up to build your bet: the hundred twice and the fifty once "
                         + "is 250. Undo puts the last one back. It is a click or two slower than a "
                         + "slider, which is the idea — a bet you had to count out is a bet you "
                         + "thought about."),
        TutorialPage(icon: "person.2.fill",
                     title: "Nobody tells you who is who",
                     body: "The players around you have styles — some far too loose, some who never "
                         + "bluff — but the app will not label them. Working out who you are up "
                         + "against is the skill. As your game improves, the people you are sat "
                         + "with quietly get better too."),
        TutorialPage(icon: "clock.arrow.circlepath",
                     title: "Nothing is thrown away",
                     body: "Every hand you play is saved and stays saved, including through "
                         + "updates. You can go back through any of them, see what the coach "
                         + "thought at each decision, and compare one sitting against everything "
                         + "you have ever played."),
    ]

    // MARK: - The cards

    public static let cardColoursTitle = "Card colours"
    public static let cardColoursSubtitle =
        "Clubs and spades are both black in a traditional deck, and at the size a phone draws them "
        + "the only difference is a small notch at the bottom."
    public static let tapToPutItBack = "Touch anywhere to put it back."
    public static let touchACard =
        "Touch any card on the table to hold it up large enough to read."

    // MARK: - The subscription

    /// Apple rejects a paywall that does not say, on the screen where the buyer taps buy, what it
    /// costs, how long it lasts, and that it renews until cancelled. Those three facts are in
    /// `paywallPrice` and it must not be shortened into something friendlier.

    public static let paywallTitle = "The free hands are used up"
    public static func paywallBlurb(_ played: Int) -> String {
        "That is \(hands(played)) played. A subscription keeps the table open: as many hands as "
        + "you want, every drill, and the coach watching all of them."
    }

    /// The renewal terms, in the buyer's own currency. `price` comes from Apple — never build it.
    public static func paywallPrice(_ price: String) -> String {
        "\(price) a year. It renews every year until you cancel, and you can cancel any time in "
        + "your Apple Account settings."
    }

    /// Said instead of a price when Apple has not answered. A buy button with no price beside it
    /// is somebody being asked to agree to a number nobody has shown them.
    public static let paywallNoPrice =
        "The price could not be loaded just now. Check your connection and try again."

    public static let paywallKeepsYourHands =
        "Everything you have already played stays where it is. Your hands, your numbers and the "
        + "coach's write-ups are yours whether you subscribe or not."

    public static let paywallRestoreNote =
        "Already subscribed, or playing on a new phone? Restore it here."

    /// Ask to Buy, or a bank wanting confirmation. Nothing has been charged and nothing is owed.
    public static let paywallPendingNote =
        "That purchase is waiting for somebody else to approve it, and nothing has been charged. "
        + "The table opens by itself once it goes through."

    /// Said throughout the free hands, so the price is not the first anybody hears of it.
    public static func handsLeftWarning(_ left: Int) -> String {
        "\(left) of your \(FreeTrial.freeHands) free hands left."
    }

    // MARK: The free tier — one habit in full, the rest blurred, one drill

    /// Over the blurred rows, with the count. Said the same way on the scorecard and the report.
    public static func hiddenHabits(_ count: Int) -> String {
        count == 1
            ? "One more habit is in your game. See it, and everything else, with a subscription."
            : "\(count) more habits are in your game. See them all with a subscription."
    }
    /// The one row a free player is shown, when it is not yet proved.
    public static let freeRowNotProved =
        "Your most common habit so far, on too few chances to be sure. Play on and it firms up."
    /// The one row, when it is proved.
    public static let freeRowNamed = "Your most common habit. This is the one to work on first."
    /// What a screen reader says for a blurred row, in place of the name.
    public static let hiddenRowLabel = "A habit, hidden until you subscribe"
    /// The practice button on a locked row.
    public static let practiceLocked = "Included with a subscription"
    /// The Practice screen's line for a free player.
    public static let practiceFreeNote =
        "Your free hands come with the drill for your most common habit. The rest open with a subscription."
    /// Home's "what to work on" for a free player with nothing shown yet.
    public static let focusFreeEmpty =
        "Play your free hands and the coach names your most common habit here."

    public static let subscribedTitle = "Subscribed"
    public static let subscribedSubtitle =
        "Renews every year. You can manage it or cancel it in your Apple Account settings."

    /// Apple's own standard terms. Every subscription paywall has to link to them, and to a privacy
    /// policy — `privacyURL` above is the other half.
    public static let termsURL = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
    public static let termsLink = "Terms of Use"

    // MARK: - Everything above, for the suite

    /// Every sentence on the list, with the ones that take a number filled in.
    ///
    /// The filled-in values are ordinary ones rather than edge cases: the test is reading English,
    /// and a sentence reads the same whether the number in it is 12 or 40.
    public static var prose: [String] {
        var lines: [String] = Basics.allText + [
            cashSitDownTitle, cashSitDownButton, cashStakesLabel, cashStructureLabel,
            cashOfferLine(stakes: "25/50", structure: "no-limit", buyInBB: 100),
            structureName(.fixedLimit), structureName(.potLimit), structureName(.noLimit),
            structureExplained(.fixedLimit), structureExplained(.potLimit), structureExplained(.noLimit),
            cashHowItWorks, cashResultLine(netBB: 12, hands: 84, per100: 14.3, rebuys: 1),
            cashResultLine(netBB: -3, hands: 20, per100: -15, rebuys: 2), cashResultNone,
            fixedBetTitle(100), fixedRaiseTitle(200), bettingCapped, cashStructureReadout,
            basicsTitle, basicsPanelSubtitle, basicsBlurb, basicsOpen, workedThrough,
            basicsFooter, basicsSubtitle(10), practiseThis,
            practiceTitle, practiceSubtitle, practicePanelSubtitle, practiceBlurb,
            practiceOpen, practiceStart, practiceFooter, practiceSomethingElse,
            madeByTitle, madeBySubtitle, madeByBlurb, visitAdLab, readAboutPokerTutor,
            privacyLink, versionLine("1.0"),
            appTagline, ratingSettling, ratingSettled, ratingUnknown, tournamentOffer, assessmentOffer, focusNone, focusWorst, focusEmpty, handsSavedSubtitle, handsSavedBlurb,
            assessmentResult(rating: 54, label: "Solid amateur", low: 46, high: 61, hands: 40),
            assessmentPlayedOn(Date(timeIntervalSince1970: 1_788_000_000)),
            assessmentJustFinishedTitle(hands: 40), ratingCostLine(costPer100: 23.4),
            ratingLabel(value: 0, label: "Beginner"), ratingHandsWatched(199),
            handsSavedTitle(saved: 271, played: 199, practice: 72),
            nothingToFlag, nothingToFlagShort, quietOnCloseSpots, countedTowardsUnnamed,
            myGameIntro, costingYouSubtitle, tooSoonSubtitle, everythingMeasuredSubtitle,
            numbersMeasured, bySeatSubtitle, whatItCostSubtitle, worthAnotherLookSubtitle,
            thinSeatRows, noFlaggedHands, statisticsEmpty, statisticsSettled, flaggedCostNote,
            scorecardEmpty, barMarkNote, costNotKnown,
            flaggedOnlyToggle, noHandsYet, noHandsYetBlurb, nothingFlagged, nothingFlaggedBlurb,
            pickAHand, pickAHandBlurb, replayOffer, noComment,
            cannotReplay, cannotReplayBlurb,
            everyHandIsChosen, everyHandIsChosenBlurb, cardsNotChosen, cardsNotChosenBlurb,
            stopWhenever, stopWheneverBlurb, noPretending,
            assessmentFinishedTitle, assessmentStoppedTitle, startingRating,
            alreadyClear, whereToLookFirst, whereToLookFirstNote, nothingToName,
            whatIGotToLookAt, whatIArranged,
            checkingBackTitle, checkingBackHint,
            sameSituation, cardsStillNotChosen, cardsStillNotChosenBlurb,
            whereYouLoseMost, whereYouLoseMostSubtitle, seatDrillTitle, seatDrillLocked,
            seatDrillHint, sameSeat, seatCounted, seatCountedBlurb, seatDrillsHeading,
            seatDrillsFooter,
            checkTakesAsLong, drillEndsWhenStopped, drillEndsWhenStoppedBlurb,
            drillHandsKeptOut, drillHandsKeptOutBlurb, whatHappened,
            willCheckBack, willCheckBackBlurb, checkHeldBlurb, backOnYourList, backOnYourListBlurb,
            whatToDoAboutIt, stoppingCostsNothing, checkIsDue, drillHalfDone,
            practiseOneThing, practiseOneThingBlurb,
            nineSeatsBlurb, topOfTheLadder, whatItTakesToMoveUp, costingAtThisTable, everyRung,
            sameEightPeople, nobody, seatsPayBeforeCards,
            coloursSubtitle, textSizeSubtitle, soundSubtitle, playTableSounds, pokerWordsBlurb,
            gettingStarted, gettingStartedSubtitle, startOver, startOverSubtitle, nothingSavedYet,
            whereHandsAreKept, whereHandsAreKeptSubtitle, whereHandsAreKeptBlurb,
            resetTitle, resetWarning,
            pokerWordsTitle, noWordMatches,
            cardColoursTitle, cardColoursSubtitle, tapToPutItBack, touchACard,
            paywallTitle, paywallNoPrice, paywallKeepsYourHands, paywallRestoreNote,
            paywallPendingNote, subscribedTitle, subscribedSubtitle, termsLink,
        ]
        lines += [
            assessmentLength(40), assessmentStoppedAt(15),
            assessmentResult(rating: 62, label: "Steady", low: 48, high: 74, hands: 40),
            numbersAcross(3), statisticsThin(60), coachSaidNothing(about: "call"),
            roundsLeaveOutArranged(40), roundsLeaveOutArranged(1),
            statisticsAllArranged(40), statisticsAllArranged(1),
            statisticsFootnote(hands: 0, arranged: 40, enough: false),
            statisticsFootnote(hands: 60, arranged: 40, enough: false),
            statisticsFootnote(hands: 400, enough: true),
            nothingFlaggedButCounted(1), nothingFlaggedButCounted(3),
            countedTowards("Playing too many hands"),
            assessmentIntro(40), noPretendingBlurb(40),
            ratingRangeLine(low: 48, high: 74, hands: 40),
            decisionsAcrossHands(decisions: 96, hands: 40),
            drillTitle("Folding the big blind too often"),
            sameSituationBlurb(seat: "seat 2 of 9 (the big blind)", depthInBB: 40,
                               lean: "with a raiser or two behind you"),
            checkTakesAsLongBlurb(12), drillHandsAndChances(hands: 30, chances: 14),
            inAboutDays(5), chancesAtTheSameSituation(12),
            drillHintLine("play back more of the hands worth playing back"),
            ladderIntro(5), handsAtThisTable(played: 80, needed: 200),
            nextTableIs("the kitchen table"), nextTableIsAgain("the kitchen table"),
            moveUpAt(60), whoChanges(leaving: "two calling stations", arriving: "two regulars"),
            pokerWordsSubtitle(72), pokerWordsIntro(72),
            handsAcrossSittings(hands: 900, sittings: 14),
            sittingLine(hands: 0, flagged: 0), sittingLine(hands: 24, flagged: 0),
            sittingLine(hands: 24, flagged: 3), sittingLine(hands: 60, flagged: 3),
            paywallBlurb(100), paywallBlurb(1), paywallPrice("$9.99"),
            handsLeftWarning(12), handsLeftWarning(1), hiddenHabits(1), hiddenHabits(4),
            freeRowNotProved, freeRowNamed, hiddenRowLabel, practiceLocked, practiceFreeNote, focusFreeEmpty,
        ]
        lines += tournamentTutorial.flatMap { [$0.title, $0.body] }
        lines += cashTutorial.flatMap { [$0.title, $0.body] }
        lines += [cashRungLine(rung: "50/100", chosen: "50/100"), cashRungLine(rung: "50/100", chosen: "100/200"),
                  cashRungLine(rung: "50/100", chosen: "25/50"), cashWinRateLine(rate: 8, hands: 213)]
        return lines
    }

    /// Where the ladder has the player, said on the cash setup panel beside the stakes they chose.
    public static func cashRungLine(rung: String, chosen: String) -> String {
        if rung == chosen {
            return "\(rung) is where the ladder has you. Play well there over enough hands and it "
                + "will suggest the next stakes up; any stakes are yours to sit at whenever you like."
        }
        return "The ladder has you at \(rung). You can sit down at \(chosen) whenever you like — each "
            + "stakes keeps its own record, and the ladder reads the one you are playing."
    }

    public static func cashWinRateLine(rate: Double, hands: Int) -> String {
        String(format: "%+.0f big blinds for every 100 hands, over \(hands) hands.", rate)
    }
}

/// What the running build calls itself, read from the bundle rather than written down twice.
///
/// A version number kept in the source is a version number that disagrees with the store within a
/// release or two, and the one place it must be right is the screen somebody reads before writing
/// to say something is broken.
public enum AppInfo {
    public static var version: String {
        let marketing = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
        return marketing ?? "1.0"
    }
}
