import XCTest @testable import ByteLystDiagnostics final class DiagnosticsClientTests: XCTestCase { override func setUp() { super.setUp() // Reset to initial state Task { // Client is actor, need to await } } // MARK: - Singleton Tests func testSharedInstance() { let client1 = DiagnosticsClient.shared let client2 = DiagnosticsClient.shared XCTAssertTrue(client1 === client2, "Shared instance should be singleton") } // MARK: - Configuration Tests func testConfiguration() async { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) let state = await DiagnosticsClient.shared.getState() XCTAssertEqual(state, .idle) } // MARK: - Session State Tests func testInitialSessionState() async { let isActive = await DiagnosticsClient.shared.isSessionActive() XCTAssertFalse(isActive) let session = await DiagnosticsClient.shared.getCurrentSession() XCTAssertNil(session) } // MARK: - Breadcrumb Tests func testBreadcrumbAdding() async { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) await DiagnosticsClient.shared.breadcrumb( category: "navigation", message: "Page loaded", data: ["path": AnyCodable("/home")] ) let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs() XCTAssertEqual(breadcrumbs.count, 1) XCTAssertEqual(breadcrumbs.first?.category, "navigation") XCTAssertEqual(breadcrumbs.first?.message, "Page loaded") } func testMultipleBreadcrumbs() async { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) for i in 1...5 { await DiagnosticsClient.shared.breadcrumb( category: "test", message: "Message \(i)" ) } let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs() XCTAssertEqual(breadcrumbs.count, 5) } // MARK: - Tracing Tests func testSuccessfulTrace() async throws { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) let result = try await DiagnosticsClient.shared.trace(name: "test-operation") { return 42 } XCTAssertEqual(result, 42) } func testFailingTrace() async { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) do { _ = try await DiagnosticsClient.shared.trace(name: "failing-operation") { throw TestError.test } XCTFail("Should have thrown") } catch { // Expected XCTAssertTrue(error is TestError) } } // MARK: - Logging Tests func testLogAdding() async { let config = DiagnosticsConfiguration( productId: "test-app", anonymousInstallId: "install-123", platform: "ios", channel: "ios_app", osFamily: "ios", appVersion: "1.0.0", buildNumber: "100", releaseChannel: "beta", serverUrl: "https://api.test.com" ) await DiagnosticsClient.shared.configure(config) await DiagnosticsClient.shared.log( level: .info, message: "Test message", module: "TestModule" ) // Log is buffered, no immediate assertion possible // But breadcrumb should be created let breadcrumbs = await DiagnosticsClient.shared.getBreadcrumbs() XCTAssertTrue(breadcrumbs.contains { $0.category == "log" }) } // MARK: - Types Tests func testDiagnosticsSessionEncoding() throws { let session = DiagnosticsSession( id: "ds_test123", productId: "test-app", status: .active, collectionLevel: .debug, captureLogs: true, captureNetwork: true, captureScreenshots: false, screenshotOnError: true, maxDurationMinutes: 60, createdAt: "2026-03-03T12:00:00Z", expiresAt: "2026-03-03T13:00:00Z" ) let encoder = JSONEncoder() let data = try encoder.encode(session) let decoder = JSONDecoder() let decoded = try decoder.decode(DiagnosticsSession.self, from: data) XCTAssertEqual(decoded.id, session.id) XCTAssertEqual(decoded.status, session.status) XCTAssertEqual(decoded.collectionLevel, session.collectionLevel) } func testLogEntryCreation() { let entry = DiagnosticsLogEntry( level: .error, message: "Something went wrong", timestamp: "2026-03-03T12:00:00Z", module: "TestModule", file: "Test.swift", line: 42, function: "testFunction", context: ["key": AnyCodable("value")], correlationId: "corr-123" ) XCTAssertEqual(entry.level, .error) XCTAssertEqual(entry.message, "Something went wrong") XCTAssertEqual(entry.module, "TestModule") XCTAssertEqual(entry.line, 42) XCTAssertEqual(entry.correlationId, "corr-123") } func testTraceSpanCreation() { let span = DiagnosticsTraceSpan( spanId: "span-123", parentId: "parent-456", name: "test-span", kind: .internal, startTime: "2026-03-03T12:00:00Z", endTime: "2026-03-03T12:00:01Z", durationMs: 1000, attributes: ["key": AnyCodable("value")], status: .ok, statusMessage: nil ) XCTAssertEqual(span.spanId, "span-123") XCTAssertEqual(span.parentId, "parent-456") XCTAssertEqual(span.name, "test-span") XCTAssertEqual(span.status, .ok) } func testBreadcrumbCreation() { let breadcrumb = DiagnosticsBreadcrumb( timestamp: "2026-03-03T12:00:00Z", category: "navigation", message: "User tapped button", data: ["buttonId": AnyCodable("submit")] ) XCTAssertEqual(breadcrumb.category, "navigation") XCTAssertEqual(breadcrumb.message, "User tapped button") XCTAssertNotNil(breadcrumb.data) } func testAnyCodableEncoding() throws { let value = AnyCodable("test-string") let encoder = JSONEncoder() let data = try encoder.encode(value) // Should not throw XCTAssertFalse(data.isEmpty) } func testAnyCodableIntEncoding() throws { let value = AnyCodable(42) let encoder = JSONEncoder() let data = try encoder.encode(value) XCTAssertFalse(data.isEmpty) } func testAnyCodableBoolEncoding() throws { let value = AnyCodable(true) let encoder = JSONEncoder() let data = try encoder.encode(value) XCTAssertFalse(data.isEmpty) } } enum TestError: Error { case test }