import XCTest @testable import AIStudyApp final class ReadingAPIClientTests: XCTestCase { // MARK: - Batch response decoding func test_batch_response_decodes_all_fields() throws { let json = """ { "processed": 5, "duplicate": 2, "failed": 1, "warnings": [ {"eventId": "evt-001", "code": "DUPLICATE_EVENT", "message": "already processed"} ] } """.data(using: .utf8)! let resp = try JSONDecoder().decode(ReadingEventBatchResponse.self, from: json) XCTAssertEqual(resp.processed, 5) XCTAssertEqual(resp.duplicate, 2) XCTAssertEqual(resp.failed, 1) XCTAssertEqual(resp.warnings?.count, 1) } func test_batch_response_no_warnings() throws { let json = """ { "processed": 3, "duplicate": 0, "failed": 0 } """.data(using: .utf8)! let resp = try JSONDecoder().decode(ReadingEventBatchResponse.self, from: json) XCTAssertEqual(resp.processed, 3) XCTAssertNil(resp.warnings) } func test_batch_response_duplicate_not_counted_as_failed() throws { let json = """ { "processed": 1, "duplicate": 5, "failed": 0 } """.data(using: .utf8)! let resp = try JSONDecoder().decode(ReadingEventBatchResponse.self, from: json) // Duplicate should not count as failed XCTAssertEqual(resp.failed, 0) XCTAssertEqual(resp.duplicate, 5) } // MARK: - API service existence func test_api_service_shared_exists() { let api = ReadingAPIService.shared XCTAssertNotNil(api) } // MARK: - Other response DTOs func test_material_reading_progress_decodes() throws { let json = """ { "status": "reading", "lastProgress": 0.65, "totalActiveSeconds": 1200, "isMarkedRead": false, "firstOpenedAt": "2026-01-01T00:00:00Z", "lastReadAt": "2026-06-12T00:00:00Z" } """.data(using: .utf8)! let dto = try JSONDecoder().decode(MaterialReadingProgressDTO.self, from: json) XCTAssertEqual(dto.status, "reading") XCTAssertEqual(dto.totalActiveSeconds, 1200) XCTAssertFalse(dto.isMarkedRead) } func test_continue_learning_response_decodes() throws { let json = """ { "type": "knowledge_source", "materialId": "mat-001", "title": "Rust Book", "lastProgress": 0.4, "totalActiveSeconds": 600, "lastReadAt": "2026-06-12T00:00:00Z" } """.data(using: .utf8)! let dto = try JSONDecoder().decode(ContinueLearningResponse.self, from: json) XCTAssertEqual(dto.type, "knowledge_source") XCTAssertEqual(dto.materialId, "mat-001") } func test_continue_learning_response_none_type() throws { let json = """ { "type": "none", "materialId": null, "title": null, "lastProgress": null, "totalActiveSeconds": null, "lastReadAt": null } """.data(using: .utf8)! let dto = try JSONDecoder().decode(ContinueLearningResponse.self, from: json) XCTAssertEqual(dto.type, "none") XCTAssertNil(dto.materialId) } func test_learning_summary_decodes() throws { let json = """ { "todaySeconds": 300, "weekSeconds": 2100, "totalSeconds": 50000, "activeDays": 45, "sessionsCount": 120, "materialsReadCount": 15, "markedReadCount": 8, "dailyAverageSeconds": 1100 } """.data(using: .utf8)! let dto = try JSONDecoder().decode(LearningSummaryResponse.self, from: json) XCTAssertEqual(dto.todaySeconds, 300) XCTAssertEqual(dto.materialsReadCount, 15) } }