import XCTest import Foundation @testable import ForgeLibrary final class LibraryStoreTests: XCTestCase { var dbPath: String! var store: LibraryStore! override func setUp() async throws { dbPath = NSTemporaryDirectory() + "forge-test-\(UUID().uuidString).db" store = try LibraryStore(path: dbPath) } override func tearDown() async throws { store = nil try? FileManager.default.removeItem(atPath: dbPath) } // Schema migration table exists with v1. func testMigrationVersion() throws { XCTAssertEqual(try store.schemaVersion(), 1) } // Project CRUD. func testProjectCRUD() throws { let p = try store.createProject(name: "Feature X") XCTAssertFalse(p.id.uuidString.isEmpty) XCTAssertEqual(p.name, "Feature X") let all = try store.listProjects() XCTAssertEqual(all.count, 1) XCTAssertEqual(all[0].name, "Feature X") try store.deleteProject(id: p.id) XCTAssertTrue(try store.listProjects().isEmpty) } // Day belongs to project. func testDayCRUD() throws { let p = try store.createProject(name: "P") let d = try store.createDay(projectID: p.id, label: "Day 01", date: "2026-07-10") let days = try store.listDays(projectID: p.id) XCTAssertEqual(days.count, 1) XCTAssertEqual(days[0].label, "Day 01") XCTAssertEqual(days[0].date, "2026-07-10") XCTAssertEqual(days[0].id, d.id) } // Shot CRUD with metadata + grade snapshot blob round-trip. func testShotRoundTrip() throws { let p = try store.createProject(name: "P") let d = try store.createDay(projectID: p.id, label: "D1", date: "2026-07-10") let snapshot = Data(#"{"nodes":[],"version":1}"#.utf8) var shot = Shot( dayID: d.id, clipName: "A001C001_260710_R1CD", cameraSlot: "A-Cam", timecodeIn: "10:00:00:00", timecodeOut: nil, exposureIndex: 800, whiteBalance: 5600, tint: 0, fps: 24, gradeSnapshot: snapshot) shot = try store.createShot(shot) var fetched = try XCTUnwrap(store.shot(id: shot.id)) XCTAssertEqual(fetched.clipName, "A001C001_260710_R1CD") XCTAssertEqual(fetched.cameraSlot, "A-Cam") XCTAssertEqual(fetched.exposureIndex, 800) XCTAssertEqual(fetched.gradeSnapshot, snapshot) XCTAssertNil(fetched.timecodeOut) // Update TC out on rec stop. fetched.timecodeOut = "10:02:30:12" try store.updateShot(fetched) let updated = try XCTUnwrap(store.shot(id: shot.id)) XCTAssertEqual(updated.timecodeOut, "10:02:30:12") } // Shots listed per day in insert order. func testListShotsByDay() throws { let p = try store.createProject(name: "P") let d = try store.createDay(projectID: p.id, label: "D1", date: "2026-07-10") for i in 1...3 { _ = try store.createShot(Shot(dayID: d.id, clipName: "A001C00\(i)", cameraSlot: "A-Cam")) } let shots = try store.listShots(dayID: d.id) XCTAssertEqual(shots.map(\.clipName), ["A001C001", "A001C002", "A001C003"]) } // Concurrent writers serialized safely. func testConcurrentWrites() async throws { let p = try store.createProject(name: "P") let d = try store.createDay(projectID: p.id, label: "D1", date: "2026-07-10") let s = store! let dayID = d.id await withTaskGroup(of: Void.self) { group in for i in 0..<50 { group.addTask { _ = try? s.createShot(Shot(dayID: dayID, clipName: "C\(i)", cameraSlot: "A")) } } } XCTAssertEqual(try store.listShots(dayID: d.id).count, 50) } // Reopen db: data persists. func testPersistence() throws { let p = try store.createProject(name: "Persist") store = nil store = try LibraryStore(path: dbPath) let all = try store.listProjects() XCTAssertEqual(all.count, 1) XCTAssertEqual(all[0].id, p.id) } // Cascade: deleting project removes days + shots. func testCascadeDelete() throws { let p = try store.createProject(name: "P") let d = try store.createDay(projectID: p.id, label: "D1", date: "2026-07-10") _ = try store.createShot(Shot(dayID: d.id, clipName: "C1", cameraSlot: "A")) try store.deleteProject(id: p.id) XCTAssertTrue(try store.listDays(projectID: p.id).isEmpty) XCTAssertTrue(try store.listShots(dayID: d.id).isEmpty) } }