import XCTest
@testable import SaplingEditor

final class DocumentPresentationStateTests: XCTestCase {
    func testEveryLineHasExactlyOnePresentationState() {
        let source = "# Heading\n* [x] Done\nParagraph"
        let lineIndex = DocumentLineIndex(source: source)

        let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 1)

        XCTAssertEqual(presentation.lines.count, 3)
        XCTAssertEqual(presentation.lineState(at: 0), .rendered)
        XCTAssertEqual(presentation.lineState(at: 1), .source)
        XCTAssertEqual(presentation.lineState(at: 2), .rendered)
        XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1])
        XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 2])
    }

    func testPresentationStateIsDeterministicForSameDocumentAndActiveLine() {
        let source = "# Heading\nThis has **bold** and `code`.\n* [ ] Todo"
        let lineIndex = DocumentLineIndex(source: source)

        let first = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2)
        let second = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2)

        XCTAssertEqual(first, second)
    }

    func testDocumentRenderModelIsIndependentOfActiveLinePresentation() {
        let source = "# Heading\nThis has **bold** and `code`.\n* [ ] Todo"
        let lineIndex = DocumentLineIndex(source: source)

        let renderModel = DocumentRenderModel(lineIndex: lineIndex)
        let firstPresentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 0)
        let secondPresentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 2)

        XCTAssertEqual(renderModel.snapshot, DocumentRenderModel(lineIndex: lineIndex).snapshot)
        XCTAssertNotEqual(firstPresentation, secondPresentation)
        XCTAssertTrue(renderModel.nodes.contains {
            guard case .heading(let heading) = $0.element else { return false }
            return heading.lineIndex == 0
        })
    }

    func testInitialRenderModelContainsHeadingsWithoutInteraction() {
        let source = "# Title\n\n## Section\nParagraph"
        let renderModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source))

        let headingLevels = renderModel.nodes.compactMap { node -> Int? in
            guard case .heading(let heading) = node.element else { return nil }
            return heading.level
        }

        XCTAssertEqual(headingLevels, [1, 2])
    }

    func testRenderSnapshotChangesOnlyWhenMarkdownChanges() {
        let source = "# Heading\n* [ ] Move with arrow keys."
        let originalModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source))
        let repeatedModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: source))
        let toggledSource = (source as NSString).replacingCharacters(
            in: NSRange(location: (source as NSString).range(of: "[ ]").location, length: 3),
            with: "[x]"
        )
        let toggledModel = DocumentRenderModel(lineIndex: DocumentLineIndex(source: toggledSource))

        XCTAssertEqual(originalModel.snapshot, repeatedModel.snapshot)
        XCTAssertNotEqual(originalModel.snapshot, toggledModel.snapshot)
    }

    func testRenderedTaskToggleReplacementDoesNotMoveSourceRanges() {
        let source = "Intro\n* [ ] Move with arrow keys.\nOutro"
        let lineIndex = DocumentLineIndex(source: source)
        let task = DocumentRenderModel(lineIndex: lineIndex).nodes.compactMap { node -> RenderedTaskElement? in
            guard case .task(let task) = node.element else { return nil }
            return task
        }.first

        XCTAssertEqual(task?.toggledMarkdownCheckbox, "[x]")
        XCTAssertEqual(task?.checkboxRange.length, ((task?.toggledMarkdownCheckbox ?? "") as NSString).length)
    }

    func testRenderedElementsAreSemantic() {
        let source = "# Heading\n* [x] Done\nSee [Docs](https://example.com)"
        let lineIndex = DocumentLineIndex(source: source)

        let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 99)

        XCTAssertTrue(presentation.elements.contains {
            guard case .heading(let heading) = $0 else { return false }
            return heading.lineIndex == 0 && heading.level == 1
        })
        XCTAssertEqual(presentation.renderedTasks.map(\.checked), [true])
        XCTAssertTrue(presentation.elements.contains {
            guard case .link(let link) = $0 else { return false }
            return (source as NSString).substring(with: link.titleRange) == "Docs"
        })
    }

    func testCodeBlockIsRepresentedAsSemanticBlock() {
        let source = "Intro\n```swift\nlet value = 42\n```"
        let lineIndex = DocumentLineIndex(source: source)

        let presentation = DocumentPresentationState(lineIndex: lineIndex, activeLineIndex: 0)

        XCTAssertEqual(presentation.renderedCodeBlocks.count, 1)
        XCTAssertEqual(presentation.renderedCodeBlocks[0].lineIndexes, [1, 2, 3])
        XCTAssertEqual(
            (source as NSString).substring(with: presentation.renderedCodeBlocks[0].languageRange!),
            "swift"
        )
    }

    func testDirtyPresentationResolvesNearbyCodeBlockContext() {
        let source = "Intro\n```swift\nlet value = 42\n```"
        let lineIndex = DocumentLineIndex(source: source)

        let presentation = DocumentPresentationState(
            lineIndex: lineIndex,
            activeLineIndex: 0,
            lineIndexes: [2]
        )

        XCTAssertEqual(presentation.lines.count, 1)
        XCTAssertEqual(presentation.lines[0].state, .rendered)
        XCTAssertEqual(presentation.lines[0].renderPlan.kind, .codeBlockContent(language: "swift"))
    }

    func testEditableRegionMakesMultipleSelectedLinesSource() {
        let source = "# Heading\nParagraph\n* [ ] Task\nOutro"
        let lineIndex = DocumentLineIndex(source: source)
        let selectionStart = (source as NSString).range(of: "Paragraph").location
        let selectionEnd = (source as NSString).range(of: "Task").upperBound
        let region = EditableRegion.selection(
            NSRange(location: selectionStart, length: selectionEnd - selectionStart),
            in: lineIndex
        )

        let presentation = DocumentPresentationState(lineIndex: lineIndex, editableRegion: region)

        XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1, 2])
        XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 3])
    }

    func testEditableRegionExpandsToEntireCodeBlock() {
        let source = "Intro\n```swift\nlet value = 42\n```\nOutro"
        let lineIndex = DocumentLineIndex(source: source)
        let cursorLocation = (source as NSString).range(of: "value").location

        let region = EditableRegion.selection(NSRange(location: cursorLocation, length: 0), in: lineIndex)
        let presentation = DocumentPresentationState(lineIndex: lineIndex, editableRegion: region)

        XCTAssertEqual(region.lineIndexes, [1, 2, 3])
        XCTAssertEqual(presentation.lines.filter { $0.state == .source }.map(\.line.index), [1, 2, 3])
        XCTAssertEqual(presentation.lines.filter { $0.state == .rendered }.map(\.line.index), [0, 4])
    }
}

private extension NSRange {
    var upperBound: Int {
        location + length
    }
}
