import XCTest
@testable import SaplingEditor

#if os(macOS)
import AppKit

@MainActor
final class HybridMarkdownLiveEditorHarnessTests: XCTestCase {
    func testLiveInitialFirstResponderDoesNotShowHeadingSourceBeforeUserInteraction() {
        let harness = HybridMarkdownLiveEditorHarness(source: "# Heading\nParagraph")

        harness.simulateLaunchFirstResponder()

        XCTAssertTrue(harness.headingMarkerIsHidden())
    }

    func testLiveParagraphGeometryReturnsAfterClickAndFocusAway() throws {
        let source = """
        # Heading
        Paragraph with **bold**, *italic*, `code`, and [Link](https://example.com) markers.
        Outro
        """
        let harness = HybridMarkdownLiveEditorHarness(source: source)
        harness.simulateLaunchFirstResponder()
        let initialPoint = try XCTUnwrap(harness.point(for: "Paragraph"))

        let paragraphLocation = (source as NSString).range(of: "bold").location
        harness.setSelection(NSRange(location: paragraphLocation, length: 0))
        harness.simulateFocusAway()
        let finalPoint = try XCTUnwrap(harness.point(for: "Paragraph"))

        XCTAssertEqual(initialPoint.x, finalPoint.x, accuracy: 0.001)
        XCTAssertEqual(initialPoint.y, finalPoint.y, accuracy: 0.001)
    }

    func testLiveCheckboxClickPreservesSelectionActiveLineAndGeometry() throws {
        let source = "Editing here\n* [ ] Move with arrow keys."
        let editingRange = (source as NSString).range(of: "Editing")
        let harness = HybridMarkdownLiveEditorHarness(
            source: source,
            selectedRange: NSRange(location: editingRange.location, length: 0)
        )
        harness.simulateLaunchFirstResponder()
        harness.setSelection(NSRange(location: editingRange.location, length: 0))

        let selectionBefore = harness.selectedRange()
        let activeLineBefore = harness.effectiveActiveLineIndex()
        let labelPointBefore = try XCTUnwrap(harness.point(for: "Move"))
        let checkboxFrameBefore = try XCTUnwrap(harness.checklistButtonFrame(lineIndex: 1))

        harness.clickRenderedCheckbox(lineIndex: 1)

        let labelPointAfter = try XCTUnwrap(harness.point(for: "Move"))
        let checkboxFrameAfter = try XCTUnwrap(harness.checklistButtonFrame(lineIndex: 1))
        XCTAssertEqual(harness.selectedRange(), selectionBefore)
        XCTAssertEqual(harness.effectiveActiveLineIndex(), activeLineBefore)
        XCTAssertTrue(harness.source().contains("* [x] Move with arrow keys."))
        XCTAssertEqual(labelPointBefore.x, labelPointAfter.x, accuracy: 0.001)
        XCTAssertEqual(labelPointBefore.y, labelPointAfter.y, accuracy: 0.001)
        XCTAssertEqual(checkboxFrameBefore.origin.x, checkboxFrameAfter.origin.x, accuracy: 0.001)
        XCTAssertEqual(checkboxFrameBefore.origin.y, checkboxFrameAfter.origin.y, accuracy: 0.001)
        XCTAssertEqual(checkboxFrameBefore.size.width, checkboxFrameAfter.size.width, accuracy: 0.001)
        XCTAssertEqual(checkboxFrameBefore.size.height, checkboxFrameAfter.size.height, accuracy: 0.001)
    }

    func testInitialChecklistOverlayTracksFirstLiveLayoutPass() throws {
        let source = """
        ## Navigation Checklist

        Use this section for quick keyboard testing.

        * [ ] Move with arrow keys.
        * [ ] Jump by word with Option + Arrow.
        * [ ] Extend selection with Shift + Arrow.
        * [ ] Select across multiple lines.
        * [ ] Use Home, End, Page Up, and Page Down.
        * [ ] Type into the active line after moving quickly.
        """
        let harness = HybridMarkdownLiveEditorHarness(source: source, initialWidth: 640)
        let initialGaps = try Dictionary(uniqueKeysWithValues: (4...9).map { lineIndex in
            (lineIndex, try XCTUnwrap(harness.checklistLabelGap(lineIndex: lineIndex)))
        })

        harness.simulateLayout(width: 900)

        for lineIndex in 4...9 {
            let alignmentDelta = try XCTUnwrap(harness.checklistAlignmentDelta(lineIndex: lineIndex))
            let labelGap = try XCTUnwrap(harness.checklistLabelGap(lineIndex: lineIndex))
            XCTAssertLessThan(alignmentDelta, 8, "Checklist overlay for line \(lineIndex) is not aligned with its label")
            XCTAssertEqual(
                labelGap,
                initialGaps[lineIndex] ?? labelGap,
                accuracy: 0.001,
                "Checklist overlay for line \(lineIndex) did not track the label through the first layout pass"
            )
        }
    }

    func testLiveMultiLineSelectionUsesEditableRegion() throws {
        let source = "# Heading\nParagraph\n* [ ] Task\nOutro"
        let nsSource = source as NSString
        let selectionStart = nsSource.range(of: "#").location
        let selectionEnd = nsSource.range(of: "Task").upperBound
        let taskLineIndex = 2
        let harness = HybridMarkdownLiveEditorHarness(source: source)

        harness.simulateLaunchFirstResponder()
        harness.setSelection(NSRange(location: selectionStart, length: selectionEnd - selectionStart))

        XCTAssertFalse(harness.characterIsHidden(at: nsSource.range(of: "#").location))
        XCTAssertNil(harness.checklistButtonFrame(lineIndex: taskLineIndex))

        harness.setSelection(NSRange(location: nsSource.range(of: "Outro").location, length: 0))

        XCTAssertTrue(harness.characterIsHidden(at: nsSource.range(of: "#").location))
        XCTAssertNotNil(harness.checklistButtonFrame(lineIndex: taskLineIndex))
    }

    func testLiveCodeBlockSelectionUsesWholeBlockEditableRegion() {
        let source = "Intro\n```swift\nlet value = 42\n```"
        let nsSource = source as NSString
        let harness = HybridMarkdownLiveEditorHarness(source: source)
        let cursorLocation = nsSource.range(of: "value").location

        harness.simulateLaunchFirstResponder()
        harness.setSelection(NSRange(location: cursorLocation, length: 0))

        XCTAssertFalse(harness.characterIsHidden(at: nsSource.range(of: "```swift").location))
        XCTAssertFalse(harness.characterIsHidden(at: nsSource.range(of: "```", options: .backwards).location))

        harness.setSelection(NSRange(location: nsSource.range(of: "Intro").location, length: 0))

        XCTAssertTrue(harness.characterIsHidden(at: nsSource.range(of: "```swift").location))
        XCTAssertTrue(harness.characterIsHidden(at: nsSource.range(of: "```", options: .backwards).location))
    }
}
#endif

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