import Testing @testable import bookstax import Foundation // MARK: - DonationPurchaseState Enum @Suite("DonationPurchaseState – Computed Properties") struct DonationPurchaseStateTests { @Test("activePurchasingID returns id when purchasing") func activePurchasingID() { let state = DonationPurchaseState.purchasing(productID: "donatebook") #expect(state.activePurchasingID == "donatebook") } @Test("activePurchasingID returns nil for non-purchasing states") @available(iOS 18.0, *) func activePurchasingIDNil() { #expect(DonationPurchaseState.idle.activePurchasingID == nil) #expect(DonationPurchaseState.thankYou(productID: "x").activePurchasingID == nil) #expect(DonationPurchaseState.pending(productID: "x").activePurchasingID == nil) #expect(DonationPurchaseState.failed(productID: "x", message: "err").activePurchasingID == nil) } @Test("thankYouID returns id when in thankYou state") func thankYouID() { let state = DonationPurchaseState.thankYou(productID: "doneatepage") #expect(state.thankYouID == "doneatepage") } @Test("thankYouID returns nil for other states") func thankYouIDNil() { #expect(DonationPurchaseState.idle.thankYouID == nil) #expect(DonationPurchaseState.purchasing(productID: "x").thankYouID == nil) } @Test("pendingID returns id when in pending state") func pendingID() { let state = DonationPurchaseState.pending(productID: "donateencyclopaedia") #expect(state.pendingID == "donateencyclopaedia") } @Test("pendingID returns nil for other states") func pendingIDNil() { #expect(DonationPurchaseState.idle.pendingID == nil) #expect(DonationPurchaseState.thankYou(productID: "x").pendingID == nil) } @Test("errorMessage returns message when failed for matching id") func errorMessageMatch() { let state = DonationPurchaseState.failed(productID: "donatebook", message: "Payment declined") #expect(state.errorMessage(for: "donatebook") == "Payment declined") } @Test("errorMessage returns nil for wrong product id") func errorMessageNoMatch() { let state = DonationPurchaseState.failed(productID: "donatebook", message: "error") #expect(state.errorMessage(for: "doneatepage") == nil) } @Test("errorMessage returns nil for non-failed states") func errorMessageNotFailed() { #expect(DonationPurchaseState.idle.errorMessage(for: "x") == nil) #expect(DonationPurchaseState.purchasing(productID: "x").errorMessage(for: "x") == nil) } @Test("isIdle returns true only for .idle") func isIdleCheck() { #expect(DonationPurchaseState.idle.isIdle == true) #expect(DonationPurchaseState.purchasing(productID: "x").isIdle == false) #expect(DonationPurchaseState.thankYou(productID: "x").isIdle == false) #expect(DonationPurchaseState.failed(productID: "x", message: "e").isIdle == false) } @Test("Equatable: same purchasing states are equal") func equatablePurchasing() { #expect(DonationPurchaseState.purchasing(productID: "a") == DonationPurchaseState.purchasing(productID: "a")) #expect(DonationPurchaseState.purchasing(productID: "a") != DonationPurchaseState.purchasing(productID: "b")) } @Test("Equatable: idle equals idle") func equatableIdle() { #expect(DonationPurchaseState.idle == DonationPurchaseState.idle) #expect(DonationPurchaseState.idle != DonationPurchaseState.purchasing(productID: "x")) } } // MARK: - shouldShowNudge Timing @Suite("DonationService – shouldShowNudge", .serialized) @MainActor struct DonationServiceNudgeTests { private let installKey = "bookstax.installDate" private let nudgeKey = "bookstax.lastNudgeDate" private let historyKey = "bookstax.donationHistory" init() { // Clean UserDefaults before each test so DonationService reads fresh state UserDefaults.standard.removeObject(forKey: installKey) UserDefaults.standard.removeObject(forKey: nudgeKey) UserDefaults.standard.removeObject(forKey: historyKey) } @Test("Within 3-day grace period: nudge should not show") func gracePeriodPreventsNudge() { let recentInstall = Date().addingTimeInterval(-(2 * 24 * 3600)) // 2 days ago UserDefaults.standard.set(recentInstall, forKey: installKey) #expect(DonationService.shared.shouldShowNudge == false) } @Test("After 3-day grace period: nudge should show") func afterGracePeriodNudgeShows() { let oldInstall = Date().addingTimeInterval(-(4 * 24 * 3600)) // 4 days ago UserDefaults.standard.set(oldInstall, forKey: installKey) #expect(DonationService.shared.shouldShowNudge == true) } @Test("No install date recorded: nudge should not show (safe fallback)") func noInstallDateFallback() { // No install date stored — graceful fallback #expect(DonationService.shared.shouldShowNudge == false) } @Test("Nudge recently dismissed: should not show again") func recentNudgeHidden() { let oldInstall = Date().addingTimeInterval(-(90 * 24 * 3600)) let recentNudge = Date().addingTimeInterval(-(10 * 24 * 3600)) // dismissed 10 days ago UserDefaults.standard.set(oldInstall, forKey: installKey) UserDefaults.standard.set(recentNudge, forKey: nudgeKey) #expect(DonationService.shared.shouldShowNudge == false) } @Test("Nudge dismissed ~6 months ago: should show again") func sixMonthsLaterShowAgain() { let oldInstall = Date().addingTimeInterval(-(200 * 24 * 3600)) let oldNudge = Date().addingTimeInterval(-(183 * 24 * 3600)) // ~6 months ago UserDefaults.standard.set(oldInstall, forKey: installKey) UserDefaults.standard.set(oldNudge, forKey: nudgeKey) #expect(DonationService.shared.shouldShowNudge == true) } @Test("Nudge not yet 6 months: should not show") func beforeSixMonthsHidden() { let oldInstall = Date().addingTimeInterval(-(200 * 24 * 3600)) let recentNudge = Date().addingTimeInterval(-(100 * 24 * 3600)) // only 100 days UserDefaults.standard.set(oldInstall, forKey: installKey) UserDefaults.standard.set(recentNudge, forKey: nudgeKey) #expect(DonationService.shared.shouldShowNudge == false) } }