import XCTest @testable import AIStudyApp final class ReadingPositionAdapterTests: XCTestCase { private func makeValue( type: String?, blockId: String? = nil, blockProgress: Float? = nil, lineNumber: UInt32? = nil, pageNumber: UInt32? = nil, scrollProgress: Float? = nil, pageProgress: Float? = nil, overallProgress: Float? = nil, zoomScale: Float? = nil, offsetX: Float? = nil, offsetY: Float? = nil, chapterId: String? = nil, chapterProgress: Float? = nil ) -> ReadingPositionValue { ReadingPositionValue( type: type, pageNumber: pageNumber, blockId: blockId, blockProgress: blockProgress, scrollProgress: scrollProgress, lineNumber: lineNumber, pageProgress: pageProgress, overallProgress: overallProgress, zoomScale: zoomScale, offsetX: offsetX, offsetY: offsetY, chapterId: chapterId, chapterProgress: chapterProgress, offset: nil, percent: nil, section: nil ) } func test_fromValue_markdown_with_blockId() { let v = makeValue(type: "Markdown", blockId: "b1", scrollProgress: 0.5) let pos = ReadingPositionAdapter.fromValue(v) guard case let .markdown(blockId, scrollProgress) = pos else { return XCTFail("Expected .markdown, got \(String(describing: pos))") } XCTAssertEqual(blockId, "b1") XCTAssertEqual(scrollProgress, 0.5) } func test_fromValue_markdown_nil_blockId_returns_nil() { let v = makeValue(type: "Markdown") XCTAssertNil(ReadingPositionAdapter.fromValue(v)) } func test_fromValue_text_with_optional_fields() { let v = makeValue(type: "Text", lineNumber: 42, scrollProgress: 0.8) guard case let .text(lineNumber, scrollProgress) = ReadingPositionAdapter.fromValue(v) else { return XCTFail("Expected .text") } XCTAssertEqual(lineNumber, 42) XCTAssertEqual(scrollProgress, 0.8) } func test_fromValue_pdf_with_all_fields() { let v = makeValue(type: "Pdf", pageNumber: 5, pageProgress: 0.3, overallProgress: 0.15) guard case let .pdf(pageNumber, pageProgress, overallProgress) = ReadingPositionAdapter.fromValue(v) else { return XCTFail("Expected .pdf") } XCTAssertEqual(pageNumber, 5) XCTAssertEqual(pageProgress, 0.3) XCTAssertEqual(overallProgress, 0.15) } func test_fromBlockScroll_markdown_returns_position() { let blocks: [DocumentBlock] = [ .heading(id: "h1", level: 1, text: "Title"), .paragraph(id: "p1", text: "Content"), ] let pos = ReadingPositionAdapter.fromBlockScroll( materialType: .markdown, blocks: blocks, scrollProgress: 0.5, blockIndex: 0 ) guard case .block(let blockId, let blockProgress, let overallProgress) = pos else { return XCTFail("Expected .block") } XCTAssertEqual(blockId, "h1") XCTAssertEqual(blockProgress, 0.5) XCTAssertEqual(overallProgress, 0.5) } func test_blockIdForRestore_prefers_existing_blockId() { let blocks: [DocumentBlock] = [ .heading(id: "h1", level: 1, text: "Title"), .paragraph(id: "p1", text: "Content"), .paragraph(id: "p2", text: "Tail"), ] let restored = ReadingPositionAdapter.blockIdForRestore( from: .block(blockId: "p1", blockProgress: 0.25, overallProgress: 0.8), in: blocks ) XCTAssertEqual(restored, "p1") } func test_blockIdForRestore_fallsBack_to_progress_when_blockId_missing() { let blocks: [DocumentBlock] = [ .heading(id: "h1", level: 1, text: "Title"), .paragraph(id: "p1", text: "Content"), .paragraph(id: "p2", text: "Tail"), ] let restored = ReadingPositionAdapter.blockIdForRestore( from: .block(blockId: "missing", blockProgress: 0.1, overallProgress: 0.9), in: blocks ) XCTAssertEqual(restored, "p2") } func test_blockIdForRestore_text_out_of_range_fallsBack_to_scroll_progress() { let blocks: [DocumentBlock] = [ .heading(id: "h1", level: 1, text: "Title"), .paragraph(id: "p1", text: "Content"), .paragraph(id: "p2", text: "Tail"), ] let restored = ReadingPositionAdapter.blockIdForRestore( from: .text(lineNumber: 99, scrollProgress: 0.45), in: blocks ) XCTAssertEqual(restored, "p1") } }