import AppKit
import ApplicationServices
import CoreGraphics
import Foundation

struct WindowRecord {
    let ownerName: String
    let name: String
    let layer: Int
    let pid: Int
    let isOnscreen: Bool
    let bounds: CGRect
    let axBounds: CGRect?

    var verificationBounds: CGRect {
        axBounds ?? bounds
    }
}

struct ScreenRecord {
    let frame: CGRect
    let visibleFrame: CGRect
    let cgFrame: CGRect
}

enum QAError: Error, CustomStringConvertible {
    case windowListUnavailable
    case forbiddenAlignerWindows([WindowRecord])
    case unexpectedOverlayCount(count: Int, expected: Int)
    case tooManyAllowedWindows(title: String, count: Int, maximum: Int)
    case unexpectedWindowCount(title: String, count: Int, expected: Int)
    case invalidOverlayLayer(title: String, layer: Int, maximumExclusive: Int)
    case mouseOutsideScreens(location: CGPoint)
    case overlayNotOnMouseScreen(title: String, bounds: CGRect, mouseLocation: CGPoint, expectedBounds: CGRect, screenFrame: CGRect)
    case overlayDoesNotUseScreenFrame(title: String, bounds: CGRect, expectedBounds: CGRect, screenFrame: CGRect, visibleFrame: CGRect)

    var description: String {
        switch self {
        case .windowListUnavailable:
            return "CGWindowListCopyWindowInfo returned no data."
        case .forbiddenAlignerWindows(let windows):
            let details = windows.map {
                "- owner=\($0.ownerName) pid=\($0.pid) layer=\($0.layer) bounds=\(formatRect($0.bounds)) axBounds=\(formatOptionalRect($0.axBounds)) title=\"\($0.name)\""
            }.joined(separator: "\n")
            return "Unexpected Aligner windows found in Round0:\n\(details)"
        case .unexpectedOverlayCount(let count, let expected):
            return "Unexpected Aligner overlay count: found \(count), expected \(expected)."
        case .tooManyAllowedWindows(let title, let count, let maximum):
            return "Too many allowed Aligner windows titled \"\(title)\": found \(count), maximum \(maximum)."
        case .unexpectedWindowCount(let title, let count, let expected):
            return "Unexpected Aligner window count for \"\(title)\": found \(count), expected \(expected)."
        case .invalidOverlayLayer(let title, let layer, let maximumExclusive):
            return "Invalid overlay layer for \"\(title)\": found \(layer), expected below \(maximumExclusive)."
        case .mouseOutsideScreens(let location):
            return "Mouse is outside all NSScreen frames: \(formatPoint(location))."
        case .overlayNotOnMouseScreen(let title, let bounds, let mouseLocation, let expectedBounds, let screenFrame):
            return "Overlay \"\(title)\" is not on the mouse screen. bounds=\(formatRect(bounds)) expectedCGWindowBounds=\(formatRect(expectedBounds)) mouse=\(formatPoint(mouseLocation)) screen.frame=\(formatRect(screenFrame))."
        case .overlayDoesNotUseScreenFrame(let title, let bounds, let expectedBounds, let screenFrame, let visibleFrame):
            return "Overlay \"\(title)\" does not use full screen.frame. bounds=\(formatRect(bounds)) expectedCGWindowBounds=\(formatRect(expectedBounds)) screen.frame=\(formatRect(screenFrame)) visibleFrame=\(formatRect(visibleFrame))."
        }
    }
}

struct QAOptions {
    let expectDebugOverlay: Bool
    let expectDebugSettingsChild: Bool
    let expectPermissions: Bool
    let expectNoPermissions: Bool
    let expectNoDebugOverlay: Bool
    let expectNoDebugSettingsChild: Bool
    let expectRound1PerformancePoC: Bool
    let expectNoRound1PerformancePoC: Bool
    let expectQuickSwitch: Bool
    let expectNoQuickSwitch: Bool
    let expectSingleAlignerOverlay: Bool
    let expectOverlayOnMouseScreen: Bool
    let expectOverlayUsesScreenFrame: Bool

    static func parse(arguments: [String]) -> QAOptions {
        QAOptions(
            expectDebugOverlay: arguments.contains("--expect-debug-overlay"),
            expectDebugSettingsChild: arguments.contains("--expect-debug-settings-child"),
            expectPermissions: arguments.contains("--expect-permissions"),
            expectNoPermissions: arguments.contains("--expect-no-permissions"),
            expectNoDebugOverlay: arguments.contains("--expect-no-debug-overlay"),
            expectNoDebugSettingsChild: arguments.contains("--expect-no-debug-settings-child"),
            expectRound1PerformancePoC: arguments.contains("--expect-round01-performance-poc")
                || arguments.contains("--expect-round1-performance-poc"),
            expectNoRound1PerformancePoC: arguments.contains("--expect-no-round01-performance-poc")
                || arguments.contains("--expect-no-round1-performance-poc")
                || arguments.contains("--expect-no-performance-poc"),
            expectQuickSwitch: arguments.contains("--expect-quick-switch"),
            expectNoQuickSwitch: arguments.contains("--expect-no-quick-switch"),
            expectSingleAlignerOverlay: arguments.contains("--expect-single-aligner-overlay"),
            expectOverlayOnMouseScreen: arguments.contains("--expect-overlay-on-mouse-screen"),
            expectOverlayUsesScreenFrame: arguments.contains("--expect-overlay-uses-screen-frame")
        )
    }
}

func boolValue(_ value: Any?) -> Bool {
    switch value {
    case let number as NSNumber:
        return number.boolValue
    case let bool as Bool:
        return bool
    default:
        return false
    }
}

func intValue(_ value: Any?) -> Int {
    switch value {
    case let number as NSNumber:
        return number.intValue
    case let int as Int:
        return int
    default:
        return 0
    }
}

func doubleValue(_ value: Any?) -> Double {
    switch value {
    case let number as NSNumber:
        return number.doubleValue
    case let double as Double:
        return double
    case let int as Int:
        return Double(int)
    default:
        return 0
    }
}

func stringValue(_ value: Any?) -> String {
    value as? String ?? ""
}

func rectValue(_ value: Any?) -> CGRect {
    guard let dictionary = value as? [String: Any] else {
        return .zero
    }

    return CGRect(
        x: doubleValue(dictionary["X"]),
        y: doubleValue(dictionary["Y"]),
        width: doubleValue(dictionary["Width"]),
        height: doubleValue(dictionary["Height"])
    )
}

func formatRect(_ rect: CGRect) -> String {
    "x=\(Int(rect.origin.x)) y=\(Int(rect.origin.y)) w=\(Int(rect.size.width)) h=\(Int(rect.size.height))"
}

func formatOptionalRect(_ rect: CGRect?) -> String {
    guard let rect else { return "nil" }
    return formatRect(rect)
}

func formatPoint(_ point: CGPoint) -> String {
    "x=\(Int(point.x)) y=\(Int(point.y))"
}

func allWindows() throws -> [WindowRecord] {
    guard let windowInfo = CGWindowListCopyWindowInfo(
        [.optionOnScreenOnly, .excludeDesktopElements],
        kCGNullWindowID
    ) as? [[String: Any]] else {
        throw QAError.windowListUnavailable
    }

    return windowInfo.map { item in
        WindowRecord(
            ownerName: stringValue(item[kCGWindowOwnerName as String]),
            name: stringValue(item[kCGWindowName as String]),
            layer: intValue(item[kCGWindowLayer as String]),
            pid: intValue(item[kCGWindowOwnerPID as String]),
            isOnscreen: boolValue(item[kCGWindowIsOnscreen as String]),
            bounds: rectValue(item[kCGWindowBounds as String]),
            axBounds: axWindowBounds(
                pid: intValue(item[kCGWindowOwnerPID as String]),
                title: stringValue(item[kCGWindowName as String])
            )
        )
    }
}

func axWindowBounds(pid: Int, title: String) -> CGRect? {
    guard pid > 0 else { return nil }

    let appElement = AXUIElementCreateApplication(pid_t(pid))
    var windowsValue: CFTypeRef?
    guard AXUIElementCopyAttributeValue(
        appElement,
        kAXWindowsAttribute as CFString,
        &windowsValue
    ) == .success,
          let windows = windowsValue as? [AXUIElement]
    else {
        return nil
    }

    for window in windows {
        var titleValue: CFTypeRef?
        let axTitle = AXUIElementCopyAttributeValue(
            window,
            kAXTitleAttribute as CFString,
            &titleValue
        ) == .success ? titleValue as? String : nil
        guard axTitle == title else { continue }

        var positionValue: CFTypeRef?
        var sizeValue: CFTypeRef?
        guard AXUIElementCopyAttributeValue(
            window,
            kAXPositionAttribute as CFString,
            &positionValue
        ) == .success,
              AXUIElementCopyAttributeValue(
                window,
                kAXSizeAttribute as CFString,
                &sizeValue
              ) == .success,
              let positionValue,
              let sizeValue,
              CFGetTypeID(positionValue) == AXValueGetTypeID(),
              CFGetTypeID(sizeValue) == AXValueGetTypeID()
        else {
            return nil
        }

        let positionAXValue = positionValue as! AXValue
        let sizeAXValue = sizeValue as! AXValue
        var position = CGPoint.zero
        var size = CGSize.zero
        guard AXValueGetValue(positionAXValue, .cgPoint, &position),
              AXValueGetValue(sizeAXValue, .cgSize, &size)
        else {
            return nil
        }

        return CGRect(origin: position, size: size)
    }

    return nil
}

func screenRecords() -> [ScreenRecord] {
    let screens = NSScreen.screens
    let globalFrame = screens.map(\.frame).reduce(CGRect.null) { partialResult, frame in
        partialResult.union(frame)
    }

    return screens.map { screen in
        let displayID = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0
        let cgFrame = displayID == 0
            ? CGRect(
                x: screen.frame.minX,
                y: globalFrame.maxY - screen.frame.maxY,
                width: screen.frame.width,
                height: screen.frame.height
            )
            : CGDisplayBounds(displayID)

        return ScreenRecord(
            frame: screen.frame,
            visibleFrame: screen.visibleFrame,
            cgFrame: cgFrame
        )
    }
}

func mouseScreen(from screens: [ScreenRecord]) -> ScreenRecord? {
    let location = NSEvent.mouseLocation
    return screens.first { screen in
        screen.frame.contains(location)
    }
}

func equivalentRect(_ lhs: CGRect, _ rhs: CGRect, tolerance: CGFloat = 2) -> Bool {
    abs(lhs.origin.x - rhs.origin.x) <= tolerance
        && abs(lhs.origin.y - rhs.origin.y) <= tolerance
        && abs(lhs.size.width - rhs.size.width) <= tolerance
        && abs(lhs.size.height - rhs.size.height) <= tolerance
}

func equivalentWindowBounds(_ bounds: CGRect, to screen: ScreenRecord) -> Bool {
    equivalentRect(bounds, screen.cgFrame)
}

let permissionTitle = "Aligner Needs Permissions"
let debugOverlayTitle = "Aligner Round0 Debug Overlay"
let debugSettingsTitle = "Aligner Round0 Debug Settings"
let round1PerformancePoCTitle = "Aligner Round01 Performance PoC"
let quickSwitchTitle = "Aligner Quick Switch"
let overlayTitles = Set([debugOverlayTitle, round1PerformancePoCTitle, quickSwitchTitle])

func run(options: QAOptions) throws {
    let windows = try allWindows()
    let alignerWindows = windows.filter { $0.ownerName == "Aligner" }
    let alignerOverlayWindows = alignerWindows.filter { overlayTitles.contains($0.name) }
    let allowedTitleMaximums = [
        permissionTitle: 1,
        debugOverlayTitle: 1,
        debugSettingsTitle: 1,
        round1PerformancePoCTitle: 1,
        quickSwitchTitle: 1
    ]
    let allowedTitles = Set(allowedTitleMaximums.keys)
    let forbiddenWindows = alignerWindows.filter { window in
        !allowedTitles.contains(window.name)
    }

    print("WindowLogic QA")
    print("  screens: \(NSScreen.screens.count)")
    print("  onscreen windows: \(windows.count)")
    print("  aligner windows: \(alignerWindows.count)")

    for window in alignerWindows {
        print("  Aligner window: pid=\(window.pid) layer=\(window.layer) bounds=\(formatRect(window.bounds)) axBounds=\(formatOptionalRect(window.axBounds)) title=\"\(window.name)\"")
    }

    guard forbiddenWindows.isEmpty else {
        throw QAError.forbiddenAlignerWindows(forbiddenWindows)
    }

    for (title, maximum) in allowedTitleMaximums {
        let count = alignerWindows.filter { $0.name == title }.count
        if count > maximum {
            throw QAError.tooManyAllowedWindows(title: title, count: count, maximum: maximum)
        }
    }

    try assertWindow(title: debugOverlayTitle, in: alignerWindows, expected: options.expectDebugOverlay ? 1 : nil)
    try assertWindow(title: debugSettingsTitle, in: alignerWindows, expected: options.expectDebugSettingsChild ? 1 : nil)
    try assertWindow(title: permissionTitle, in: alignerWindows, expected: options.expectPermissions ? 1 : nil)
    try assertWindow(title: round1PerformancePoCTitle, in: alignerWindows, expected: options.expectRound1PerformancePoC ? 1 : nil)
    try assertWindow(title: quickSwitchTitle, in: alignerWindows, expected: options.expectQuickSwitch ? 1 : nil)

    if options.expectNoDebugOverlay {
        try assertWindow(title: debugOverlayTitle, in: alignerWindows, expected: 0)
    }
    if options.expectNoDebugSettingsChild {
        try assertWindow(title: debugSettingsTitle, in: alignerWindows, expected: 0)
    }
    if options.expectNoPermissions {
        try assertWindow(title: permissionTitle, in: alignerWindows, expected: 0)
    }
    if options.expectNoRound1PerformancePoC {
        try assertWindow(title: round1PerformancePoCTitle, in: alignerWindows, expected: 0)
    }
    if options.expectNoQuickSwitch {
        try assertWindow(title: quickSwitchTitle, in: alignerWindows, expected: 0)
    }
    if options.expectSingleAlignerOverlay && alignerOverlayWindows.count != 1 {
        throw QAError.unexpectedOverlayCount(count: alignerOverlayWindows.count, expected: 1)
    }

    let mainMenuLayer = Int(CGWindowLevelForKey(.mainMenuWindow))
    for overlay in alignerOverlayWindows {
        if overlay.layer >= mainMenuLayer {
            throw QAError.invalidOverlayLayer(
                title: overlay.name,
                layer: overlay.layer,
                maximumExclusive: mainMenuLayer
            )
        }
    }

    if options.expectOverlayOnMouseScreen || options.expectOverlayUsesScreenFrame {
        let screens = screenRecords()
        let mouseLocation = NSEvent.mouseLocation
        guard let screen = mouseScreen(from: screens) else {
            throw QAError.mouseOutsideScreens(location: mouseLocation)
        }

        for overlay in alignerOverlayWindows {
            let bounds = overlay.verificationBounds
            if options.expectOverlayOnMouseScreen && !equivalentWindowBounds(bounds, to: screen) {
                throw QAError.overlayNotOnMouseScreen(
                    title: overlay.name,
                    bounds: bounds,
                    mouseLocation: mouseLocation,
                    expectedBounds: screen.cgFrame,
                    screenFrame: screen.frame
                )
            }
            if options.expectOverlayUsesScreenFrame && !equivalentWindowBounds(bounds, to: screen) {
                throw QAError.overlayDoesNotUseScreenFrame(
                    title: overlay.name,
                    bounds: bounds,
                    expectedBounds: screen.cgFrame,
                    screenFrame: screen.frame,
                    visibleFrame: screen.visibleFrame
                )
            }
        }
    }

    print("  Overlay QA check: passed")
}

func assertWindow(title: String, in windows: [WindowRecord], expected: Int?) throws {
    guard let expected else { return }

    let count = windows.filter { $0.name == title }.count
    guard count == expected else {
        throw QAError.unexpectedWindowCount(title: title, count: count, expected: expected)
    }
}

do {
    try run(options: QAOptions.parse(arguments: CommandLine.arguments))
} catch {
    fputs("WindowLogic QA failed: \(error)\n", stderr)
    exit(1)
}
