import Foundation
import OpenClawKit
import Testing

struct ComputerInputGeometryTests {
    private func display(
        originX: Double = 0,
        originY: Double = 0,
        width: Double,
        height: Double) -> OpenClawComputerDisplayGeometry
    {
        OpenClawComputerDisplayGeometry(
            originX: originX,
            originY: originY,
            widthPoints: width,
            heightPoints: height)
    }

    private func capturedWidth(refWidth: Int?, sourceWidth: Double, sourceHeight: Double = 0) -> Double {
        OpenClawComputerInputGeometry.capturedWidth(
            refWidth: refWidth,
            sourceWidth: sourceWidth,
            sourceHeight: sourceHeight)
    }

    @Test func `display frame identity changes with physical display geometry`() {
        let display = self.display(originX: -1280, originY: 0, width: 1280, height: 800)
        let original = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 42,
            sourceWidth: 2560,
            sourceHeight: 1600,
            referenceWidth: 1280,
            display: display)
        let repeated = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 42,
            sourceWidth: 2560,
            sourceHeight: 1600,
            referenceWidth: 1280,
            display: display)
        let reindexed = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 43,
            sourceWidth: 2560,
            sourceHeight: 1600,
            referenceWidth: 1280,
            display: display)
        let resized = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 42,
            sourceWidth: 1920,
            sourceHeight: 1200,
            referenceWidth: 1280,
            display: display)
        let moved = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 42,
            sourceWidth: 2560,
            sourceHeight: 1600,
            referenceWidth: 1280,
            display: self.display(originX: 0, originY: 0, width: 1280, height: 800))

        let rescaled = OpenClawComputerInputGeometry.displayFrameId(
            displayID: 42,
            sourceWidth: 2560,
            sourceHeight: 1600,
            referenceWidth: 640,
            display: display)

        #expect(original.hasPrefix("display-frame:v1:"))
        #expect(original == repeated)
        #expect(Set([original, reindexed, resized, moved, rescaled]).count == 5)
    }

    @Test func `maps one to one when capture matches display`() {
        // Source width equals display points and refWidth: captured 1280px over
        // a 1280pt display → 1:1.
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 1280)
        #expect(captured == 1280)
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 100,
            y: 200,
            capturedWidthPixels: captured,
            display: self.display(width: 1280, height: 800))
        #expect(mapped.x == 100)
        #expect(mapped.y == 200)
    }

    @Test func `never upscales when ref width exceeds source`() {
        // refWidth larger than the capture source means the screenshot is the
        // full source width, so mapping over a matching-point display stays 1:1.
        let captured = self.capturedWidth(refWidth: 4000, sourceWidth: 1512)
        #expect(captured == 1512)
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 300,
            y: 150,
            capturedWidthPixels: captured,
            display: self.display(width: 1512, height: 982))
        #expect(mapped.x == 300)
        #expect(mapped.y == 150)
    }

    @Test func `scales up from downscaled screenshot`() {
        // Display is 2560pt wide, capture source 2560; screenshot captured at
        // refWidth 1280 → captured 1280px, scale 2x.
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560)
        #expect(captured == 1280)
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 100,
            y: 50,
            capturedWidthPixels: captured,
            display: self.display(width: 2560, height: 1600))
        #expect(mapped.x == 200)
        #expect(mapped.y == 100)
    }

    @Test func `scales down on retina when pixels exceed points`() {
        // Retina display set to 1024 logical points but 2048 physical pixels:
        // capture source (pixels) 2048 downscales to refWidth 1280, and the
        // 1280px screenshot must map back onto the 1024pt display (scale 0.8).
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2048)
        #expect(captured == 1280)
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 1280,
            y: 0,
            capturedWidthPixels: captured,
            display: self.display(width: 1024, height: 640))
        #expect(mapped.x == 1024)
        #expect(mapped.y == 0)
    }

    @Test func `adds display origin after scaling`() {
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560)
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 100,
            y: 0,
            capturedWidthPixels: captured,
            display: self.display(originX: 1512, originY: 0, width: 2560, height: 1600))
        // 100 * (2560/1280) + 1512 origin.
        #expect(mapped.x == 1712)
        #expect(mapped.y == 0)
    }

    @Test func `scales down portrait capture whose height exceeds reference width`() {
        // Portrait source 1080x1920: width cap keeps 1080, but the capture's
        // longest edge (1920) exceeds the 1280 reference width, so the agent
        // scales the whole frame down. Delivered width = 1080 * 1280 / 1920 = 720.
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 1080, sourceHeight: 1920)
        #expect(captured == 720)
        // The 720px-wide delivered frame maps back onto the 1080pt portrait
        // display (scale 1.5), so replayed coordinates land on target.
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 360,
            y: 0,
            capturedWidthPixels: captured,
            display: self.display(width: 1080, height: 1920))
        #expect(mapped.x == 540)
        #expect(mapped.y == 0)
    }

    @Test func `keeps landscape width when the width is the longest edge`() {
        // Landscape source 2560x1440: the width cap (1280) is already the longest
        // edge, so longest-edge scaling is a no-op and the width cap stands.
        let captured = self.capturedWidth(refWidth: 1280, sourceWidth: 2560, sourceHeight: 1440)
        #expect(captured == 1280)
    }

    @Test func `accepts finite positive mapping geometry with negative display origins`() {
        let display = self.display(originX: -2560, originY: -1440, width: 2560, height: 1440)
        #expect(OpenClawComputerInputGeometry.isValidMappingGeometry(
            sourceWidth: 2560,
            sourceHeight: 1440,
            display: display))
    }

    @Test func `rejects nonpositive or nonfinite mapping geometry`() {
        let validDisplay = self.display(originX: -100, originY: 200, width: 1280, height: 800)
        for invalidSourceWidth in [0, -1, Double.nan, .infinity, -.infinity] {
            #expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
                sourceWidth: invalidSourceWidth,
                sourceHeight: 800,
                display: validDisplay))
        }
        for invalidSourceHeight in [0, -1, Double.nan, .infinity, -.infinity] {
            #expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
                sourceWidth: 1280,
                sourceHeight: invalidSourceHeight,
                display: validDisplay))
        }

        let invalidDisplays = [
            display(originX: .nan, originY: 0, width: 1280, height: 800),
            display(originX: 0, originY: .infinity, width: 1280, height: 800),
            display(width: 0, height: 800),
            display(width: -1, height: 800),
            display(width: .nan, height: 800),
            display(width: .infinity, height: 800),
            display(width: 1280, height: 0),
            display(width: 1280, height: -1),
            display(width: 1280, height: .nan),
            display(width: 1280, height: .infinity),
        ]
        for display in invalidDisplays {
            #expect(!OpenClawComputerInputGeometry.isValidMappingGeometry(
                sourceWidth: 1280,
                sourceHeight: 800,
                display: display))
        }
    }

    @Test func `clamps far-edge and out-of-bounds points strictly inside the display`() {
        let display = self.display(originX: 100, originY: 200, width: 1280, height: 800)
        // The far right/bottom edge maps to origin + size, which belongs to the
        // adjacent display; clamp keeps it on the last in-bounds point.
        let farEdge = OpenClawComputerInputGeometry.clampToDisplay(
            x: 100 + 1280,
            y: 200 + 800,
            display: display)
        #expect(farEdge.x == 100 + 1279)
        #expect(farEdge.y == 200 + 799)
        // A slightly-negative epsilon overrun clamps back to the origin.
        let negative = OpenClawComputerInputGeometry.clampToDisplay(x: 99, y: 199, display: display)
        #expect(negative.x == 100)
        #expect(negative.y == 200)
        // A point already inside is unchanged.
        let inside = OpenClawComputerInputGeometry.clampToDisplay(x: 640, y: 400, display: display)
        #expect(inside.x == 640)
        #expect(inside.y == 400)
    }

    @Test func `captured width is non-positive for a non-positive reference width`() {
        // Justifies the executor rejecting refWidth <= 0 before mapping: geometry
        // would otherwise yield a non-positive width that collapses every
        // coordinate onto the display origin.
        #expect(self.capturedWidth(refWidth: 0, sourceWidth: 1280, sourceHeight: 800) <= 0)
        #expect(self.capturedWidth(refWidth: -5, sourceWidth: 1280, sourceHeight: 800) <= 0)
    }

    @Test func `falls back to origin for degenerate display`() {
        let mapped = OpenClawComputerInputGeometry.mapReferencePointToGlobal(
            x: 42,
            y: 42,
            capturedWidthPixels: 0,
            display: self.display(originX: 10, originY: 20, width: 0, height: 0))
        #expect(mapped.x == 10)
        #expect(mapped.y == 20)
    }

    @Test func `decodes partial act params`() throws {
        let json = """
        {"action":"left_click","displayFrameId":"display-frame:v1:test","x":12,"y":34,"modifiers":"shift","screenIndex":0,"refWidth":1280}
        """
        let data = try #require(json.data(using: .utf8))
        let params = try JSONDecoder().decode(OpenClawComputerActParams.self, from: data)
        #expect(params.action == .leftClick)
        #expect(params.displayFrameId == "display-frame:v1:test")
        #expect(params.x == 12)
        #expect(params.modifiers == "shift")
        #expect(params.refWidth == 1280)
    }

    @Test func `decodes scroll and hold actions`() throws {
        let scroll = try #require(
            "{\"action\":\"scroll\",\"scrollDirection\":\"down\",\"scrollAmount\":3}".data(using: .utf8))
        let scrollParams = try JSONDecoder().decode(OpenClawComputerActParams.self, from: scroll)
        #expect(scrollParams.action == .scroll)
        #expect(scrollParams.scrollDirection == .down)
        #expect(scrollParams.scrollAmount == 3)

        let hold = try #require(
            "{\"action\":\"hold_key\",\"keys\":\"space\",\"durationMs\":2000}".data(using: .utf8))
        let holdParams = try JSONDecoder().decode(OpenClawComputerActParams.self, from: hold)
        #expect(holdParams.action == .holdKey)
        #expect(holdParams.keys == "space")
        #expect(holdParams.durationMs == 2000)
    }
}
