// // MockImageStorage.swift // PlantGuideTests // // Mock implementation of ImageStorageProtocol for unit testing. // Provides configurable behavior and call tracking for verification. // import Foundation import UIKit @testable import PlantGuide // MARK: - MockImageStorage /// Mock implementation of ImageStorageProtocol for testing final actor MockImageStorage: ImageStorageProtocol { // MARK: - Storage private var storedImages: [String: UIImage] = [:] private var plantImages: [UUID: [String]] = [:] // MARK: - Call Tracking private(set) var saveCallCount = 0 private(set) var loadCallCount = 0 private(set) var deleteCallCount = 0 private(set) var deleteAllCallCount = 0 // MARK: - Error Configuration var shouldThrowOnSave = false var shouldThrowOnLoad = false var shouldThrowOnDelete = false var shouldThrowOnDeleteAll = false var errorToThrow: Error = ImageStorageError.writeFailed( NSError(domain: "MockError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Mock storage error"]) ) // MARK: - Captured Values private(set) var lastSavedImage: UIImage? private(set) var lastSavedPlantID: UUID? private(set) var lastLoadedPath: String? private(set) var lastDeletedPath: String? private(set) var lastDeletedAllPlantID: UUID? // MARK: - Generated Path Control var pathToReturn: String? // MARK: - ImageStorageProtocol func save(_ image: UIImage, for plantID: UUID) async throws -> String { saveCallCount += 1 lastSavedImage = image lastSavedPlantID = plantID if shouldThrowOnSave { throw errorToThrow } // Generate or use provided path let path = pathToReturn ?? "\(plantID.uuidString)/\(UUID().uuidString).jpg" // Store the image storedImages[path] = image // Track images per plant var paths = plantImages[plantID] ?? [] paths.append(path) plantImages[plantID] = paths return path } func load(path: String) async -> UIImage? { loadCallCount += 1 lastLoadedPath = path return storedImages[path] } func delete(path: String) async throws { deleteCallCount += 1 lastDeletedPath = path if shouldThrowOnDelete { throw errorToThrow } guard storedImages[path] != nil else { throw ImageStorageError.fileNotFound } storedImages.removeValue(forKey: path) // Remove from plant tracking for (plantID, var paths) in plantImages { if let index = paths.firstIndex(of: path) { paths.remove(at: index) plantImages[plantID] = paths break } } } func deleteAll(for plantID: UUID) async throws { deleteAllCallCount += 1 lastDeletedAllPlantID = plantID if shouldThrowOnDeleteAll { throw errorToThrow } // Remove all images for this plant if let paths = plantImages[plantID] { for path in paths { storedImages.removeValue(forKey: path) } plantImages.removeValue(forKey: plantID) } } // MARK: - Helper Methods /// Resets all state for clean test setup func reset() { storedImages = [:] plantImages = [:] saveCallCount = 0 loadCallCount = 0 deleteCallCount = 0 deleteAllCallCount = 0 shouldThrowOnSave = false shouldThrowOnLoad = false shouldThrowOnDelete = false shouldThrowOnDeleteAll = false lastSavedImage = nil lastSavedPlantID = nil lastLoadedPath = nil lastDeletedPath = nil lastDeletedAllPlantID = nil pathToReturn = nil } /// Adds an image directly to storage (bypasses save method) func addImage(_ image: UIImage, at path: String, for plantID: UUID) { storedImages[path] = image var paths = plantImages[plantID] ?? [] paths.append(path) plantImages[plantID] = paths } /// Gets stored image count var imageCount: Int { storedImages.count } /// Gets image count for a specific plant func imageCount(for plantID: UUID) -> Int { plantImages[plantID]?.count ?? 0 } /// Gets all paths for a plant func paths(for plantID: UUID) -> [String] { plantImages[plantID] ?? [] } /// Checks if an image exists at a path func imageExists(at path: String) -> Bool { storedImages[path] != nil } } // MARK: - Test Image Creation Helper extension MockImageStorage { /// Creates a simple test image for use in tests static func createTestImage( color: UIColor = .red, size: CGSize = CGSize(width: 100, height: 100) ) -> UIImage { let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { context in color.setFill() context.fill(CGRect(origin: .zero, size: size)) } } }