import XCTest @testable import AIStudyApp final class EventMapperTests: XCTestCase { // MARK: - eventType mapping func test_apiEventType_materialOpened() { XCTAssertEqual(ReadingEventMapper.apiEventType(.materialOpened), "material_opened") } func test_apiEventType_materialClosed() { XCTAssertEqual(ReadingEventMapper.apiEventType(.materialClosed), "material_closed") } func test_apiEventType_positionChanged() { XCTAssertEqual(ReadingEventMapper.apiEventType(.positionChanged), "position_changed") } func test_apiEventType_heartbeat() { XCTAssertEqual(ReadingEventMapper.apiEventType(.heartbeat), "heartbeat") } func test_apiEventType_markedAsRead() { XCTAssertEqual(ReadingEventMapper.apiEventType(.markedAsRead), "marked_as_read") } // MARK: - AppContext integration func test_map_uses_appContext_fields() { let ctx = ReadingMaterialContext( readingTargetType: .knowledgeSource, materialId: "mat-001", knowledgeBaseId: "kb-001" ) // No Rust events to map, but verify AppContext is used let appCtx = AppContext( platform: "ipadOS", appVersion: "2.0.0", buildNumber: "100", clientTimezoneOffsetMinutes: 480, locale: "en-US", deviceType: "tablet", osVersion: "18.0" ) let items = ReadingEventMapper.map(rustEvents: [], contexts: ["mat-001": ctx], appContext: appCtx) XCTAssertTrue(items.isEmpty, "Empty events → empty result") } func test_map_one_without_context_returns_nil() { let ctx = ReadingMaterialContext( readingTargetType: .temporaryFile, materialId: "mat-002" ) let items = ReadingEventMapper.map( rustEvents: [], contexts: ["other-id": ctx] ) XCTAssertTrue(items.isEmpty) } // MARK: - All event types are mapped func test_all_five_event_types_have_mapping() { let types: [ReadingEventTypeV2] = [ .materialOpened, .materialClosed, .positionChanged, .heartbeat, .markedAsRead ] for type in types { let str = ReadingEventMapper.apiEventType(type) XCTAssertFalse(str.isEmpty, "Event type \(type) should have non-empty string mapping") XCTAssertTrue(str.contains("_"), "Should be snake_case: \(str)") } } // MARK: - AppContext refreshed per call func test_map_refreshes_timezone_per_call() { let ctx = ReadingMaterialContext( readingTargetType: .knowledgeSource, materialId: "mat-tz" ) let appCtx = AppContext( platform: "ios", appVersion: "1.0.0", buildNumber: "1", clientTimezoneOffsetMinutes: 0, // deliberately wrong locale: nil, deviceType: nil, osVersion: nil ) // map() calls refreshed() internally, so timezone should be updated let items = ReadingEventMapper.map(rustEvents: [], contexts: ["mat-tz": ctx], appContext: appCtx) XCTAssertNotNil(items) } }