// // HybridIdentificationUseCaseTests.swift // PlantGuideTests // // Unit tests for HybridIdentificationUseCase - the use case for hybrid plant // identification combining on-device ML and online API. // import XCTest import UIKit @testable import PlantGuide // MARK: - HybridIdentificationUseCaseTests final class HybridIdentificationUseCaseTests: XCTestCase { // MARK: - Properties private var sut: HybridIdentificationUseCase! private var mockOnDeviceUseCase: MockIdentifyPlantUseCase! private var mockOnlineUseCase: MockIdentifyPlantOnlineUseCase! private var mockNetworkMonitor: MockNetworkMonitor! // MARK: - Test Lifecycle override func setUp() { super.setUp() mockOnDeviceUseCase = MockIdentifyPlantUseCase() mockOnlineUseCase = MockIdentifyPlantOnlineUseCase() mockNetworkMonitor = MockNetworkMonitor(isConnected: true) sut = HybridIdentificationUseCase( onDeviceUseCase: mockOnDeviceUseCase, onlineUseCase: mockOnlineUseCase, networkMonitor: mockNetworkMonitor ) } override func tearDown() { sut = nil mockOnDeviceUseCase = nil mockOnlineUseCase = nil mockNetworkMonitor = nil super.tearDown() } // MARK: - Test Helpers private func createTestImage() -> UIImage { let size = CGSize(width: 224, height: 224) let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { context in UIColor.green.setFill() context.fill(CGRect(origin: .zero, size: size)) } } private func createHighConfidencePredictions() -> [ViewPlantPrediction] { [ ViewPlantPrediction( id: UUID(), speciesName: "Monstera deliciosa", commonName: "Swiss Cheese Plant", confidence: 0.95 ) ] } private func createLowConfidencePredictions() -> [ViewPlantPrediction] { [ ViewPlantPrediction( id: UUID(), speciesName: "Unknown Plant", commonName: nil, confidence: 0.35 ) ] } private func createOnlinePredictions() -> [ViewPlantPrediction] { [ ViewPlantPrediction( id: UUID(), speciesName: "Monstera deliciosa", commonName: "Swiss Cheese Plant", confidence: 0.98 ) ] } // MARK: - onDeviceOnly Strategy Tests func testExecute_WithOnDeviceOnlyStrategy_ReturnsOnDeviceResults() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() // When let result = try await sut.execute(image: testImage, strategy: .onDeviceOnly) // Then XCTAssertEqual(result.source, .onDeviceML) XCTAssertTrue(result.onDeviceAvailable) XCTAssertFalse(result.predictions.isEmpty) XCTAssertEqual(result.predictions[0].speciesName, "Monstera deliciosa") } func testExecute_WithOnDeviceOnlyStrategy_IgnoresOnlineAvailability() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() // When let result = try await sut.execute(image: testImage, strategy: .onDeviceOnly) // Then XCTAssertEqual(result.source, .onDeviceML) // Online should not be called } func testExecute_WithOnDeviceOnlyStrategy_WhenOnDeviceFails_ThrowsError() async { // Given let testImage = createTestImage() mockOnDeviceUseCase.shouldThrow = true mockOnDeviceUseCase.errorToThrow = IdentifyPlantOnDeviceUseCaseError.noMatchesFound // When/Then do { _ = try await sut.execute(image: testImage, strategy: .onDeviceOnly) XCTFail("Expected error to be thrown") } catch let error as IdentifyPlantOnDeviceUseCaseError { XCTAssertEqual(error, .noMatchesFound) } catch { XCTFail("Expected IdentifyPlantOnDeviceUseCaseError, got \(error)") } } // MARK: - onlineOnly Strategy Tests func testExecute_WithOnlineOnlyStrategy_WhenConnected_ReturnsOnlineResults() async throws { // Given let testImage = createTestImage() mockNetworkMonitor.isConnected = true mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() // When let result = try await sut.execute(image: testImage, strategy: .onlineOnly) // Then XCTAssertEqual(result.source, .plantNetAPI) XCTAssertTrue(result.onlineAvailable) XCTAssertFalse(result.predictions.isEmpty) } func testExecute_WithOnlineOnlyStrategy_WhenDisconnected_ThrowsNoNetworkError() async { // Given let testImage = createTestImage() mockNetworkMonitor.isConnected = false // When/Then do { _ = try await sut.execute(image: testImage, strategy: .onlineOnly) XCTFail("Expected noNetworkForOnlineOnly error") } catch let error as HybridIdentificationError { XCTAssertEqual(error, .noNetworkForOnlineOnly) } catch { XCTFail("Expected HybridIdentificationError, got \(error)") } } func testExecute_WithOnlineOnlyStrategy_WhenOnlineFails_ThrowsError() async { // Given let testImage = createTestImage() mockNetworkMonitor.isConnected = true mockOnlineUseCase.shouldThrow = true mockOnlineUseCase.errorToThrow = IdentifyPlantOnlineUseCaseError.noMatchesFound // When/Then do { _ = try await sut.execute(image: testImage, strategy: .onlineOnly) XCTFail("Expected error to be thrown") } catch let error as IdentifyPlantOnlineUseCaseError { XCTAssertEqual(error, .noMatchesFound) } catch { XCTFail("Expected IdentifyPlantOnlineUseCaseError, got \(error)") } } // MARK: - onDeviceFirst Strategy Tests func testExecute_WithOnDeviceFirstStrategy_WhenHighConfidence_ReturnsOnDeviceResults() async throws { // Given let testImage = createTestImage() let threshold = 0.8 mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() // 0.95 confidence mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .onDeviceML) XCTAssertEqual(result.predictions[0].confidence, 0.95, accuracy: 0.001) } func testExecute_WithOnDeviceFirstStrategy_WhenLowConfidence_FallsBackToOnline() async throws { // Given let testImage = createTestImage() let threshold = 0.8 mockOnDeviceUseCase.predictionsToReturn = createLowConfidencePredictions() // 0.35 confidence mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .plantNetAPI) XCTAssertEqual(result.predictions[0].confidence, 0.98, accuracy: 0.001) } func testExecute_WithOnDeviceFirstStrategy_WhenLowConfidenceAndOffline_ReturnsOnDeviceResults() async throws { // Given let testImage = createTestImage() let threshold = 0.8 mockOnDeviceUseCase.predictionsToReturn = createLowConfidencePredictions() mockNetworkMonitor.isConnected = false // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .onDeviceML) XCTAssertFalse(result.onlineAvailable) } func testExecute_WithOnDeviceFirstStrategy_WhenOnlineFails_FallsBackToOnDevice() async throws { // Given let testImage = createTestImage() let threshold = 0.8 mockOnDeviceUseCase.predictionsToReturn = createLowConfidencePredictions() mockOnlineUseCase.shouldThrow = true mockNetworkMonitor.isConnected = true // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .onDeviceML) // Falls back to on-device } // MARK: - parallel Strategy Tests func testExecute_WithParallelStrategy_WhenBothSucceed_PrefersOnlineResults() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute(image: testImage, strategy: .parallel) // Then XCTAssertEqual(result.source, .plantNetAPI) XCTAssertTrue(result.onDeviceAvailable) XCTAssertTrue(result.onlineAvailable) } func testExecute_WithParallelStrategy_WhenOffline_OnlyRunsOnDevice() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() mockNetworkMonitor.isConnected = false // When let result = try await sut.execute(image: testImage, strategy: .parallel) // Then XCTAssertEqual(result.source, .onDeviceML) XCTAssertTrue(result.onDeviceAvailable) XCTAssertFalse(result.onlineAvailable) } func testExecute_WithParallelStrategy_WhenOnlineFails_ReturnsOnDevice() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() mockOnlineUseCase.shouldThrow = true mockNetworkMonitor.isConnected = true // When let result = try await sut.execute(image: testImage, strategy: .parallel) // Then XCTAssertEqual(result.source, .onDeviceML) XCTAssertTrue(result.onDeviceAvailable) } func testExecute_WithParallelStrategy_WhenOnDeviceFails_ReturnsOnline() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.shouldThrow = true mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute(image: testImage, strategy: .parallel) // Then XCTAssertEqual(result.source, .plantNetAPI) XCTAssertTrue(result.onlineAvailable) } func testExecute_WithParallelStrategy_WhenBothFail_ThrowsBothSourcesFailed() async { // Given let testImage = createTestImage() mockOnDeviceUseCase.shouldThrow = true mockOnlineUseCase.shouldThrow = true mockNetworkMonitor.isConnected = true // When/Then do { _ = try await sut.execute(image: testImage, strategy: .parallel) XCTFail("Expected bothSourcesFailed error") } catch let error as HybridIdentificationError { XCTAssertEqual(error, .bothSourcesFailed) } catch { XCTFail("Expected HybridIdentificationError, got \(error)") } } // MARK: - Error Description Tests func testHybridIdentificationError_NoNetworkForOnlineOnly_HasCorrectDescription() { // Given let error = HybridIdentificationError.noNetworkForOnlineOnly // Then XCTAssertNotNil(error.errorDescription) XCTAssertTrue(error.errorDescription?.contains("network") ?? false) } func testHybridIdentificationError_BothSourcesFailed_HasCorrectDescription() { // Given let error = HybridIdentificationError.bothSourcesFailed // Then XCTAssertNotNil(error.errorDescription) XCTAssertTrue(error.errorDescription?.contains("Unable to identify") ?? false) } // MARK: - HybridIdentificationResult Tests func testHybridIdentificationResult_ContainsCorrectMetadata() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = createHighConfidencePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute(image: testImage, strategy: .onDeviceOnly) // Then XCTAssertTrue(result.onDeviceAvailable) XCTAssertTrue(result.onlineAvailable) // Network is connected XCTAssertEqual(result.source, .onDeviceML) } // MARK: - Protocol Conformance Tests func testHybridIdentificationUseCase_ConformsToProtocol() { XCTAssertTrue(sut is HybridIdentificationUseCaseProtocol) } // MARK: - Edge Cases func testExecute_WithOnDeviceFirstStrategy_ExactlyAtThreshold_ReturnsOnDevice() async throws { // Given let testImage = createTestImage() let threshold = 0.95 mockOnDeviceUseCase.predictionsToReturn = [ ViewPlantPrediction( id: UUID(), speciesName: "Test", commonName: nil, confidence: 0.95 // Exactly at threshold ) ] mockNetworkMonitor.isConnected = true // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .onDeviceML) } func testExecute_WithOnDeviceFirstStrategy_JustBelowThreshold_UsesOnline() async throws { // Given let testImage = createTestImage() let threshold = 0.95 mockOnDeviceUseCase.predictionsToReturn = [ ViewPlantPrediction( id: UUID(), speciesName: "Test", commonName: nil, confidence: 0.94 // Just below threshold ) ] mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: threshold) ) // Then XCTAssertEqual(result.source, .plantNetAPI) } func testExecute_WithOnDeviceFirstStrategy_EmptyPredictions_UsesOnline() async throws { // Given let testImage = createTestImage() mockOnDeviceUseCase.predictionsToReturn = [] // Empty - no predictions mockOnlineUseCase.predictionsToReturn = createOnlinePredictions() mockNetworkMonitor.isConnected = true // When - This should use online since top confidence is 0.0 let result = try await sut.execute( image: testImage, strategy: .onDeviceFirst(apiThreshold: 0.5) ) // Then XCTAssertEqual(result.source, .plantNetAPI) } }