import XCTest @testable import AIStudyApp @MainActor final class MaterialDetailViewModelTests: XCTestCase { var vm: MaterialDetailViewModel! override func setUp() { super.setUp() vm = MaterialDetailViewModel(knowledgeBaseId: "kb-test-001", materialId: "mat-001") } 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_quizCount_is_zero() { XCTAssertEqual(vm.quizCount, 0) } func test_initial_isMarkedRead_is_false() { XCTAssertFalse(vm.isMarkedRead) } func test_initial_readingSeconds_is_zero() { XCTAssertEqual(vm.readingSeconds, 0) } func test_initial_readingStatus_is_nil() { XCTAssertNil(vm.readingStatus) } func test_knowledgeBaseId_is_set() { XCTAssertEqual(vm.knowledgeBaseId, "kb-test-001") } func test_materialId_is_set() { XCTAssertEqual(vm.materialId, "mat-001") } func test_init_without_materialId_defaults_to_nil() { let vm2 = MaterialDetailViewModel(knowledgeBaseId: "kb-002") XCTAssertNil(vm2.materialId) } // MARK: - statusLabel func test_statusLabel_completed() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.statusLabel("completed"), "阅读完成") } func test_statusLabel_inProgress() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.statusLabel("in_progress"), "阅读中") } func test_statusLabel_unknown() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.statusLabel("some_other"), "已开始") } func test_statusLabel_nil() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.statusLabel(nil), "已开始") } // MARK: - formatSeconds func test_formatSeconds_less_than_60() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.formatSeconds(30), "30 秒") XCTAssertEqual(view.formatSeconds(0), "0 秒") XCTAssertEqual(view.formatSeconds(59), "59 秒") } func test_formatSeconds_minutes() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.formatSeconds(60), "1 分钟") XCTAssertEqual(view.formatSeconds(3599), "59 分钟") } func test_formatSeconds_hours() { let view = MaterialDetailView( knowledgeBaseId: "kb-001", fileName: "test.md", fileType: .markdown, fileSize: 1024, filePath: "/tmp/test.md" ) XCTAssertEqual(view.formatSeconds(3600), "1 小时 0 分钟") XCTAssertEqual(view.formatSeconds(3660), "1 小时 1 分钟") XCTAssertEqual(view.formatSeconds(7200), "2 小时 0 分钟") } // MARK: - aiStatus logic (synchronous) func test_aiStatus_is_ready_when_itemCount_positive() { vm.itemCount = 5 // Simulate: loading done, items present → ready // load() sets aiStatus = itemCount > 0 ? .ready : .processing // We can't test load() without mock services, but we test the property boundary XCTAssertEqual(vm.aiStatus, .processing) // load() not yet called } // MARK: - Edge: nil materialId skips progress fetch func test_without_materialId_readingStatus_stays_nil_after_init() { let vm2 = MaterialDetailViewModel(knowledgeBaseId: "kb-003") XCTAssertNil(vm2.readingStatus) XCTAssertEqual(vm2.readingSeconds, 0) XCTAssertFalse(vm2.isMarkedRead) } }