import XCTest @testable import AIStudyApp final class PositionAdapterTests: XCTestCase { // MARK: - Clamping func test_clamp_normal_value() { let pos = ReadingPositionAdapter.buildPosition( materialType: .markdown, blockId: "b1", scrollProgress: 0.5 ) if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.5) XCTAssertEqual(overallProgress, 0.5) } else { XCTFail("Expected block position") } } func test_clamp_below_zero() { let pos = ReadingPositionAdapter.buildPosition( materialType: .markdown, blockId: "b1", scrollProgress: -0.5 ) if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.0, "Negative progress should clamp to 0") XCTAssertEqual(overallProgress, 0.0) } else { XCTFail("Expected block position") } } func test_clamp_above_one() { let pos = ReadingPositionAdapter.buildPosition( materialType: .markdown, blockId: "b1", scrollProgress: 1.5 ) if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 1.0, "Progress >1 should clamp to 1") XCTAssertEqual(overallProgress, 1.0) } else { XCTFail("Expected block position") } } func test_clamp_nan() { let pos = ReadingPositionAdapter.buildPosition( materialType: .markdown, blockId: "b1", scrollProgress: Float.nan ) if case .block(_, let progress, let overallProgress) = pos! { XCTAssertEqual(progress, 0.0, "NaN progress should clamp to 0") XCTAssertEqual(overallProgress, 0.0) } else { XCTFail("Expected block position") } } // MARK: - Material types func test_pdf_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .pdf, pageNumber: 3, pageProgress: 0.5, overallProgress: 0.3 ) if case .pdf(let page, let pageProg, let overall) = pos! { XCTAssertEqual(page, 3) XCTAssertEqual(pageProg, 0.5) XCTAssertEqual(overall, 0.3) } else { XCTFail("Expected PDF position") } } func test_text_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .text, scrollProgress: 0.7, lineNumber: 42 ) if case .text(let line, let progress) = pos! { XCTAssertEqual(line, 42) XCTAssertEqual(progress, 0.7) } else { XCTFail("Expected text position") } } func test_image_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .image, zoomScale: 2.0, offsetX: 100, offsetY: 200 ) if case .image(let zoom, let ox, let oy) = pos! { XCTAssertEqual(zoom, 2.0) XCTAssertEqual(ox, 100) XCTAssertEqual(oy, 200) } else { XCTFail("Expected image position") } } func test_epub_position() { let pos = ReadingPositionAdapter.buildPosition( materialType: .epub, chapterProgress: 0.4, overallProgress: 0.2, chapterId: "ch3" ) if case .epub(let ch, let chProg, let overall) = pos! { XCTAssertEqual(ch, "ch3") XCTAssertEqual(chProg, 0.4) XCTAssertEqual(overall, 0.2) } else { XCTFail("Expected epub position") } } func test_unknown_material_returns_unknown() { let pos = ReadingPositionAdapter.buildPosition(materialType: .unknown) if case .unknown = pos! { // Expected } else { XCTFail("Expected unknown position") } } func test_markdown_missing_block_id_returns_nil() { let pos = ReadingPositionAdapter.buildPosition( materialType: .markdown, blockId: nil, scrollProgress: 0.5 ) XCTAssertNil(pos, "Markdown without blockId should return nil") } func test_epub_missing_chapter_id_returns_nil() { let pos = ReadingPositionAdapter.buildPosition( materialType: .epub, chapterId: nil, chapterProgress: 0.5 ) XCTAssertNil(pos, "Epub without chapterId should return nil") } // MARK: - blockId extraction func test_blockId_from_heading() { let block = DocumentBlock.heading(id: "h1", level: 1, text: "Title") XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "h1") } func test_blockId_from_paragraph() { let block = DocumentBlock.paragraph(id: "p1", text: "Hello") XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "p1") } func test_blockId_from_codeBlock() { let block = DocumentBlock.codeBlock(id: "c1", language: "swift", code: "print()") XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "c1") } }