import Foundation import Testing @testable import PediFoods @Test("makeUserKey prefers a non-empty profileId over email") func makeUserKeyPrefersProfileId() { let key = SessionStateStore.makeUserKey(profileId: "abc123", email: "user@example.com") #expect(key == "id:abc123") } @Test("makeUserKey falls back to email when profileId is nil or blank") func makeUserKeyFallsBackToEmail() { #expect(SessionStateStore.makeUserKey(profileId: nil, email: "User@Example.com") == "email:user@example.com") #expect(SessionStateStore.makeUserKey(profileId: " ", email: "user@example.com") == "email:user@example.com") } @Test("makeUserKey returns nil when both profileId and email are absent") func makeUserKeyNilWhenBothAbsent() { #expect(SessionStateStore.makeUserKey(profileId: nil, email: nil) == nil) #expect(SessionStateStore.makeUserKey(profileId: " ", email: " ") == nil) } /// `SessionStateStore.defaults` is one shared static var - Swift Testing /// runs `@Test`s concurrently by default, so any test that swaps it would /// race every other test in this file. `.serialized` forces this suite's /// tests to run one at a time instead. @Suite(.serialized) struct SessionStateStorePersistenceTests { /// Points `SessionStateStore` at a throwaway, uniquely-named /// `UserDefaults` suite for the duration of one test, and restores /// `.standard` after. Internal (not private) so the cross-file /// extension in CartStateTests.swift can reuse it — both need to be /// in this same serialization domain, see that file's comment. func withIsolatedDefaults(_ body: () throws -> Void) rethrows { let suiteName = "SessionStateStoreTests.\(UUID().uuidString)" let suite = UserDefaults(suiteName: suiteName)! let previous = SessionStateStore.defaults SessionStateStore.defaults = suite defer { SessionStateStore.defaults = previous suite.removePersistentDomain(forName: suiteName) } try body() } @Test("shouldPromptPushOptIn is true before the first prompt is ever recorded") func shouldPromptPushOptInTrueInitially() { withIsolatedDefaults { #expect(SessionStateStore.shouldPromptPushOptIn()) } } @Test("shouldPromptPushOptIn is false immediately after recording a prompt") func shouldPromptPushOptInFalseRightAfterRecording() { withIsolatedDefaults { SessionStateStore.recordPushOptInPrompted() #expect(SessionStateStore.shouldPromptPushOptIn() == false) } } @Test("saveAddress/loadAddress round-trips every field") func addressRoundTrip() { withIsolatedDefaults { let state = AddressState(selectedId: "addr-1", display: "Rua A, 100", latitude: -23.5, longitude: -46.6) SessionStateStore.saveAddress(state) let loaded = SessionStateStore.loadAddress() #expect(loaded?.selectedId == "addr-1") #expect(loaded?.display == "Rua A, 100") #expect(loaded?.latitude == -23.5) } } @Test("loadAddress substitutes a placeholder display string for an empty one") func addressLoadReplacesEmptyDisplay() { withIsolatedDefaults { SessionStateStore.saveAddress(AddressState(selectedId: nil, display: "")) #expect(SessionStateStore.loadAddress()?.display == "Defina seu endereco") } } @Test("clearAddress removes a previously saved address") func addressClear() { withIsolatedDefaults { SessionStateStore.saveAddress(AddressState(selectedId: "addr-1", display: "Rua A")) SessionStateStore.clearAddress() #expect(SessionStateStore.loadAddress() == nil) } } @Test("saveCart/loadCart round-trips items, addons, and total") func cartRoundTrip() { withIsolatedDefaults { let item = CartItemState( id: "item-1", productId: "p1", storeId: "s1", name: "Pizza", addons: [CartItemAddonState(id: "a1", name: "Extra cheese", quantity: 1, unitPrice: 3.0)], quantity: 2, unitPrice: 29.9 ) let cart = CartState(storeId: "s1", storeName: "Loja A", items: [item], total: 65.8) SessionStateStore.saveCart(cart) let loaded = SessionStateStore.loadCart() #expect(loaded?.storeId == "s1") #expect(loaded?.items.first?.name == "Pizza") #expect(loaded?.items.first?.addons.first?.name == "Extra cheese") #expect(loaded?.total == 65.8) } } @Test("clearCart removes a previously saved cart") func cartClear() { withIsolatedDefaults { SessionStateStore.saveCart(CartState(storeId: "s1", storeName: "Loja A", items: [], total: 0)) SessionStateStore.clearCart() #expect(SessionStateStore.loadCart() == nil) } } @Test("saveTrackedOrder inserts new orders at the front and updates existing ones in place") func trackedOrdersInsertAndUpdate() { withIsolatedDefaults { SessionStateStore.saveTrackedOrder(PublicOrderResult(id: "o1", status: "PENDING")) SessionStateStore.saveTrackedOrder(PublicOrderResult(id: "o2", status: "PENDING")) var orders = SessionStateStore.loadTrackedOrders() #expect(orders.map(\.id) == ["o2", "o1"]) SessionStateStore.saveTrackedOrder(PublicOrderResult(id: "o1", status: "COMPLETED")) orders = SessionStateStore.loadTrackedOrders() #expect(orders.map(\.id) == ["o2", "o1"]) #expect(orders.first(where: { $0.id == "o1" })?.status == "COMPLETED") } } @Test("loadTrackedOrder finds a specific order by id") func trackedOrderLookupById() { withIsolatedDefaults { SessionStateStore.saveTrackedOrder(PublicOrderResult(id: "o1", status: "PENDING")) #expect(SessionStateStore.loadTrackedOrder(orderId: "o1")?.status == "PENDING") #expect(SessionStateStore.loadTrackedOrder(orderId: "missing") == nil) } } @Test("savePendingCartOrderId clears the value when given an empty string") func pendingCartOrderIdClearsOnEmptyString() { withIsolatedDefaults { SessionStateStore.savePendingCartOrderId("order-1") #expect(SessionStateStore.loadPendingCartOrderId() == "order-1") SessionStateStore.savePendingCartOrderId(" ") #expect(SessionStateStore.loadPendingCartOrderId() == nil) } } @Test("saveOrderReview de-duplicates by orderId case- and whitespace-insensitively") func orderReviewDedupesByOrderId() { withIsolatedDefaults { let review1 = OrderReviewRecord( orderId: "Order-1", storeId: nil, shortId: nil, storeName: nil, storeLogoURL: nil, createdAt: nil, submittedAt: "2026-01-01", rating: 4, comment: "Good", orderPositiveTags: nil, orderImprovementTags: nil, deliverySentiment: nil, deliveryPositiveTags: nil, deliveryNegativeTags: nil, appNps: nil, platform: nil, editableUntil: nil, storeReplyUntil: nil, reviewWindowExpiresAt: nil, storeReplyMessage: nil, storeReplyAt: nil ) SessionStateStore.saveOrderReview(review1) #expect(SessionStateStore.hasOrderReview(orderId: "order-1")) #expect(SessionStateStore.loadOrderReviews().count == 1) let review2 = OrderReviewRecord( orderId: "order-1 ", storeId: nil, shortId: nil, storeName: nil, storeLogoURL: nil, createdAt: nil, submittedAt: "2026-01-02", rating: 5, comment: "Even better", orderPositiveTags: nil, orderImprovementTags: nil, deliverySentiment: nil, deliveryPositiveTags: nil, deliveryNegativeTags: nil, appNps: nil, platform: nil, editableUntil: nil, storeReplyUntil: nil, reviewWindowExpiresAt: nil, storeReplyMessage: nil, storeReplyAt: nil ) SessionStateStore.saveOrderReview(review2) #expect(SessionStateStore.loadOrderReviews().count == 1) #expect(SessionStateStore.loadOrderReviews().first?.rating == 5) } } @Test("saveOrderReviewDraft/loadOrderReviewDraft round-trips and clearOrderReviewDraft removes it") func orderReviewDraftRoundTripAndClear() { withIsolatedDefaults { let draft = OrderReviewDraftState( orderId: "o1", orderRate: 5, orderComment: "Great", orderPositiveTags: ["fast"], orderImprovementTags: [], deliverySentiment: "good", deliveryPositiveTags: [], deliveryNegativeTags: [], appNps: 9, platform: "ios" ) SessionStateStore.saveOrderReviewDraft(draft) #expect(SessionStateStore.loadOrderReviewDraft(orderId: "o1")?.orderComment == "Great") SessionStateStore.clearOrderReviewDraft(orderId: "o1") #expect(SessionStateStore.loadOrderReviewDraft(orderId: "o1") == nil) } } }