import XCTest final class OpenClimbTests: XCTestCase { override func setUpWithError() throws { } override func tearDownWithError() throws { } // MARK: - Data Validation Tests func testDifficultyGradeComparison() throws { // Test basic difficulty grade string comparison let grade1 = "V5" let grade2 = "V3" let grade3 = "V5" XCTAssertEqual(grade1, grade3) XCTAssertNotEqual(grade1, grade2) XCTAssertFalse(grade1.isEmpty) } func testClimbTypeValidation() throws { // Test climb type validation let validClimbTypes = ["ROPE", "BOULDER"] for climbType in validClimbTypes { XCTAssertTrue(validClimbTypes.contains(climbType)) XCTAssertFalse(climbType.isEmpty) } let invalidTypes = ["", "unknown", "invalid", "sport", "trad", "toprope"] for invalidType in invalidTypes { if !invalidType.isEmpty { XCTAssertFalse(validClimbTypes.contains(invalidType)) } } } func testDateFormatting() throws { // Test ISO 8601 date formatting let formatter = ISO8601DateFormatter() let date = Date() let formattedDate = formatter.string(from: date) XCTAssertFalse(formattedDate.isEmpty) XCTAssertTrue(formattedDate.contains("T")) XCTAssertTrue(formattedDate.hasSuffix("Z")) // Test parsing back let parsedDate = formatter.date(from: formattedDate) XCTAssertNotNil(parsedDate) } func testSessionDurationCalculation() throws { // Test session duration calculation let startTime = Date() let endTime = Date(timeInterval: 3600, since: startTime) // 1 hour later let duration = endTime.timeIntervalSince(startTime) XCTAssertEqual(duration, 3600, accuracy: 1.0) XCTAssertGreaterThan(duration, 0) } func testAttemptResultValidation() throws { // Test attempt result validation let validResults = ["completed", "failed", "flash", "project"] for result in validResults { XCTAssertTrue(validResults.contains(result)) XCTAssertFalse(result.isEmpty) } } func testGymCreation() throws { // Test gym model creation with basic validation let gymName = "Test Climbing Gym" let location = "Test City" let supportedTypes = ["BOULDER", "ROPE"] XCTAssertFalse(gymName.isEmpty) XCTAssertFalse(location.isEmpty) XCTAssertFalse(supportedTypes.isEmpty) XCTAssertEqual(supportedTypes.count, 2) XCTAssertTrue(supportedTypes.contains("BOULDER")) XCTAssertTrue(supportedTypes.contains("ROPE")) } func testProblemValidation() throws { // Test problem model validation let problemName = "Test Problem" let climbType = "BOULDER" let difficulty = "V5" let tags = ["overhang", "crimpy"] XCTAssertFalse(problemName.isEmpty) XCTAssertTrue(["BOULDER", "ROPE"].contains(climbType)) XCTAssertFalse(difficulty.isEmpty) XCTAssertEqual(tags.count, 2) XCTAssertTrue(tags.allSatisfy { !$0.isEmpty }) } func testSessionStatusTransitions() throws { // Test session status transitions let validStatuses = ["planned", "active", "completed", "cancelled"] for status in validStatuses { XCTAssertTrue(validStatuses.contains(status)) XCTAssertFalse(status.isEmpty) } // Test status transitions logic let initialStatus = "planned" let activeStatus = "active" let completedStatus = "completed" XCTAssertNotEqual(initialStatus, activeStatus) XCTAssertNotEqual(activeStatus, completedStatus) } func testUniqueIDGeneration() throws { // Test unique ID generation using UUID let id1 = UUID().uuidString let id2 = UUID().uuidString XCTAssertNotEqual(id1, id2) XCTAssertFalse(id1.isEmpty) XCTAssertFalse(id2.isEmpty) XCTAssertEqual(id1.count, 36) // UUID string length XCTAssertTrue(id1.contains("-")) } func testDataValidation() throws { // Test basic data validation patterns let emptyString = "" let validString = "test" let negativeNumber = -1 let positiveNumber = 5 let zeroNumber = 0 XCTAssertTrue(emptyString.isEmpty) XCTAssertFalse(validString.isEmpty) XCTAssertLessThan(negativeNumber, 0) XCTAssertGreaterThan(positiveNumber, 0) XCTAssertEqual(zeroNumber, 0) } // MARK: - Collection Tests func testArrayOperations() throws { // Test array operations for climb data var problems: [String] = [] XCTAssertTrue(problems.isEmpty) XCTAssertEqual(problems.count, 0) problems.append("Problem 1") problems.append("Problem 2") XCTAssertFalse(problems.isEmpty) XCTAssertEqual(problems.count, 2) XCTAssertTrue(problems.contains("Problem 1")) let filteredProblems = problems.filter { $0.contains("1") } XCTAssertEqual(filteredProblems.count, 1) } func testDictionaryOperations() throws { // Test dictionary operations for data storage var gymData: [String: Any] = [:] XCTAssertTrue(gymData.isEmpty) gymData["name"] = "Test Gym" gymData["location"] = "Test City" gymData["types"] = ["BOULDER", "ROPE"] XCTAssertFalse(gymData.isEmpty) XCTAssertEqual(gymData.count, 3) XCTAssertNotNil(gymData["name"]) if let name = gymData["name"] as? String { XCTAssertEqual(name, "Test Gym") } else { XCTFail("Failed to cast gym name to String") } } // MARK: - String and Numeric Tests func testStringManipulation() throws { // Test string operations common in climb data let problemName = " Test Problem V5 " let trimmedName = problemName.trimmingCharacters(in: .whitespacesAndNewlines) let uppercaseName = trimmedName.uppercased() let lowercaseName = trimmedName.lowercased() XCTAssertEqual(trimmedName, "Test Problem V5") XCTAssertEqual(uppercaseName, "TEST PROBLEM V5") XCTAssertEqual(lowercaseName, "test problem v5") let components = trimmedName.components(separatedBy: " ") XCTAssertEqual(components.count, 3) XCTAssertEqual(components.last, "V5") } func testNumericOperations() throws { // Test numeric operations for climb ratings and statistics let grades = [3, 5, 7, 4, 6] let sum = grades.reduce(0, +) let average = Double(sum) / Double(grades.count) let maxGrade = grades.max() ?? 0 let minGrade = grades.min() ?? 0 XCTAssertEqual(sum, 25) XCTAssertEqual(average, 5.0, accuracy: 0.01) XCTAssertEqual(maxGrade, 7) XCTAssertEqual(minGrade, 3) } // MARK: - JSON and Data Format Tests func testJSONSerialization() throws { // Test JSON serialization for basic data structures let testData: [String: Any] = [ "id": "test123", "name": "Test Gym", "active": true, "rating": 4.5, "types": ["BOULDER", "ROPE"], ] XCTAssertNoThrow({ let jsonData = try JSONSerialization.data(withJSONObject: testData) XCTAssertFalse(jsonData.isEmpty) let deserializedData = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] XCTAssertNotNil(deserializedData) XCTAssertEqual(deserializedData?["name"] as? String, "Test Gym") }) } func testDateSerialization() throws { // Test date serialization for API compatibility let date = Date() let formatter = ISO8601DateFormatter() let dateString = formatter.string(from: date) let parsedDate = formatter.date(from: dateString) XCTAssertNotNil(parsedDate) XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0) } // MARK: - Active Session Preservation Tests func testActiveSessionPreservationDuringImport() throws { // Test that active sessions are preserved during import operations // This tests the fix for the bug where active sessions disappear after sync // Simulate an active session that exists locally but not in import data let activeSessionId = UUID() let gymId = UUID() // Test data structure representing local active session let localActiveSession: [String: Any] = [ "id": activeSessionId.uuidString, "gymId": gymId.uuidString, "status": "active", "date": "2024-01-01", "startTime": "2024-01-01T10:00:00Z", ] // Test data structure representing server sessions (without the active one) let serverSessions: [[String: Any]] = [ [ "id": UUID().uuidString, "gymId": gymId.uuidString, "status": "completed", "date": "2023-12-31", "startTime": "2023-12-31T15:00:00Z", "endTime": "2023-12-31T17:00:00Z", ] ] // Verify test setup XCTAssertEqual(localActiveSession["status"] as? String, "active") XCTAssertEqual(serverSessions.count, 1) XCTAssertEqual(serverSessions[0]["status"] as? String, "completed") // Verify that the active session ID is not in the server sessions let serverSessionIds = serverSessions.compactMap { $0["id"] as? String } XCTAssertFalse(serverSessionIds.contains(activeSessionId.uuidString)) // Test that we can identify an active session if let status = localActiveSession["status"] as? String { XCTAssertTrue(status == "active") } else { XCTFail("Failed to extract session status") } // Test session ID validation if let sessionIdString = localActiveSession["id"] as? String, let sessionId = UUID(uuidString: sessionIdString) { XCTAssertEqual(sessionId, activeSessionId) } else { XCTFail("Failed to parse session ID") } // Test that combining sessions preserves both local active and server completed var combinedSessions = serverSessions combinedSessions.append(localActiveSession) XCTAssertEqual(combinedSessions.count, 2) // Verify both session types are present let hasActiveSession = combinedSessions.contains { session in (session["status"] as? String) == "active" } let hasCompletedSession = combinedSessions.contains { session in (session["status"] as? String) == "completed" } XCTAssertTrue(hasActiveSession, "Combined sessions should contain active session") XCTAssertTrue(hasCompletedSession, "Combined sessions should contain completed session") } // MARK: - Orphaned Data Cleanup Tests func testOrphanedAttemptDetection() throws { // Test that we can detect orphaned attempts (attempts referencing non-existent sessions) let validSessionId = UUID() let deletedSessionId = UUID() let validProblemId = UUID() // Simulate a list of valid sessions let validSessions = [validSessionId] // Simulate attempts - one valid, one orphaned let validAttempt: [String: Any] = [ "id": UUID().uuidString, "sessionId": validSessionId.uuidString, "problemId": validProblemId.uuidString, "result": "completed", ] let orphanedAttempt: [String: Any] = [ "id": UUID().uuidString, "sessionId": deletedSessionId.uuidString, "problemId": validProblemId.uuidString, "result": "completed", ] let allAttempts = [validAttempt, orphanedAttempt] // Filter to find orphaned attempts let orphaned = allAttempts.filter { attempt in guard let sessionIdString = attempt["sessionId"] as? String, let sessionId = UUID(uuidString: sessionIdString) else { return false } return !validSessions.contains(sessionId) } XCTAssertEqual(orphaned.count, 1, "Should detect exactly one orphaned attempt") XCTAssertEqual(orphaned[0]["sessionId"] as? String, deletedSessionId.uuidString) } func testOrphanedAttemptRemoval() throws { // Test that orphaned attempts can be properly removed from a list let validSessionId = UUID() let deletedSessionId = UUID() let problemId = UUID() let validSessions = Set([validSessionId]) // Create test attempts var attempts: [[String: Any]] = [ [ "id": UUID().uuidString, "sessionId": validSessionId.uuidString, "problemId": problemId.uuidString, "result": "completed", ], [ "id": UUID().uuidString, "sessionId": deletedSessionId.uuidString, "problemId": problemId.uuidString, "result": "failed", ], [ "id": UUID().uuidString, "sessionId": validSessionId.uuidString, "problemId": problemId.uuidString, "result": "flash", ], ] XCTAssertEqual(attempts.count, 3, "Should start with 3 attempts") // Remove orphaned attempts attempts.removeAll { attempt in guard let sessionIdString = attempt["sessionId"] as? String, let sessionId = UUID(uuidString: sessionIdString) else { return true } return !validSessions.contains(sessionId) } XCTAssertEqual(attempts.count, 2, "Should have 2 attempts after cleanup") // Verify remaining attempts are all valid for attempt in attempts { if let sessionIdString = attempt["sessionId"] as? String, let sessionId = UUID(uuidString: sessionIdString) { XCTAssertTrue( validSessions.contains(sessionId), "All remaining attempts should reference valid sessions") } } } func testCascadeDeleteSessionWithAttempts() throws { // Test that deleting a session properly tracks all its attempts as deleted let sessionId = UUID() let problemId = UUID() // Create attempts for this session let sessionAttempts: [[String: Any]] = [ [ "id": UUID().uuidString, "sessionId": sessionId.uuidString, "problemId": problemId.uuidString, ], [ "id": UUID().uuidString, "sessionId": sessionId.uuidString, "problemId": problemId.uuidString, ], [ "id": UUID().uuidString, "sessionId": sessionId.uuidString, "problemId": problemId.uuidString, ], ] XCTAssertEqual(sessionAttempts.count, 3, "Session should have 3 attempts") // Simulate tracking deletions var deletedItems: [String] = [] // Add session to deleted items deletedItems.append(sessionId.uuidString) // Add all attempts to deleted items for attempt in sessionAttempts { if let attemptId = attempt["id"] as? String { deletedItems.append(attemptId) } } XCTAssertEqual(deletedItems.count, 4, "Should track 1 session + 3 attempts as deleted") XCTAssertTrue(deletedItems.contains(sessionId.uuidString), "Should track session deletion") // Verify all attempt IDs are tracked let attemptIds = sessionAttempts.compactMap { $0["id"] as? String } for attemptId in attemptIds { XCTAssertTrue( deletedItems.contains(attemptId), "Should track attempt \(attemptId) deletion") } } func testDataIntegrityValidation() throws { // Test data integrity validation logic let gymId = UUID() let sessionId = UUID() let problemId = UUID() // Valid data setup let gyms = [gymId] let sessions = [(id: sessionId, gymId: gymId)] let problems = [(id: problemId, gymId: gymId)] let attempts = [ (id: UUID(), sessionId: sessionId, problemId: problemId), (id: UUID(), sessionId: sessionId, problemId: problemId), ] // Validate that all relationships are correct let validGyms = Set(gyms) let validSessions = Set(sessions.map { $0.id }) let validProblems = Set(problems.map { $0.id }) // Check sessions reference valid gyms for session in sessions { XCTAssertTrue(validGyms.contains(session.gymId), "Session should reference valid gym") } // Check problems reference valid gyms for problem in problems { XCTAssertTrue(validGyms.contains(problem.gymId), "Problem should reference valid gym") } // Check attempts reference valid sessions and problems for attempt in attempts { XCTAssertTrue( validSessions.contains(attempt.sessionId), "Attempt should reference valid session") XCTAssertTrue( validProblems.contains(attempt.problemId), "Attempt should reference valid problem") } // Test integrity check passes let hasOrphanedSessions = sessions.contains { !validGyms.contains($0.gymId) } let hasOrphanedProblems = problems.contains { !validGyms.contains($0.gymId) } let hasOrphanedAttempts = attempts.contains { !validSessions.contains($0.sessionId) || !validProblems.contains($0.problemId) } XCTAssertFalse(hasOrphanedSessions, "Should not have orphaned sessions") XCTAssertFalse(hasOrphanedProblems, "Should not have orphaned problems") XCTAssertFalse(hasOrphanedAttempts, "Should not have orphaned attempts") } }