import XCTest
import SaplingCore
@testable import SaplingEditor

final class EditorStateTests: XCTestCase {
    func testActiveLineFollowsSelection() {
        let source = "# Heading\nRendered **line**\nRaw source line"
        let document = EditorDocument(
            url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
            title: "EditorStateTests",
            source: source
        )
        var state = EditorState(document: document)

        let secondLineLocation = (source as NSString).range(of: "Rendered").location
        state.updateSelection(EditorSelection(location: secondLineLocation, length: 0))

        XCTAssertEqual(state.activeLineIndex, 1)
        XCTAssertEqual(state.lines[0].mode, .rendered)
        XCTAssertEqual(state.lines[1].mode, .source)
        XCTAssertEqual(state.lines[2].mode, .rendered)
    }

    func testSelectionIsClampedAfterDeletion() {
        let document = EditorDocument(
            url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
            title: "EditorStateTests",
            source: "First line\nSecond line\nThird line"
        )
        var state = EditorState(document: document)
        state.updateSelection(EditorSelection(location: 30, length: 20))

        state.updateSource("Short")

        XCTAssertEqual(state.selection.location, 5)
        XCTAssertEqual(state.selection.length, 0)
        XCTAssertEqual(state.activeLineIndex, 0)
        XCTAssertEqual(state.lines.count, 1)
    }

    func testLineTrackerPreservesTrailingBlankLine() {
        let lines = EditorActiveLineTracker.lines(from: "One\nTwo\n", activeLineIndex: 2)

        XCTAssertEqual(lines.count, 3)
        XCTAssertEqual(lines[2].source, "")
        XCTAssertEqual(lines[2].range.location, 8)
        XCTAssertEqual(lines[2].mode, .source)
    }

    func testUpdatingSourceTracksUnsavedChanges() {
        let document = EditorDocument(
            url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
            title: "EditorStateTests",
            source: "Initial"
        )
        var state = EditorState(document: document)

        state.updateSource("Initial\nChanged")

        XCTAssertTrue(state.hasUnsavedChanges)
        XCTAssertEqual(state.lines.count, 2)

        state.markSaved()

        XCTAssertFalse(state.hasUnsavedChanges)
    }

    func testActiveColumnFollowsSelectionWithinLine() {
        let source = "First line\nSecond line"
        let document = EditorDocument(
            url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
            title: "EditorStateTests",
            source: source
        )
        var state = EditorState(document: document)

        let secondLineLocation = (source as NSString).range(of: "Second").location
        state.updateSelection(EditorSelection(location: secondLineLocation + 3, length: 0))

        XCTAssertEqual(state.activeLineIndex, 1)
        XCTAssertEqual(state.activeColumnNumber, 4)
    }

    func testHybridRendererSupportsMilestoneTwoInlineElements() {
        let source = "Plain **bold** and *italic* with `code`"
        let line = EditorLine(
            index: 0,
            source: source,
            range: NSRange(location: 0, length: source.utf16.count),
            mode: .rendered
        )

        let plan = HybridMarkdownLineRenderer().renderPlan(for: line)

        XCTAssertEqual(plan.kind, .paragraph)
        XCTAssertTrue(plan.spans.contains { $0.kind == .bold && (source as NSString).substring(with: $0.range) == "bold" })
        XCTAssertTrue(plan.spans.contains { $0.kind == .italic && (source as NSString).substring(with: $0.range) == "italic" })
        XCTAssertTrue(plan.spans.contains { $0.kind == .inlineCode && (source as NSString).substring(with: $0.range) == "code" })
        XCTAssertEqual(plan.spans.filter { $0.kind == .markdownDelimiter }.count, 6)
    }

    func testHybridRendererSupportsHeadings() {
        let source = "## Architecture"
        let line = EditorLine(
            index: 0,
            source: source,
            range: NSRange(location: 12, length: source.utf16.count),
            mode: .rendered
        )

        let plan = HybridMarkdownLineRenderer().renderPlan(for: line)

        XCTAssertEqual(
            plan.kind,
            .heading(
                level: 2,
                markerRange: NSRange(location: 12, length: 2),
                textRange: NSRange(location: 15, length: 12)
            )
        )
    }

    func testHybridRendererSupportsTaskListsAndLinks() {
        let source = "- [ ] Task with [link](https://example.com)"
        let line = EditorLine(
            index: 0,
            source: source,
            range: NSRange(location: 0, length: source.utf16.count),
            mode: .rendered
        )

        let plan = HybridMarkdownLineRenderer().renderPlan(for: line)

        XCTAssertEqual(
            plan.kind,
            .taskList(
                markerRange: NSRange(location: 0, length: 1),
                checkboxRange: NSRange(location: 2, length: 3),
                contentRange: NSRange(location: 6, length: 37),
                checked: false,
                nestingLevel: 0
            )
        )
        XCTAssertTrue(plan.spans.contains { $0.kind == .link && (source as NSString).substring(with: $0.range) == "link" })
        XCTAssertEqual(plan.spans.filter { $0.kind == .markdownDelimiter }.count, 4)
    }

    func testHybridRendererSupportsBlockquotesHorizontalRulesAndLists() {
        let blockquoteSource = "> quoted **text**"
        let blockquote = EditorLine(
            index: 0,
            source: blockquoteSource,
            range: NSRange(location: 10, length: blockquoteSource.utf16.count),
            mode: .rendered
        )
        let horizontalRule = EditorLine(
            index: 1,
            source: "---",
            range: NSRange(location: 28, length: 3),
            mode: .rendered
        )
        let nestedList = EditorLine(
            index: 2,
            source: "  1. nested item",
            range: NSRange(location: 32, length: 16),
            mode: .rendered
        )

        let renderer = HybridMarkdownLineRenderer()

        XCTAssertEqual(
            renderer.renderPlan(for: blockquote).kind,
            .blockquote(
                markerRange: NSRange(location: 10, length: 2),
                contentRange: NSRange(location: 12, length: 15)
            )
        )
        XCTAssertEqual(renderer.renderPlan(for: horizontalRule).kind, .horizontalRule(range: horizontalRule.range))
        XCTAssertEqual(
            renderer.renderPlan(for: nestedList).kind,
            .orderedList(
                markerRange: NSRange(location: 34, length: 2),
                contentRange: NSRange(location: 37, length: 11),
                nestingLevel: 1
            )
        )
    }

    func testHybridRendererSupportsAutomaticLinks() {
        let source = "See https://example.com/docs for details"
        let line = EditorLine(
            index: 0,
            source: source,
            range: NSRange(location: 0, length: source.utf16.count),
            mode: .rendered
        )

        let plan = HybridMarkdownLineRenderer().renderPlan(for: line)

        XCTAssertTrue(plan.spans.contains {
            $0.kind == .automaticLink && (source as NSString).substring(with: $0.range) == "https://example.com/docs"
        })
    }

    func testHybridRendererSupportsFencedCodeBlocksWithLanguage() {
        let source = "```swift\nlet value = 42\n```"
        let lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: 99)

        let plans = HybridMarkdownLineRenderer().renderPlans(for: lines)

        XCTAssertEqual(
            plans[0].kind,
            .fencedCodeFence(
                markerRange: NSRange(location: 0, length: 3),
                languageRange: NSRange(location: 3, length: 5)
            )
        )
        XCTAssertEqual(plans[1].kind, .codeBlockContent(language: "swift"))
        XCTAssertEqual(plans[2].kind, .fencedCodeFence(markerRange: NSRange(location: 24, length: 3), languageRange: nil))
    }

    func testHybridRendererSupportsMarkdownTables() {
        let header = "| Name | Value |"
        let divider = "| ---- | ----- |"
        let row = "| A    | B     |"
        let source = [header, divider, row].joined(separator: "\n")
        let lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: 99)

        let plans = HybridMarkdownLineRenderer().renderPlans(for: lines)

        guard case .tableRow(let headerCells, let headerSeparators, false) = plans[0].kind else {
            return XCTFail("Expected header table row.")
        }
        guard case .tableRow(_, _, true) = plans[1].kind else {
            return XCTFail("Expected divider table row.")
        }
        guard case .tableRow(let rowCells, _, false) = plans[2].kind else {
            return XCTFail("Expected body table row.")
        }

        XCTAssertEqual(headerCells.count, 2)
        XCTAssertEqual(headerSeparators.count, 3)
        XCTAssertEqual((source as NSString).substring(with: rowCells[0]), "A")
        XCTAssertEqual((source as NSString).substring(with: rowCells[1]), "B")
    }

    @MainActor
    func testViewModelTracksActiveLineAndSelectionInstrumentation() {
        let document = MarkdownDocument(
            url: URL(fileURLWithPath: "/tmp/EditorStateTests.md"),
            title: "EditorStateTests",
            content: "One\nTwo\nThree"
        )
        let viewModel = HybridMarkdownEditorViewModel(document: document)

        viewModel.updateSelection(EditorSelection(location: 4, length: 0))
        viewModel.updateSource("One\nTwo updated\nThree")
        viewModel.recordRenderPass(EditorRenderPassMetric(
            reason: .activeLineChange,
            durationMilliseconds: 1.5,
            characterCount: 21,
            lineCount: 3,
            activeLineIndex: 1
        ))

        XCTAssertEqual(viewModel.instrumentation.selectionChangeCount, 1)
        XCTAssertEqual(viewModel.instrumentation.sourceChangeCount, 1)
        XCTAssertEqual(viewModel.instrumentation.activeLineChangeCount, 1)
        XCTAssertEqual(viewModel.instrumentation.renderPassCount, 1)
        XCTAssertEqual(viewModel.instrumentation.lastRenderReason, .activeLineChange)
    }

    func testHybridRendererHandlesLargePrototypeDocumentShape() {
        let source = Self.prototypeDocument(lineCount: 2_100)
        let lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: 1_000)
        let renderer = HybridMarkdownLineRenderer()

        let plans = lines.map(renderer.renderPlan(for:))

        XCTAssertEqual(lines.count, 2_100)
        XCTAssertEqual(plans.filter {
            if case .heading = $0.kind {
                return true
            }
            return false
        }.count, 210)
        XCTAssertEqual(plans.flatMap(\.spans).filter { $0.kind == .inlineCode }.count, 210)
    }

    @MainActor
    func testViewModelSavesDocumentToDisk() throws {
        let directory = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString, isDirectory: true)
        try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
        let url = directory.appendingPathComponent("Note.md")
        try "# Note\n".write(to: url, atomically: true, encoding: .utf8)

        let document = try HybridMarkdownEditorViewModel.loadDocument(at: url)
        let viewModel = HybridMarkdownEditorViewModel(document: document)
        viewModel.updateSource("# Note\n\nUpdated")
        try viewModel.save()

        let saved = try String(contentsOf: url, encoding: .utf8)
        XCTAssertEqual(saved, "# Note\n\nUpdated")
        XCTAssertFalse(viewModel.hasUnsavedChanges)
    }

    private static func prototypeDocument(lineCount: Int) -> String {
        (1...lineCount).map { index in
            if index.isMultiple(of: 10) {
                return "## Section \(index / 10)"
            }
            if index.isMultiple(of: 5) {
                return "Line \(index) uses **bold** context and `inline code` for scanning."
            }
            return "Line \(index) is a realistic paragraph with *light emphasis* and enough text to wrap naturally."
        }
        .joined(separator: "\n")
    }
}
