import Foundation import SwiftData import Testing @testable import GasTankAR @MainActor struct ARProjectsTests { private func detail(_ id: String = "L-240318") throws -> LeadDetail { let lead = try #require(LeadSampleData.records.first { $0.id == id }).toDomain() return LeadDetailSampleData.make(for: lead) } private func container() throws -> ModelContainer { try ModelContainer(for: Project.self, LeadARProjectLink.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true)) } @Test func suppliedProjectsAreIdempotentAndExcludeServiceOffers() throws { let container = try container() let detail = try detail() let service = LeadARProjectsService(detail: detail, container: container) let first = try service.loadProjects() let second = try LeadARProjectsService(detail: detail, container: container).loadProjects() #expect(first.count == 2) #expect(Set(first.map(\.id)) == Set(second.map(\.id))) #expect(first.allSatisfy { $0.source == .supplied }) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 2) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 2) } @Test func customProjectSurvivesReloadAndIsScopedToItsLead() throws { let container = try container() let detail = try detail() let service = LeadARProjectsService(detail: detail, container: container) let project = try service.createProject(name: " Hinter dem Haus ", tank: Tank()) let reopened = LeadARProjectsService(detail: detail, container: container) #expect(try reopened.loadProjects().contains { $0.id == project.id && $0.name == "Hinter dem Haus" }) #expect(try reopened.resolveProject(id: project.id).tank == Tank()) let otherDetail = try self.detail("L-240330") let other = LeadARProjectsService(detail: otherDetail, container: container) #expect(try !other.loadProjects().contains { $0.id == project.id }) #expect(throws: LeadARProjectsService.ProjectError.self) { try other.resolveProject(id: project.id) } } @Test func legacySessionIsLinkedWithoutLosingARData() throws { let container = try container() let detail = try detail() let context = ModelContext(container) let legacy = Project(leadID: detail.lead.id, customerName: detail.lead.displayName, creationDate: .now, tank: Tank(), worldMapData: Data([1, 2, 3]), tankTransform: [1, 2, 3], measurementsData: Data([4, 5])) context.insert(legacy) try context.save() let service = LeadARProjectsService(detail: detail, container: container) let projects = try service.loadProjects() #expect(projects.contains { $0.id == legacy.projectID && $0.source == .supplied }) let restored = try service.resolveProject(id: legacy.projectID) #expect(restored.worldMapData == Data([1, 2, 3])) #expect(restored.measurementsData == Data([4, 5])) #expect(restored.tankTransform == [1, 2, 3]) #expect(try context.fetchCount(FetchDescriptor()) == 2) } @Test func invalidCreationDoesNotLeaveProjectsOrLinks() throws { let container = try container() let service = LeadARProjectsService(detail: try detail(), container: container) #expect(throws: LeadARProjectsService.ProjectError.self) { try service.createProject(name: " ", tank: Tank()) } #expect(throws: LeadARProjectsService.ProjectError.self) { try service.createProject(name: "Invalid", tank: Tank(color: .white, placement: .belowGround)) } #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 0) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 0) } @Test func deletingCustomProjectRemovesAssociationAndSurvivesReload() throws { let container = try container() let detail = try detail() let service = LeadARProjectsService(detail: detail, container: container) let project = try service.createProject(name: "Löschen", tank: Tank()) try service.deleteProject(id: project.id) let reloaded = LeadARProjectsService(detail: detail, container: container) #expect(try !reloaded.loadProjects().contains { $0.id == project.id }) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 2) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 2) } @Test func suppliedProjectsCannotBeDeleted() throws { let container = try container() let service = LeadARProjectsService(detail: try detail(), container: container) let project = try #require(service.loadProjects().first) #expect(throws: LeadARProjectsService.ProjectError.self) { try service.deleteProject(id: project.id) } #expect(try service.loadProjects().contains { $0.id == project.id }) } @Test func editingUpdatesExistingProjectAndPreservesSession() throws { let container = try container() let detail = try detail() let service = LeadARProjectsService(detail: detail, container: container) let original = try service.createProject(name: "Alt", tank: Tank()) let stored = try service.resolveProject(id: original.id) stored.worldMapData = Data([1, 2, 3]) stored.tankTransform = [1, 2, 3] try stored.modelContext?.save() let tank = Tank(size: .t2_1, color: .olive, placement: .belowGround) let updated = try service.updateProject(id: original.id, name: " Neu ", tank: tank) #expect(updated.id == original.id) #expect(updated.createdAt == original.createdAt) let reopened = LeadARProjectsService(detail: detail, container: container) let project = try reopened.resolveProject(id: original.id) #expect(project.projectName == "Neu") #expect(project.tank == tank) #expect(project.worldMapData == Data([1, 2, 3])) #expect(project.tankTransform == [1, 2, 3]) #expect(try ModelContext(container).fetchCount(FetchDescriptor()) == 1) } @Test func editingRejectsSuppliedForeignAndInvalidProjects() throws { let container = try container() let service = LeadARProjectsService(detail: try detail(), container: container) let supplied = try #require(service.loadProjects().first) #expect(throws: LeadARProjectsService.ProjectError.self) { try service.updateProject(id: supplied.id, name: "Changed", tank: Tank()) } let own = try service.createProject(name: "Original", tank: Tank()) #expect(throws: LeadARProjectsService.ProjectError.self) { try service.updateProject(id: own.id, name: " ", tank: Tank()) } #expect(throws: LeadARProjectsService.ProjectError.self) { try service.updateProject(id: own.id, name: "Invalid", tank: Tank(color: .white, placement: .belowGround)) } let other = LeadARProjectsService(detail: try detail("L-240330"), container: container) #expect(throws: LeadARProjectsService.ProjectError.self) { try other.updateProject(id: own.id, name: "Foreign", tank: Tank()) } #expect(try service.resolveProject(id: own.id).projectName == "Original") } @Test func galleryLoadsNewARPhotosWithoutDuplicatesAndExcludesOtherLeads() throws { let container = try container() let detail = try detail() let service = LeadARProjectsService(detail: detail, container: container) let own = try service.createProject(name: "Garten", tank: Tank()) let ownProject = try service.resolveProject(id: own.id) let context = try #require(ownProject.modelContext) let adapter = ProjectPersistenceAdapter(project: ownProject, modelContext: context) adapter.savePhoto(Data([1]), date: Date(timeIntervalSince1970: 1), coordinate: nil) let supplied = try #require(service.loadProjects().first { $0.source == .supplied }) let suppliedProject = try service.resolveProject(id: supplied.id) ProjectPersistenceAdapter(project: suppliedProject, modelContext: context) .savePhoto(Data([2]), date: Date(timeIntervalSince1970: 2), coordinate: nil) let other = LeadARProjectsService(detail: try self.detail("L-240330"), container: container) let otherValue = try other.createProject(name: "Anderer Kunde", tank: Tank()) let otherProject = try other.resolveProject(id: otherValue.id) ProjectPersistenceAdapter(project: otherProject, modelContext: try #require(otherProject.modelContext)) .savePhoto(Data([9]), date: .now, coordinate: nil) let initial = try LeadARPhotoStore(container: container).photos(for: detail.lead.id) #expect(initial.map(\.imageData) == [Data([2]), Data([1])]) adapter.savePhoto(Data([3]), date: Date(timeIntervalSince1970: 3), coordinate: nil) let refreshed = try LeadARPhotoStore(container: container).photos(for: detail.lead.id) #expect(refreshed.map(\.imageData) == [Data([3]), Data([2]), Data([1])]) try service.deleteProject(id: own.id) #expect(try LeadARPhotoStore(container: container).photos(for: detail.lead.id).count == 1) } @Test func associationPersistsWhenDiskStoreReopens() throws { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } let url = directory.appendingPathComponent("projects.store") let detail = try detail() let projectID: UUID do { let store = try ModelContainer(for: Project.self, LeadARProjectLink.self, configurations: ModelConfiguration(url: url)) projectID = try LeadARProjectsService(detail: detail, container: store) .createProject(name: "Persistiert", tank: Tank()).id } let reopened = try ModelContainer(for: Project.self, LeadARProjectLink.self, configurations: ModelConfiguration(url: url)) let projects = try LeadARProjectsService(detail: detail, container: reopened).loadProjects() #expect(projects.contains { $0.id == projectID && $0.name == "Persistiert" }) } }