import XCTest @testable import PortfolioJournal /// The watch payload travels between processes and devices, so its encoding and /// the formatting the complications rely on are worth pinning down. final class WatchPortfolioSnapshotTests: XCTestCase { private func makeSnapshot( goals: [WatchPortfolioSnapshot.Goal] = [], totalValue: Double = 1_000 ) -> WatchPortfolioSnapshot { WatchPortfolioSnapshot( generatedAt: Date(timeIntervalSince1970: 1_770_000_000), currencyCode: "EUR", totalValue: totalValue, monthChange: 250, monthChangePercent: 0.025, sources: [], goals: goals, currentStreak: 3, bestStreak: 9, checkInDone: true, checkInMonthLabel: "August 2026" ) } private func makeGoal(name: String, current: Double, target: Double) -> WatchPortfolioSnapshot.Goal { WatchPortfolioSnapshot.Goal( id: UUID(), name: name, categoryName: "Cash", categoryIcon: "banknote.fill", categoryColorHex: "#6B7280", currentValue: current, targetValue: target ) } // MARK: - Transport func testApplicationContextRoundTrip() throws { let snapshot = makeSnapshot(goals: [makeGoal(name: "Cash", current: 40_000, target: 100_000)]) let context = try XCTUnwrap(snapshot.applicationContext()) let decoded = try XCTUnwrap(WatchPortfolioSnapshot.from(applicationContext: context)) XCTAssertEqual(decoded, snapshot) } func testApplicationContextIgnoresUnrelatedPayload() { XCTAssertNil(WatchPortfolioSnapshot.from(applicationContext: ["somethingElse": 1])) } func testEmptySnapshotReportsNoData() { XCTAssertFalse(WatchPortfolioSnapshot.empty.hasData) XCTAssertTrue(makeSnapshot().hasData) } // MARK: - Goals func testProgressIsClampedBetweenZeroAndOne() { XCTAssertEqual(makeGoal(name: "Over", current: 150, target: 100).progress, 1) XCTAssertEqual(makeGoal(name: "Negative", current: -50, target: 100).progress, 0) XCTAssertEqual(makeGoal(name: "Half", current: 50, target: 100).progress, 0.5) } func testProgressWithoutTargetIsZero() { XCTAssertEqual(makeGoal(name: "No target", current: 50, target: 0).progress, 0) } /// The single-goal complication must skip goals already reached. func testFeaturedGoalSkipsAchievedGoals() { let achieved = makeGoal(name: "Done", current: 100, target: 100) let open = makeGoal(name: "Cash buffer", current: 40, target: 100) let snapshot = makeSnapshot(goals: [achieved, open]) XCTAssertEqual(snapshot.featuredGoal?.name, "Cash buffer") } func testFeaturedGoalFallsBackToFirstWhenAllAchieved() { let first = makeGoal(name: "First", current: 100, target: 100) let second = makeGoal(name: "Second", current: 200, target: 200) XCTAssertEqual(makeSnapshot(goals: [first, second]).featuredGoal?.name, "First") } func testFeaturedGoalIsNilWithoutGoals() { XCTAssertNil(makeSnapshot().featuredGoal) } // MARK: - Formatting func testCompactFormatShortensLargeAmounts() { func compact(_ value: Double) -> String { WatchPortfolioSnapshot.format(value, currencyCode: "EUR", compact: true) } XCTAssertTrue(compact(750).contains("750")) XCTAssertTrue(compact(1_500).contains("1.5K")) XCTAssertTrue(compact(25_000).contains("25K")) XCTAssertTrue(compact(2_000_000).contains("2.0M")) XCTAssertTrue(compact(-1_500).hasPrefix("-")) } func testMonthChangeCarriesExplicitSign() { let positive = makeSnapshot() XCTAssertTrue(positive.formattedMonthChange().hasPrefix("+")) XCTAssertEqual(positive.formattedMonthChangePercent, "+2.5%") let negative = WatchPortfolioSnapshot( generatedAt: Date(), currencyCode: "EUR", totalValue: 1_000, monthChange: -250, monthChangePercent: -0.025, sources: [], goals: [], currentStreak: 0, bestStreak: 0, checkInDone: false, checkInMonthLabel: "" ) XCTAssertTrue(negative.formattedMonthChange().hasPrefix("-")) XCTAssertEqual(negative.formattedMonthChangePercent, "-2.5%") } }