import CoreGraphics
import Testing
@testable import Kanban

/// `DragPreviewAnchor` — given a preview's length and the point in it the pointer grabbed, how much
/// transparent room goes on either side so the system's centred placement lands that point under the
/// cursor? The visual result is a real drag session's and cannot be asserted here; this is the one
/// decision it is made of.

/// The centre of the padded box, and the anchor's position in it — the two figures the padding
/// exists to make equal.
private func centreAndAnchor(
    length: CGFloat,
    anchor: CGFloat
) -> (centre: CGFloat, anchor: CGFloat) {
    let padding = DragPreviewAnchor.padding(length: length, anchor: anchor)
    let padded = padding.before + length + padding.after
    return (centre: padded / 2, anchor: padding.before + anchor)
}

private func isClose(_ value: CGFloat, _ expected: CGFloat, _ tolerance: CGFloat = 0.0001) -> Bool {
    abs(value - expected) <= tolerance
}

@Suite("DragPreviewAnchor")
struct DragPreviewAnchorTests {

    // MARK: The promise

    @Test("The padded preview's centre is the anchor, wherever the anchor is")
    func anchorBecomesTheCentre() {
        let length: CGFloat = 640
        for anchor in stride(from: CGFloat(0), through: length, by: 16) {
            let result = centreAndAnchor(length: length, anchor: anchor)
            #expect(
                isClose(result.centre, result.anchor),
                "anchor \(anchor) landed at \(result.anchor), centre is \(result.centre)"
            )
        }
    }

    @Test("A lane grabbed by its title bar hangs from the bar, not from its middle")
    func laneHeaderAnchor() {
        // A tall lane whose bar sits near the top: the compensation is most of a second lane's
        // height, all of it transparent and all of it above.
        let padding = DragPreviewAnchor.padding(length: 700, anchor: 20)
        #expect(padding.before == 660)
        #expect(padding.after == 0)

        // The bar's centre and the image's centre are then the same point — 20 down from the
        // replica's top edge, which is 680 down from the padded image's.
        let result = centreAndAnchor(length: 700, anchor: 20)
        #expect(isClose(result.centre, result.anchor))
        #expect(isClose(result.centre, 680))
    }

    // MARK: Which side

    @Test("An anchor above the middle pads before it, one below pads after it")
    func padsTheOppositeSide() {
        let high = DragPreviewAnchor.padding(length: 100, anchor: 30)
        #expect(high.before == 40)
        #expect(high.after == 0)

        let low = DragPreviewAnchor.padding(length: 100, anchor: 70)
        #expect(low.before == 0)
        #expect(low.after == 40)
    }

    @Test("An anchor already at the centre pads nothing")
    func centredAnchorPadsNothing() {
        #expect(DragPreviewAnchor.padding(length: 100, anchor: 50) == .none)
    }

    @Test("Either extreme pads a whole length — the preview hangs entirely from one edge")
    func extremesPadAWholeLength() {
        #expect(DragPreviewAnchor.padding(length: 100, anchor: 0) == .init(before: 100, after: 0))
        #expect(DragPreviewAnchor.padding(length: 100, anchor: 100) == .init(before: 0, after: 100))
    }

    // MARK: Symmetric padding does not disturb it

    @Test("The figure is invariant under the replica's own transparent margin")
    func invariantUnderSymmetricPadding() {
        // `BoardMetrics.replicaPadding` wraps every replica so its shadow is not clipped. Adding it
        // to both ends adds to the length and to the anchor alike, so the compensation is the same
        // number and the two paddings compose in either order.
        let margin: CGFloat = 12
        let bare = DragPreviewAnchor.padding(length: 700, anchor: 20)
        let wrapped = DragPreviewAnchor.padding(length: 700 + 2 * margin, anchor: 20 + margin)
        #expect(bare == wrapped)
    }

    // MARK: Degenerate input

    @Test("An unmeasured preview pads nothing")
    func unmeasuredPadsNothing() {
        #expect(DragPreviewAnchor.padding(length: 0, anchor: 0) == .none)
        #expect(DragPreviewAnchor.padding(length: -100, anchor: 20) == .none)
    }

    @Test("Non-finite input pads nothing")
    func nonFinitePadsNothing() {
        #expect(DragPreviewAnchor.padding(length: .infinity, anchor: 20) == .none)
        #expect(DragPreviewAnchor.padding(length: .nan, anchor: 20) == .none)
        #expect(DragPreviewAnchor.padding(length: 700, anchor: .infinity) == .none)
        #expect(DragPreviewAnchor.padding(length: 700, anchor: .nan) == .none)
    }

    @Test("An anchor outside the preview clamps to its edges")
    func anchorClampsIntoThePreview() {
        #expect(DragPreviewAnchor.padding(length: 100, anchor: -50) == .init(before: 100, after: 0))
        #expect(DragPreviewAnchor.padding(length: 100, anchor: 500) == .init(before: 0, after: 100))
    }
}
