import XCTest
@testable import CtrlProxy

final class HierarchyMergerTests: XCTestCase {

    // MARK: - Helpers

    private func makeElement(
        className: String = "UIView",
        bounds: ElementBounds = ElementBounds(left: 0, top: 0, right: 100, bottom: 100),
        text: String? = nil,
        contentDesc: String? = nil,
        extras: [String: String]? = nil,
        children: [UIElementInfo]? = nil
    ) -> UIElementInfo {
        UIElementInfo(
            text: text,
            contentDesc: contentDesc,
            className: className,
            bounds: bounds,
            extras: extras,
            node: children
        )
    }

    private func makeSdkNode(
        className: String = "UIView",
        bounds: SdkBounds = SdkBounds(left: 0, top: 0, right: 100, bottom: 100),
        accessibilityLabel: String? = nil,
        accessibilityIdentifier: String? = nil,
        accessibilityTraits: [String] = [],
        accessibilityCustomActions: [String] = [],
        gestureRecognizers: [SdkGestureInfo] = [],
        alpha: Float = 1.0,
        backgroundColor: String? = nil,
        cornerRadius: Float = 0,
        isAccessibilityElement: Bool = false,
        accessibilityElementsHidden: Bool = false,
        hasTapTarget: Bool = false,
        isOccluded: Bool = false,
        isUserInteractionEnabled: Bool = true,
        children: [SdkViewNode]? = nil
    ) -> SdkViewNode {
        SdkViewNode(
            className: className,
            bounds: bounds,
            accessibilityLabel: accessibilityLabel,
            accessibilityIdentifier: accessibilityIdentifier,
            isAccessibilityElement: isAccessibilityElement,
            accessibilityElementsHidden: accessibilityElementsHidden,
            accessibilityTraits: accessibilityTraits,
            accessibilityCustomActions: accessibilityCustomActions,
            gestureRecognizers: gestureRecognizers,
            alpha: alpha,
            backgroundColor: backgroundColor,
            cornerRadius: cornerRadius,
            isUserInteractionEnabled: isUserInteractionEnabled,
            hasTapTarget: hasTapTarget,
            isOccluded: isOccluded,
            children: children
        )
    }

    private func makeHierarchy(root: UIElementInfo) -> ViewHierarchy {
        ViewHierarchy(
            packageName: "com.test.app",
            hierarchy: root,
            screenScale: 3.0,
            screenWidth: 375,
            screenHeight: 812
        )
    }

    private func makeSdkHierarchy(root: SdkViewNode) -> SdkViewHierarchy {
        SdkViewHierarchy(
            timestamp: 1000,
            bundleId: "com.test.app",
            screenScale: 3.0,
            screenWidth: 375,
            screenHeight: 812,
            root: root
        )
    }

    // MARK: - No SDK Hierarchy (graceful fallback)

    func testMergeWithNilSdkReturnsUnchanged() {
        let root = makeElement(text: "Hello")
        let hierarchy = makeHierarchy(root: root)

        let result = HierarchyMerger.merge(xcuitest: hierarchy, sdk: nil)

        XCTAssertEqual(result.hierarchy?.text, "Hello")
        XCTAssertNil(result.hierarchy?.extras)
    }

    func testMergeWithNilSdkRootReturnsUnchanged() {
        let root = makeElement(text: "Hello")
        let hierarchy = makeHierarchy(root: root)
        let sdkHierarchy = SdkViewHierarchy(
            timestamp: 1000, bundleId: nil, screenScale: 3.0,
            screenWidth: 375, screenHeight: 812, root: nil
        )

        let result = HierarchyMerger.merge(xcuitest: hierarchy, sdk: sdkHierarchy)

        XCTAssertNil(result.hierarchy?.extras)
    }

    // MARK: - Exact Match

    func testExactBoundsMatch() {
        let xcuiRoot = makeElement(
            className: "UIButton",
            bounds: ElementBounds(left: 10, top: 20, right: 110, bottom: 60)
        )
        let sdkRoot = makeSdkNode(
            className: "UIButton",
            bounds: SdkBounds(left: 10, top: 20, right: 110, bottom: 60),
            accessibilityTraits: ["button"],
            hasTapTarget: true
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertNotNil(extras)
        XCTAssertEqual(extras?["sdk.accessibilityTraits"], "button")
        XCTAssertEqual(extras?["sdk.hasTapTarget"], "true")
    }

    // MARK: - Tolerance Matching

    func testBoundsToleranceMatch() {
        let xcuiRoot = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 20, right: 100, bottom: 50)
        )
        // SDK bounds off by 1pt on each edge
        let sdkRoot = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 11, top: 21, right: 101, bottom: 51),
            accessibilityTraits: ["staticText"],
            backgroundColor: "#FF0000FF"
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertNotNil(extras)
        XCTAssertEqual(extras?["sdk.accessibilityTraits"], "staticText")
        XCTAssertEqual(extras?["sdk.backgroundColor"], "#FF0000FF")
    }

    func testBoundsOutOfToleranceNoMatch() {
        let xcuiRoot = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 20, right: 100, bottom: 50)
        )
        // SDK bounds off by 5pt — exceeds ±2 tolerance
        let sdkRoot = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 15, top: 25, right: 105, bottom: 55),
            accessibilityTraits: ["staticText"]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        XCTAssertNil(result.hierarchy?.extras)
    }

    // MARK: - Class Name Mismatch (bounds-only fallback)

    func testClassNameMismatchMatchesByBounds() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 10, top: 20, right: 100, bottom: 50)
        )
        let sdkRoot = makeSdkNode(
            className: "_UIHostingView",
            bounds: SdkBounds(left: 10, top: 20, right: 100, bottom: 50),
            backgroundColor: "#FF0000FF",
            cornerRadius: 8.0
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertNotNil(extras, "Bounds-only fallback should match even when class names differ")
        XCTAssertEqual(extras?["sdk.backgroundColor"], "#FF0000FF")
        XCTAssertEqual(extras?["sdk.cornerRadius"], "8.0")
    }

    func testExactClassMatchPreferredOverBoundsOnly() {
        let xcuiRoot = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 20, right: 100, bottom: 50)
        )
        let exactSdkNode = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 10, top: 20, right: 100, bottom: 50),
            accessibilityTraits: ["staticText"],
            backgroundColor: "#00FF00FF"
        )
        let otherSdkNode = makeSdkNode(
            className: "_UIHostingView",
            bounds: SdkBounds(left: 10, top: 20, right: 100, bottom: 50),
            backgroundColor: "#FF0000FF"
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 375, bottom: 812),
            children: [exactSdkNode, otherSdkNode]
        )
        let xcuiWrapper = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 375, bottom: 812),
            children: [xcuiRoot]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiWrapper),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let childExtras = result.hierarchy?.node?.first?.extras
        XCTAssertEqual(childExtras?["sdk.accessibilityTraits"], "staticText")
        XCTAssertEqual(childExtras?["sdk.backgroundColor"], "#00FF00FF")
    }

    func testIdentifierFallbackMatchesDifferentBounds() {
        // XCUITest and SDK have different bounds but same accessibilityIdentifier
        let xcuiWithId = UIElementInfo(
            resourceId: "layered-stack",
            className: "UIView",
            bounds: ElementBounds(left: 10, top: 100, right: 200, bottom: 300)
        )
        let sdkRoot = makeSdkNode(
            className: "_UIHostingView",
            bounds: SdkBounds(left: 15, top: 105, right: 205, bottom: 305),
            accessibilityIdentifier: "layered-stack",
            backgroundColor: "#0000FFFF",
            cornerRadius: 16.0
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiWithId),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertNotNil(extras, "Identifier fallback should match when bounds differ")
        XCTAssertEqual(extras?["sdk.backgroundColor"], "#0000FFFF")
        XCTAssertEqual(extras?["sdk.cornerRadius"], "16.0")
    }

    // MARK: - Children

    func testChildrenAreEnriched() {
        let childElement = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 50, right: 200, bottom: 80)
        )
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 375, bottom: 812),
            children: [childElement]
        )

        let childSdkNode = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 10, top: 50, right: 200, bottom: 80),
            accessibilityTraits: ["header"]
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 375, bottom: 812),
            children: [childSdkNode]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let childExtras = result.hierarchy?.node?.first?.extras
        XCTAssertEqual(childExtras?["sdk.accessibilityTraits"], "header")
    }

    // MARK: - All Extras Fields

    func testAllExtrasFieldsPopulated() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 100, bottom: 100)
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 100, bottom: 100),
            accessibilityTraits: ["button", "selected"],
            accessibilityCustomActions: ["Delete", "Share"],
            gestureRecognizers: [SdkGestureInfo(type: "UITapGestureRecognizer", isEnabled: true)],
            alpha: 0.8,
            backgroundColor: "#00FF00FF",
            cornerRadius: 12.0,
            isAccessibilityElement: true,
            accessibilityElementsHidden: true,
            hasTapTarget: true,
            isOccluded: true,
            isUserInteractionEnabled: false
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertNotNil(extras)
        XCTAssertEqual(extras?["sdk.accessibilityTraits"], "button,selected")
        XCTAssertEqual(extras?["sdk.accessibilityCustomActions"], "Delete,Share")
        XCTAssertEqual(extras?["sdk.gestureRecognizers"], "UITapGestureRecognizer(enabled)")
        XCTAssertEqual(extras?["sdk.alpha"], "0.8")
        XCTAssertEqual(extras?["sdk.backgroundColor"], "#00FF00FF")
        XCTAssertEqual(extras?["sdk.cornerRadius"], "12.0")
        XCTAssertEqual(extras?["sdk.isAccessibilityElement"], "true")
        XCTAssertEqual(extras?["sdk.accessibilityElementsHidden"], "true")
        XCTAssertEqual(extras?["sdk.hasTapTarget"], "true")
        XCTAssertEqual(extras?["sdk.isOccluded"], "true")
        XCTAssertEqual(extras?["sdk.isUserInteractionEnabled"], "false")
    }

    // MARK: - Existing Extras Preserved

    func testExistingExtrasPreserved() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 100, bottom: 100),
            extras: ["custom.key": "custom.value"]
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 100, bottom: 100),
            hasTapTarget: true
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let extras = result.hierarchy?.extras
        XCTAssertEqual(extras?["custom.key"], "custom.value")
        XCTAssertEqual(extras?["sdk.hasTapTarget"], "true")
    }

    // MARK: - SDK-Only Node Injection

    func testSdkOnlyNodeWithIdentifierInjected() {
        // XCUITest tree has a parent but is missing a child that the SDK tree has
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        // SDK tree has same parent + a child with an accessibilityIdentifier
        let sdkChild = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 10, top: 10, right: 100, bottom: 50),
            accessibilityIdentifier: "decorative-divider",
            backgroundColor: "#FF0000FF",
            cornerRadius: 4,
            children: nil
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        // The injected child should appear as a child of the root
        let children = result.hierarchy?.node
        XCTAssertNotNil(children)
        XCTAssertEqual(children?.count, 1)

        let injected = children?.first
        XCTAssertEqual(injected?.className, "UIView")
        XCTAssertEqual(injected?.resourceId, "decorative-divider")
        XCTAssertEqual(injected?.extras?["sdk.source"], "sdkWalker")
        XCTAssertEqual(injected?.extras?["sdk.backgroundColor"], "#FF0000FF")
        XCTAssertEqual(injected?.extras?["sdk.cornerRadius"], "4.0")
    }

    func testSdkOnlyNodeWithCustomActionsInjected() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let sdkChild = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 100, right: 390, bottom: 200),
            accessibilityLabel: "Message from Alice",
            accessibilityCustomActions: ["Reply", "Forward", "Delete"]
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let injected = result.hierarchy?.node?.first
        XCTAssertNotNil(injected)
        XCTAssertEqual(injected?.text, "Message from Alice")
        XCTAssertEqual(injected?.extras?["sdk.accessibilityCustomActions"], "Reply,Forward,Delete")
        XCTAssertEqual(injected?.extras?["sdk.source"], "sdkWalker")
    }

    func testSdkOnlyNodeWithA11yHiddenInjected() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let sdkChild = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 10, top: 50, right: 200, bottom: 80),
            accessibilityLabel: "Hidden helper text",
            accessibilityElementsHidden: true
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let injected = result.hierarchy?.node?.first
        XCTAssertNotNil(injected)
        XCTAssertEqual(injected?.className, "UILabel")
        XCTAssertEqual(injected?.text, "Hidden helper text")
        XCTAssertEqual(injected?.extras?["sdk.accessibilityElementsHidden"], "true")
        XCTAssertEqual(injected?.extras?["sdk.source"], "sdkWalker")
    }

    func testMatchedSdkNodeNotDuplicatedAsInjection() {
        // Both trees have the same child — it should be enriched, not injected
        let xcuiChild = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 50, right: 200, bottom: 80),
            text: "Hello"
        )
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [xcuiChild]
        )
        let sdkChild = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 10, top: 50, right: 200, bottom: 80),
            accessibilityTraits: ["staticText"]
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        // Should still be exactly 1 child (enriched, not duplicated)
        XCTAssertEqual(result.hierarchy?.node?.count, 1)
        let child = result.hierarchy?.node?.first
        XCTAssertEqual(child?.text, "Hello")
        XCTAssertEqual(child?.extras?["sdk.accessibilityTraits"], "staticText")
        XCTAssertNil(child?.extras?["sdk.source"]) // Not injected, just enriched
    }

    func testStructuralOnlyNodeNotInjected() {
        // SDK child has no identifier, label, actions, or visual properties — skip it
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let sdkChild = makeSdkNode(
            className: "UITransitionView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        // Structural-only node should not be injected
        XCTAssertNil(result.hierarchy?.node)
    }

    func testNestedSdkOnlyChildrenInjected() {
        // SDK-only parent with a meaningful child — both should be injected
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let sdkGrandchild = makeSdkNode(
            className: "UIImageView",
            bounds: SdkBounds(left: 20, top: 20, right: 60, bottom: 60),
            accessibilityIdentifier: "nested-icon"
        )
        let sdkChild = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 10, top: 10, right: 100, bottom: 100),
            accessibilityIdentifier: "nested-container",
            children: [sdkGrandchild]
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkChild]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let injectedParent = result.hierarchy?.node?.first
        XCTAssertNotNil(injectedParent)
        XCTAssertEqual(injectedParent?.resourceId, "nested-container")
        XCTAssertEqual(injectedParent?.extras?["sdk.source"], "sdkWalker")

        let injectedChild = injectedParent?.node?.first
        XCTAssertNotNil(injectedChild)
        XCTAssertEqual(injectedChild?.resourceId, "nested-icon")
        XCTAssertEqual(injectedChild?.extras?["sdk.source"], "sdkWalker")
    }

    func testInjectedNodePreservesExistingChildren() {
        // XCUITest parent has existing children; SDK-only nodes should be appended
        let xcuiChild = makeElement(
            className: "UILabel",
            bounds: ElementBounds(left: 10, top: 10, right: 200, bottom: 40),
            text: "Existing"
        )
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [xcuiChild]
        )
        let sdkExisting = makeSdkNode(
            className: "UILabel",
            bounds: SdkBounds(left: 10, top: 10, right: 200, bottom: 40)
        )
        let sdkNew = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 10, top: 50, right: 200, bottom: 100),
            accessibilityIdentifier: "sdk-only-view",
            backgroundColor: "#00FF00FF"
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [sdkExisting, sdkNew]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let children = result.hierarchy?.node
        XCTAssertEqual(children?.count, 2)
        XCTAssertEqual(children?[0].text, "Existing")
        XCTAssertNil(children?[0].extras?["sdk.source"])
        XCTAssertEqual(children?[1].resourceId, "sdk-only-view")
        XCTAssertEqual(children?[1].extras?["sdk.source"], "sdkWalker")
    }

    // MARK: - Hierarchy Metadata Preserved

    func testHierarchyMetadataPreserved() {
        let xcuiRoot = makeElement()
        let hierarchy = ViewHierarchy(
            updatedAt: 12345,
            packageName: "com.test.app",
            hierarchy: xcuiRoot,
            screenScale: 3.0,
            screenWidth: 375,
            screenHeight: 812,
            fallbackToSpringboard: true
        )

        let result = HierarchyMerger.merge(xcuitest: hierarchy, sdk: nil)

        XCTAssertEqual(result.updatedAt, 12345)
        XCTAssertEqual(result.packageName, "com.test.app")
        XCTAssertEqual(result.screenScale, 3.0)
        XCTAssertEqual(result.screenWidth, 375)
        XCTAssertEqual(result.screenHeight, 812)
        XCTAssertEqual(result.fallbackToSpringboard, true)
    }

    // MARK: - Layer-only Nodes (SwiftUI shapes via CALayer)

    func testLayerNodeBorderAndLayerFlagSurfaceInExtras() {
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 95, top: 202, right: 295, bottom: 402)
        )
        let layerNode = SdkViewNode(
            className: "CAShapeLayer",
            bounds: SdkBounds(left: 95, top: 202, right: 295, bottom: 402),
            alpha: 0.7,
            backgroundColor: "#FF000080",
            cornerRadius: 20,
            borderColor: "#00FF00FF",
            borderWidth: 2,
            isLayerNode: true
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: layerNode)
        )

        let extras = result.hierarchy?.extras
        XCTAssertEqual(extras?["sdk.backgroundColor"], "#FF000080")
        XCTAssertEqual(extras?["sdk.cornerRadius"], "20.0")
        XCTAssertEqual(extras?["sdk.borderColor"], "#00FF00FF")
        XCTAssertEqual(extras?["sdk.borderWidth"], "2.0")
        XCTAssertEqual(extras?["sdk.isLayerNode"], "true")
    }

    func testLayerOnlyChildInjectedWhenAbsentFromXcuitest() {
        // XCUITest sees only the container; SDK walker reports a CALayer shape child.
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let shapeLayer = SdkViewNode(
            className: "CAShapeLayer",
            bounds: SdkBounds(left: 95, top: 202, right: 295, bottom: 402),
            backgroundColor: "#FF000080",
            cornerRadius: 20,
            isLayerNode: true
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [shapeLayer]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let injected = result.hierarchy?.node?.first
        XCTAssertNotNil(injected, "Layer-only shape should be injected as a child")
        XCTAssertEqual(injected?.className, "CAShapeLayer")
        XCTAssertEqual(injected?.extras?["sdk.source"], "sdkWalker")
        XCTAssertEqual(injected?.extras?["sdk.isLayerNode"], "true")
        XCTAssertEqual(injected?.extras?["sdk.backgroundColor"], "#FF000080")
        XCTAssertEqual(injected?.extras?["sdk.cornerRadius"], "20.0")
    }

    func testLayerNodeBorderOnlyIsWorthInjecting() {
        // Verifies isWorthInjecting treats border-only nodes as meaningful.
        let xcuiRoot = makeElement(
            className: "UIView",
            bounds: ElementBounds(left: 0, top: 0, right: 390, bottom: 844)
        )
        let borderOnly = SdkViewNode(
            className: "CALayer",
            bounds: SdkBounds(left: 50, top: 50, right: 150, bottom: 150),
            borderColor: "#123456FF",
            borderWidth: 1,
            isLayerNode: true
        )
        let sdkRoot = makeSdkNode(
            className: "UIView",
            bounds: SdkBounds(left: 0, top: 0, right: 390, bottom: 844),
            children: [borderOnly]
        )

        let result = HierarchyMerger.merge(
            xcuitest: makeHierarchy(root: xcuiRoot),
            sdk: makeSdkHierarchy(root: sdkRoot)
        )

        let injected = result.hierarchy?.node?.first
        XCTAssertNotNil(injected)
        XCTAssertEqual(injected?.extras?["sdk.borderColor"], "#123456FF")
        XCTAssertEqual(injected?.extras?["sdk.borderWidth"], "1.0")
    }
}
