// // FilterPreferencesStorageTests.swift // PlantGuideTests // // Unit tests for FilterPreferencesStorage - the UserDefaults-based persistence // for filter and view mode preferences in the plant collection. // import XCTest @testable import PlantGuide final class FilterPreferencesStorageTests: XCTestCase { // MARK: - Properties private var sut: FilterPreferencesStorage! private var testUserDefaults: UserDefaults! // MARK: - Test Lifecycle override func setUp() { super.setUp() // Create a unique suite name for test isolation let suiteName = "com.plantguide.tests.\(UUID().uuidString)" testUserDefaults = UserDefaults(suiteName: suiteName)! sut = FilterPreferencesStorage(userDefaults: testUserDefaults) } override func tearDown() { // Clean up test UserDefaults if let suiteName = testUserDefaults.volatileDomainNames.first { testUserDefaults.removePersistentDomain(forName: suiteName) } testUserDefaults = nil sut = nil super.tearDown() } // MARK: - saveFilter and loadFilter Round-Trip Tests func testSaveFilterAndLoadFilter_WithDefaultFilter_RoundTripsSuccessfully() { // Given let filter = PlantFilter.default // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, filter.sortBy) XCTAssertEqual(loadedFilter.sortAscending, filter.sortAscending) XCTAssertNil(loadedFilter.families) XCTAssertNil(loadedFilter.lightRequirements) XCTAssertNil(loadedFilter.isFavorite) XCTAssertNil(loadedFilter.identificationSource) } func testSaveFilterAndLoadFilter_WithSortByName_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.sortBy = .name filter.sortAscending = true // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, .name) XCTAssertEqual(loadedFilter.sortAscending, true) } func testSaveFilterAndLoadFilter_WithSortByFamily_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.sortBy = .family filter.sortAscending = false // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, .family) XCTAssertEqual(loadedFilter.sortAscending, false) } func testSaveFilterAndLoadFilter_WithSortByDateIdentified_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.sortBy = .dateIdentified filter.sortAscending = true // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, .dateIdentified) XCTAssertEqual(loadedFilter.sortAscending, true) } func testSaveFilterAndLoadFilter_WithFamilies_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.families = Set(["Araceae", "Moraceae", "Asparagaceae"]) // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.families, Set(["Araceae", "Moraceae", "Asparagaceae"])) } func testSaveFilterAndLoadFilter_WithLightRequirements_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.lightRequirements = Set([.fullSun, .partialShade]) // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.lightRequirements, Set([.fullSun, .partialShade])) } func testSaveFilterAndLoadFilter_WithAllLightRequirements_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.lightRequirements = Set([.fullSun, .partialShade, .fullShade, .lowLight]) // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.lightRequirements, Set([.fullSun, .partialShade, .fullShade, .lowLight])) } func testSaveFilterAndLoadFilter_WithIsFavoriteTrue_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.isFavorite = true // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.isFavorite, true) } func testSaveFilterAndLoadFilter_WithIsFavoriteFalse_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.isFavorite = false // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.isFavorite, false) } func testSaveFilterAndLoadFilter_WithIdentificationSourceOnDeviceML_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.identificationSource = .onDeviceML // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.identificationSource, .onDeviceML) } func testSaveFilterAndLoadFilter_WithIdentificationSourcePlantNetAPI_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.identificationSource = .plantNetAPI // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.identificationSource, .plantNetAPI) } func testSaveFilterAndLoadFilter_WithIdentificationSourceUserManual_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.identificationSource = .userManual // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.identificationSource, .userManual) } func testSaveFilterAndLoadFilter_WithAllPropertiesSet_RoundTripsSuccessfully() { // Given var filter = PlantFilter() filter.sortBy = .name filter.sortAscending = true filter.families = Set(["Araceae"]) filter.lightRequirements = Set([.fullSun, .lowLight]) filter.isFavorite = true filter.identificationSource = .plantNetAPI // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, .name) XCTAssertEqual(loadedFilter.sortAscending, true) XCTAssertEqual(loadedFilter.families, Set(["Araceae"])) XCTAssertEqual(loadedFilter.lightRequirements, Set([.fullSun, .lowLight])) XCTAssertEqual(loadedFilter.isFavorite, true) XCTAssertEqual(loadedFilter.identificationSource, .plantNetAPI) } // MARK: - saveViewMode and loadViewMode Round-Trip Tests func testSaveViewModeAndLoadViewMode_WithGrid_RoundTripsSuccessfully() { // Given let viewMode = ViewMode.grid // When sut.saveViewMode(viewMode) let loadedViewMode = sut.loadViewMode() // Then XCTAssertEqual(loadedViewMode, .grid) } func testSaveViewModeAndLoadViewMode_WithList_RoundTripsSuccessfully() { // Given let viewMode = ViewMode.list // When sut.saveViewMode(viewMode) let loadedViewMode = sut.loadViewMode() // Then XCTAssertEqual(loadedViewMode, .list) } func testSaveViewMode_OverwritesPreviousValue() { // Given sut.saveViewMode(.grid) XCTAssertEqual(sut.loadViewMode(), .grid) // When sut.saveViewMode(.list) // Then XCTAssertEqual(sut.loadViewMode(), .list) } // MARK: - clearFilter Tests func testClearFilter_RemovesAllFilterPreferences() { // Given var filter = PlantFilter() filter.sortBy = .name filter.sortAscending = true filter.families = Set(["Araceae"]) filter.lightRequirements = Set([.fullSun]) filter.isFavorite = true filter.identificationSource = .onDeviceML sut.saveFilter(filter) sut.saveViewMode(.list) // Verify filter was saved let savedFilter = sut.loadFilter() XCTAssertEqual(savedFilter.sortBy, .name) XCTAssertEqual(savedFilter.families, Set(["Araceae"])) // When sut.clearFilter() // Then let clearedFilter = sut.loadFilter() XCTAssertEqual(clearedFilter.sortBy, .dateAdded) // Default XCTAssertEqual(clearedFilter.sortAscending, false) // Default XCTAssertNil(clearedFilter.families) XCTAssertNil(clearedFilter.lightRequirements) XCTAssertNil(clearedFilter.isFavorite) XCTAssertNil(clearedFilter.identificationSource) // View mode should NOT be cleared by clearFilter XCTAssertEqual(sut.loadViewMode(), .list) } func testClearFilter_DoesNotAffectViewMode() { // Given sut.saveViewMode(.list) sut.saveFilter(PlantFilter(sortBy: .name)) // When sut.clearFilter() // Then XCTAssertEqual(sut.loadViewMode(), .list) } // MARK: - Loading Defaults When No Preferences Exist Tests func testLoadFilter_WhenNoPreferencesExist_ReturnsDefaultFilter() { // When let filter = sut.loadFilter() // Then XCTAssertEqual(filter.sortBy, .dateAdded) XCTAssertEqual(filter.sortAscending, false) XCTAssertNil(filter.searchQuery) XCTAssertNil(filter.families) XCTAssertNil(filter.lightRequirements) XCTAssertNil(filter.isFavorite) XCTAssertNil(filter.identificationSource) } func testLoadViewMode_WhenNoPreferencesExist_ReturnsDefaultGrid() { // When let viewMode = sut.loadViewMode() // Then XCTAssertEqual(viewMode, .grid) } // MARK: - Edge Case Tests func testSaveFilter_WithEmptyFamiliesSet_SavesAsNil() { // Given var filter = PlantFilter() filter.families = Set() // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then - Empty set should be treated as nil // Note: The implementation saves empty sets, so this tests that behavior XCTAssertNil(loadedFilter.families) } func testSaveFilter_WithEmptyLightRequirementsSet_SavesAsNil() { // Given var filter = PlantFilter() filter.lightRequirements = Set() // When sut.saveFilter(filter) let loadedFilter = sut.loadFilter() // Then - Empty set should be treated as nil XCTAssertNil(loadedFilter.lightRequirements) } func testSaveFilter_OverwritesPreviousValues() { // Given var firstFilter = PlantFilter() firstFilter.sortBy = .name firstFilter.families = Set(["Araceae"]) sut.saveFilter(firstFilter) var secondFilter = PlantFilter() secondFilter.sortBy = .family secondFilter.families = nil // When sut.saveFilter(secondFilter) let loadedFilter = sut.loadFilter() // Then XCTAssertEqual(loadedFilter.sortBy, .family) XCTAssertNil(loadedFilter.families) } func testLoadFilter_WithCorruptedSortByValue_ReturnsDefault() { // Given - Manually set an invalid sortBy value testUserDefaults.set("invalidSortOption", forKey: "PlantGuide.Filter.SortBy") // When let loadedFilter = sut.loadFilter() // Then - Should use default value XCTAssertEqual(loadedFilter.sortBy, .dateAdded) } func testLoadViewMode_WithCorruptedValue_ReturnsDefaultGrid() { // Given - Manually set an invalid view mode value testUserDefaults.set("invalidViewMode", forKey: "PlantGuide.ViewMode") // When let loadedViewMode = sut.loadViewMode() // Then - Should use default value XCTAssertEqual(loadedViewMode, .grid) } func testLoadFilter_WithCorruptedLightRequirementValue_IgnoresInvalidValues() { // Given - Manually set light requirements with some invalid values testUserDefaults.set(["fullSun", "invalidValue", "lowLight"], forKey: "PlantGuide.Filter.LightRequirements") // When let loadedFilter = sut.loadFilter() // Then - Should only include valid values XCTAssertEqual(loadedFilter.lightRequirements, Set([.fullSun, .lowLight])) } func testLoadFilter_WithCorruptedIdentificationSourceValue_ReturnsNil() { // Given - Manually set an invalid identification source value testUserDefaults.set("invalidSource", forKey: "PlantGuide.Filter.IdentificationSource") // When let loadedFilter = sut.loadFilter() // Then - Should be nil for invalid value XCTAssertNil(loadedFilter.identificationSource) } // MARK: - Thread Safety Tests func testSaveAndLoad_FromMultipleThreads_WorksCorrectly() async { // Given let iterations = 100 // When - Perform concurrent saves and loads await withTaskGroup(of: Void.self) { group in for i in 0..