// // FetchCollectionUseCaseTests.swift // PlantGuideTests // // Unit tests for FetchCollectionUseCase - the use case for fetching plants // from the user's collection with filtering and statistics. // import XCTest @testable import PlantGuide // MARK: - FetchCollectionUseCaseTests final class FetchCollectionUseCaseTests: XCTestCase { // MARK: - Properties private var sut: FetchCollectionUseCase! private var mockPlantRepository: MockPlantCollectionRepository! private var mockCareScheduleRepository: MockCareScheduleRepository! // MARK: - Test Lifecycle override func setUp() { super.setUp() mockPlantRepository = MockPlantCollectionRepository() mockCareScheduleRepository = MockCareScheduleRepository() sut = FetchCollectionUseCase( plantRepository: mockPlantRepository, careScheduleRepository: mockCareScheduleRepository ) } override func tearDown() { sut = nil mockPlantRepository = nil mockCareScheduleRepository = nil super.tearDown() } // MARK: - execute() Basic Fetch Tests func testExecute_WhenCollectionIsEmpty_ReturnsEmptyArray() async throws { // When let result = try await sut.execute() // Then XCTAssertTrue(result.isEmpty) XCTAssertEqual(mockPlantRepository.fetchAllCallCount, 1) } func testExecute_WhenCollectionHasPlants_ReturnsAllPlants() async throws { // Given let plants = [ Plant.mockMonstera(), Plant.mockPothos(), Plant.mockSnakePlant() ] mockPlantRepository.addPlants(plants) // When let result = try await sut.execute() // Then XCTAssertEqual(result.count, 3) XCTAssertEqual(mockPlantRepository.fetchAllCallCount, 1) } func testExecute_WhenFetchingAll_ReturnsSortedByDateDescending() async throws { // Given let calendar = Calendar.current let now = Date() let plant1 = Plant.mock( id: UUID(), scientificName: "First", dateIdentified: calendar.date(byAdding: .day, value: -2, to: now)! ) let plant2 = Plant.mock( id: UUID(), scientificName: "Second", dateIdentified: calendar.date(byAdding: .day, value: -1, to: now)! ) let plant3 = Plant.mock( id: UUID(), scientificName: "Third", dateIdentified: now ) mockPlantRepository.addPlants([plant1, plant2, plant3]) // When let result = try await sut.execute() // Then - Should be sorted by dateIdentified descending XCTAssertEqual(result.count, 3) XCTAssertEqual(result[0].scientificName, "Third") XCTAssertEqual(result[1].scientificName, "Second") XCTAssertEqual(result[2].scientificName, "First") } // MARK: - execute(filter:) Filter Tests func testExecuteWithFilter_WhenFilteringByFavorites_ReturnsOnlyFavorites() async throws { // Given let favoriteMonster = Plant.mockMonstera(isFavorite: true) let regularPothos = Plant.mockPothos(isFavorite: false) let favoriteSnake = Plant.mockSnakePlant(isFavorite: true) mockPlantRepository.addPlants([favoriteMonster, regularPothos, favoriteSnake]) var filter = PlantFilter() filter.isFavorite = true // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 2) XCTAssertTrue(result.allSatisfy { $0.isFavorite }) XCTAssertEqual(mockPlantRepository.filterCallCount, 1) } func testExecuteWithFilter_WhenFilteringByFamily_ReturnsMatchingFamily() async throws { // Given let araceaePlant1 = Plant.mockMonstera() // Family: Araceae let araceaePlant2 = Plant.mockPothos() // Family: Araceae let asparagaceaePlant = Plant.mockSnakePlant() // Family: Asparagaceae mockPlantRepository.addPlants([araceaePlant1, araceaePlant2, asparagaceaePlant]) var filter = PlantFilter() filter.families = Set(["Araceae"]) // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 2) XCTAssertTrue(result.allSatisfy { $0.family == "Araceae" }) } func testExecuteWithFilter_WhenFilteringByIdentificationSource_ReturnsMatchingSource() async throws { // Given let onDevicePlant = Plant.mock(identificationSource: .onDeviceML) let apiPlant = Plant.mock(identificationSource: .plantNetAPI) let manualPlant = Plant.mock(identificationSource: .userManual) mockPlantRepository.addPlants([onDevicePlant, apiPlant, manualPlant]) var filter = PlantFilter() filter.identificationSource = .plantNetAPI // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 1) XCTAssertEqual(result.first?.identificationSource, .plantNetAPI) } func testExecuteWithFilter_WhenSearchingByQuery_ReturnsMatchingPlants() async throws { // Given let monstera = Plant.mockMonstera() let pothos = Plant.mockPothos() let peaceLily = Plant.mockPeaceLily() mockPlantRepository.addPlants([monstera, pothos, peaceLily]) var filter = PlantFilter() filter.searchQuery = "Monstera" // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 1) XCTAssertTrue(result.first?.scientificName.contains("Monstera") ?? false) } func testExecuteWithFilter_WhenSortingByName_ReturnsSortedByName() async throws { // Given let plants = [ Plant.mock(scientificName: "Zebrina"), Plant.mock(scientificName: "Aloe vera"), Plant.mock(scientificName: "Monstera") ] mockPlantRepository.addPlants(plants) var filter = PlantFilter() filter.sortBy = .name filter.sortAscending = true // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 3) XCTAssertEqual(result[0].scientificName, "Aloe vera") XCTAssertEqual(result[1].scientificName, "Monstera") XCTAssertEqual(result[2].scientificName, "Zebrina") } func testExecuteWithFilter_WhenSortingByFamily_ReturnsSortedByFamily() async throws { // Given let plants = [ Plant.mock(family: "Moraceae"), Plant.mock(family: "Araceae"), Plant.mock(family: "Asparagaceae") ] mockPlantRepository.addPlants(plants) var filter = PlantFilter() filter.sortBy = .family filter.sortAscending = true // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 3) XCTAssertEqual(result[0].family, "Araceae") XCTAssertEqual(result[1].family, "Asparagaceae") XCTAssertEqual(result[2].family, "Moraceae") } func testExecuteWithFilter_WhenCombiningFilters_AppliesAllCriteria() async throws { // Given let favAraceae = Plant.mock(family: "Araceae", isFavorite: true) let notFavAraceae = Plant.mock(family: "Araceae", isFavorite: false) let favMoraceae = Plant.mock(family: "Moraceae", isFavorite: true) mockPlantRepository.addPlants([favAraceae, notFavAraceae, favMoraceae]) var filter = PlantFilter() filter.families = Set(["Araceae"]) filter.isFavorite = true // When let result = try await sut.execute(filter: filter) // Then XCTAssertEqual(result.count, 1) XCTAssertEqual(result.first?.family, "Araceae") XCTAssertTrue(result.first?.isFavorite ?? false) } // MARK: - fetchStatistics() Tests func testFetchStatistics_WhenCollectionIsEmpty_ReturnsZeroStatistics() async throws { // When let stats = try await sut.fetchStatistics() // Then XCTAssertEqual(stats.totalPlants, 0) XCTAssertEqual(stats.favoriteCount, 0) XCTAssertEqual(mockPlantRepository.getStatisticsCallCount, 1) } func testFetchStatistics_WhenCollectionHasPlants_ReturnsCorrectStatistics() async throws { // Given let plants = [ Plant.mockMonstera(isFavorite: true), Plant.mockPothos(isFavorite: false), Plant.mockSnakePlant(isFavorite: true) ] mockPlantRepository.addPlants(plants) // When let stats = try await sut.fetchStatistics() // Then XCTAssertEqual(stats.totalPlants, 3) XCTAssertEqual(stats.favoriteCount, 2) } func testFetchStatistics_ReturnsCorrectFamilyDistribution() async throws { // Given let plants = [ Plant.mockMonstera(), // Araceae Plant.mockPothos(), // Araceae Plant.mockSnakePlant() // Asparagaceae ] mockPlantRepository.addPlants(plants) // When let stats = try await sut.fetchStatistics() // Then XCTAssertEqual(stats.familyDistribution["Araceae"], 2) XCTAssertEqual(stats.familyDistribution["Asparagaceae"], 1) } func testFetchStatistics_ReturnsCorrectIdentificationSourceBreakdown() async throws { // Given let plants = [ Plant.mock(identificationSource: .onDeviceML), Plant.mock(identificationSource: .onDeviceML), Plant.mock(identificationSource: .plantNetAPI) ] mockPlantRepository.addPlants(plants) // When let stats = try await sut.fetchStatistics() // Then XCTAssertEqual(stats.identificationSourceBreakdown[.onDeviceML], 2) XCTAssertEqual(stats.identificationSourceBreakdown[.plantNetAPI], 1) } // MARK: - Error Handling Tests func testExecute_WhenRepositoryFetchFails_ThrowsRepositoryFetchFailed() async { // Given mockPlantRepository.shouldThrowOnFetch = true mockPlantRepository.errorToThrow = NSError(domain: "CoreData", code: 500) // When/Then do { _ = try await sut.execute() XCTFail("Expected repositoryFetchFailed error to be thrown") } catch let error as FetchCollectionError { switch error { case .repositoryFetchFailed(let underlyingError): XCTAssertEqual((underlyingError as NSError).domain, "CoreData") default: XCTFail("Expected repositoryFetchFailed error, got \(error)") } } catch { XCTFail("Expected FetchCollectionError, got \(error)") } } func testExecuteWithFilter_WhenFilterFails_ThrowsRepositoryFetchFailed() async { // Given mockPlantRepository.shouldThrowOnFilter = true var filter = PlantFilter() filter.isFavorite = true // When/Then do { _ = try await sut.execute(filter: filter) XCTFail("Expected error to be thrown") } catch let error as FetchCollectionError { switch error { case .repositoryFetchFailed: break // Expected default: XCTFail("Expected repositoryFetchFailed error, got \(error)") } } catch { XCTFail("Expected FetchCollectionError, got \(error)") } } func testFetchStatistics_WhenCalculationFails_ThrowsStatisticsCalculationFailed() async { // Given mockPlantRepository.shouldThrowOnGetStatistics = true mockPlantRepository.errorToThrow = NSError(domain: "Stats", code: 1) // When/Then do { _ = try await sut.fetchStatistics() XCTFail("Expected statisticsCalculationFailed error to be thrown") } catch let error as FetchCollectionError { switch error { case .statisticsCalculationFailed: break // Expected default: XCTFail("Expected statisticsCalculationFailed error, got \(error)") } } catch { XCTFail("Expected FetchCollectionError, got \(error)") } } // MARK: - Error Description Tests func testFetchCollectionError_RepositoryFetchFailed_HasCorrectDescription() { // Given let underlyingError = NSError(domain: "Test", code: 123) let error = FetchCollectionError.repositoryFetchFailed(underlyingError) // Then XCTAssertNotNil(error.errorDescription) XCTAssertTrue(error.errorDescription?.contains("load plants") ?? false) } func testFetchCollectionError_StatisticsCalculationFailed_HasCorrectDescription() { // Given let underlyingError = NSError(domain: "Test", code: 456) let error = FetchCollectionError.statisticsCalculationFailed(underlyingError) // Then XCTAssertNotNil(error.errorDescription) XCTAssertTrue(error.errorDescription?.contains("statistics") ?? false) } func testFetchCollectionError_InvalidFilter_HasCorrectDescription() { // Given let error = FetchCollectionError.invalidFilter("Search query too long") // Then XCTAssertNotNil(error.errorDescription) XCTAssertTrue(error.errorDescription?.contains("filter") ?? false) } // MARK: - Protocol Conformance Tests func testFetchCollectionUseCase_ConformsToProtocol() { XCTAssertTrue(sut is FetchCollectionUseCaseProtocol) } // MARK: - Edge Cases func testExecute_WithLargeCollection_HandlesCorrectly() async throws { // Given - Add 100 plants let plants = (0..<100).map { _ in Plant.mock() } mockPlantRepository.addPlants(plants) // When let result = try await sut.execute() // Then XCTAssertEqual(result.count, 100) } func testExecuteWithFilter_WhenNoMatchesFound_ReturnsEmptyArray() async throws { // Given let plants = [ Plant.mockMonstera(isFavorite: false), Plant.mockPothos(isFavorite: false) ] mockPlantRepository.addPlants(plants) var filter = PlantFilter() filter.isFavorite = true // When let result = try await sut.execute(filter: filter) // Then XCTAssertTrue(result.isEmpty) } }