import XCTest @testable import AIStudyApp // MARK: - Mock services private final class MockKnowledgeBaseService: KnowledgeBaseServicing { var detailResult: Result = .failure(NSError(domain: "test", code: -1)) func detail(id: String) async throws -> KnowledgeBase { try detailResult.get() } } private final class MockKnowledgeItemService: KnowledgeItemServicing { var items: [KnowledgeItem] = [] func list(knowledgeBaseId: String, sortBy: String?, order: String?) async throws -> [KnowledgeItem] { items } } private final class MockQuizService: QuizServicing { var quizzes: [Quiz] = [] func list(knowledgeBaseId: String) async throws -> [Quiz] { quizzes } } private final class MockReadingProgressService: ReadingProgressServicing { var progress: MaterialReadingProgressDTO? func getReadingProgress(materialId: String, targetType: String) async throws -> MaterialReadingProgressDTO { guard let p = progress else { throw NSError(domain: "test", code: -1) } return p } } // MARK: - Tests @MainActor final class MaterialDetailViewModelTests: XCTestCase { var vm: MaterialDetailViewModel! var mockKB: MockKnowledgeBaseService! var mockItem: MockKnowledgeItemService! var mockQuiz: MockQuizService! var mockReading: MockReadingProgressService! override func setUp() { super.setUp() mockKB = MockKnowledgeBaseService() mockItem = MockKnowledgeItemService() mockQuiz = MockQuizService() mockReading = MockReadingProgressService() vm = MaterialDetailViewModel( knowledgeBaseId: "kb-test-001", materialId: "mat-001", kbService: mockKB, itemService: mockItem, quizService: mockQuiz, readingService: mockReading ) } override func tearDown() { vm = nil super.tearDown() } // MARK: - Initial state func test_initial_isLoading_is_true() { XCTAssertTrue(vm.isLoading) } func test_initial_aiStatus_is_processing() { XCTAssertEqual(vm.aiStatus, .processing) } func test_initial_itemCount_is_zero() { XCTAssertEqual(vm.itemCount, 0) } func test_initial_isMarkedRead_is_false() { XCTAssertFalse(vm.isMarkedRead) } func test_kbId_and_matId_set() { XCTAssertEqual(vm.knowledgeBaseId, "kb-test-001") XCTAssertEqual(vm.materialId, "mat-001") } // MARK: - load() items / quizzes func test_load_sets_itemCount() async { mockItem.items = [KnowledgeItem(id: "i1", knowledgeBaseId: "kb1", title: "Item1")] await vm.load() XCTAssertEqual(vm.itemCount, 1) XCTAssertFalse(vm.isLoading) } func test_load_sets_quizCount() async { mockQuiz.quizzes = [Quiz(id: "q1", knowledgeBaseId: "kb1", title: "Q", questionCount: 3, sourceType: "ai", status: "ready")] await vm.load() XCTAssertEqual(vm.quizCount, 1) } func test_load_aiStatus_ready_when_items_present() async { mockItem.items = [KnowledgeItem(id: "i1", knowledgeBaseId: "kb1", title: "X")] await vm.load() XCTAssertEqual(vm.aiStatus, .ready) } func test_load_aiStatus_processing_when_empty() async { await vm.load() XCTAssertEqual(vm.aiStatus, .processing) } func test_load_isLoading_false_after() async { await vm.load() XCTAssertFalse(vm.isLoading) } // MARK: - load() progress func test_load_sets_readingStatus_and_seconds() async { mockReading.progress = MaterialReadingProgressDTO( materialId: "mat-001", targetType: "knowledge_source", status: "in_progress", totalActiveSeconds: 120, isMarkedRead: false, lastReadAt: nil, lastPosition: nil ) await vm.load() XCTAssertEqual(vm.readingStatus, "in_progress") XCTAssertEqual(vm.readingSeconds, 120) } func test_load_sets_isMarkedRead_true() async { mockReading.progress = MaterialReadingProgressDTO( materialId: "mat-001", targetType: "knowledge_source", status: "completed", totalActiveSeconds: 300, isMarkedRead: true, lastReadAt: nil, lastPosition: nil ) await vm.load() XCTAssertTrue(vm.isMarkedRead) } func test_load_handles_progress_error_gracefully() async { mockReading.progress = nil // throws await vm.load() XCTAssertNil(vm.readingStatus) XCTAssertEqual(vm.readingSeconds, 0) XCTAssertFalse(vm.isLoading) } func test_load_skips_progress_when_materialId_nil() async { let vm2 = MaterialDetailViewModel( knowledgeBaseId: "kb-002", materialId: nil, kbService: mockKB, itemService: mockItem, quizService: mockQuiz, readingService: mockReading ) mockReading.progress = MaterialReadingProgressDTO( materialId: "mat-001", targetType: "knowledge_source", status: "done", totalActiveSeconds: 99, isMarkedRead: true, lastReadAt: nil, lastPosition: nil ) await vm2.load() XCTAssertNil(vm2.readingStatus) } }