import XCTest
@testable import SaplingEditor

final class EditorLargeDocumentValidationTests: XCTestCase {
    func testLargeDocumentOpenAndDirtyRenderPlanning() {
        for lineCount in [1_000, 5_000, 10_000] {
            let source = Self.prototypeDocument(lineCount: lineCount)
            let activeLineIndex = lineCount / 2

            let openMeasurement = elapsedTime {
                _ = EditorState(
                    document: document(source: source, lineCount: lineCount),
                    activeLineIndex: activeLineIndex
                )
            }

            let updatedSource = Self.replacingLine(
                activeLineIndex,
                in: source,
                with: "Line \(activeLineIndex + 1) edited with **bold** and `inline code`."
            )
            let updateMeasurement = elapsedTime {
                var state = EditorState(
                    document: document(source: source, lineCount: lineCount),
                    activeLineIndex: activeLineIndex
                )
                state.updateSource(updatedSource)
                _ = EditorDirtyLineInvalidator.plan(
                    previousText: source,
                    currentText: updatedSource,
                    previousActiveLineIndex: activeLineIndex,
                    currentActiveLineIndex: activeLineIndex
                )
            }
            let plan = EditorDirtyLineInvalidator.plan(
                previousText: source,
                currentText: updatedSource,
                previousActiveLineIndex: activeLineIndex,
                currentActiveLineIndex: activeLineIndex
            )
            let dirtyRenderMeasurement = elapsedTime {
                let lines = EditorActiveLineTracker.lines(from: updatedSource, activeLineIndex: activeLineIndex)
                let dirty = Set(plan.dirtyLineIndexes)
                let renderer = HybridMarkdownLineRenderer()
                _ = lines
                    .filter { dirty.contains($0.index) }
                    .map(renderer.renderPlan(for:))
            }

            XCTAssertEqual(EditorActiveLineTracker.lines(from: source, activeLineIndex: activeLineIndex).count, lineCount)
            XCTAssertFalse(plan.isFullRender)
            XCTAssertLessThanOrEqual(plan.dirtyLineCount, 3)
            XCTAssertLessThan(openMeasurement, 0.25, "Opening \(lineCount) generated lines should remain interactive.")
            XCTAssertLessThan(updateMeasurement, 0.50, "State update for \(lineCount) lines should remain bounded.")
            XCTAssertLessThan(dirtyRenderMeasurement, 0.05, "Dirty render planning should not scale with the full document.")

            if ProcessInfo.processInfo.environment["SAPLING_EDITOR_PRINT_METRICS"] == "1" {
                print(
                    "SaplingEditorMetrics lines=\(lineCount) "
                        + "openMs=\(milliseconds(openMeasurement)) "
                        + "typingMs=\(milliseconds(updateMeasurement)) "
                        + "dirtyRenderMs=\(milliseconds(dirtyRenderMeasurement)) "
                        + "dirtyLines=\(plan.dirtyLineCount) "
                        + "fullRender=\(plan.isFullRender)"
                )
            }
        }
    }

    func testLargeDocumentActiveLineNavigationDoesNotScheduleFullRender() {
        let source = Self.prototypeDocument(lineCount: 10_000)

        let plan = EditorDirtyLineInvalidator.plan(
            previousText: source,
            currentText: source,
            previousActiveLineIndex: 250,
            currentActiveLineIndex: 9_750
        )

        XCTAssertEqual(plan.reason, .activeLineChange)
        XCTAssertFalse(plan.isFullRender)
        XCTAssertEqual(plan.dirtyLineIndexes, [250, 9_750])
    }

    private func document(source: String, lineCount: Int) -> EditorDocument {
        EditorDocument(
            url: URL(fileURLWithPath: "/tmp/EditorLargeDocument-\(lineCount).md"),
            title: "EditorLargeDocument-\(lineCount)",
            source: source
        )
    }

    private func elapsedTime(_ operation: () -> Void) -> TimeInterval {
        let start = Date()
        operation()
        return Date().timeIntervalSince(start)
    }

    private func milliseconds(_ interval: TimeInterval) -> String {
        String(format: "%.3f", interval * 1000)
    }

    private static func prototypeDocument(lineCount: Int) -> String {
        (1...lineCount).map { index in
            if index.isMultiple(of: 25) {
                return "## Section \(index / 25)"
            }
            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")
    }

    private static func replacingLine(_ lineIndex: Int, in source: String, with replacement: String) -> String {
        var lines = source.components(separatedBy: "\n")
        lines[lineIndex] = replacement
        return lines.joined(separator: "\n")
    }
}
