import SwiftUI
import Testing
import UIKit
import WireProtocol
@testable import WebTerm

/// UX-A · Proves the FROZEN design system: the status mapping is complete and
/// "color + shape" (never color alone), the token scales are well-formed, and
/// the adaptive accent resolves distinctly in light vs dark. Pure logic — runs
/// on both iPhone and iPad sims with no rendering.
@Suite("DesignSystem")
struct DesignSystemTests {

    // MARK: - StatusStyle: complete, distinct, color + shape

    @Test("every status maps to a non-empty symbol + Chinese label")
    func everyStatusHasSymbolAndLabel() {
        for status in DisplayStatus.allCases {
            let style = StatusStyle.style(for: status)
            #expect(!style.symbolName.isEmpty, "\(status) has no symbol")
            #expect(!style.label.isEmpty, "\(status) has no label")
        }
    }

    @Test("status is never color-alone — all seven symbols are distinct")
    func symbolsAreDistinctAcrossStatuses() {
        let symbols = DisplayStatus.allCases.map { StatusStyle.style(for: $0).symbolName }
        #expect(Set(symbols).count == DisplayStatus.allCases.count)
    }

    @Test("Chinese labels are all distinct")
    func labelsAreDistinct() {
        let labels = DisplayStatus.allCases.map { StatusStyle.style(for: $0).label }
        #expect(Set(labels).count == DisplayStatus.allCases.count)
    }

    @Test("the critical trio working/waiting/stuck use distinct colors")
    func criticalColorsAreDistinct() {
        let working = rgba(StatusStyle.style(for: DisplayStatus.working).color)
        let waiting = rgba(StatusStyle.style(for: DisplayStatus.waiting).color)
        let stuck = rgba(StatusStyle.style(for: DisplayStatus.stuck).color)
        #expect(!approxEqual(working, waiting))
        #expect(!approxEqual(working, stuck))
        #expect(!approxEqual(waiting, stuck))
    }

    @Test("semantic status colors match the desktop/web status palette")
    func statusColorsMatchDirection() {
        assertColor(StatusStyle.style(for: DisplayStatus.working).color, r: 70, g: 208, b: 127)  // #46D07F web --green
        assertColor(StatusStyle.style(for: DisplayStatus.waiting).color, r: 245, g: 177, b: 76)  // #F5B14C web --amber
        assertColor(StatusStyle.style(for: DisplayStatus.stuck).color, r: 255, g: 107, b: 107)   // #FF6B6B web --red
    }

    @Test("the wire ClaudeStatus bridge covers all five cases")
    func wireBridgeCoversEveryCase() {
        let pairs: [(ClaudeStatus, DisplayStatus)] = [
            (.working, .working), (.waiting, .waiting), (.idle, .idle),
            (.stuck, .stuck), (.unknown, .unknown),
        ]
        for (wire, expected) in pairs {
            #expect(DisplayStatus(wire) == expected)
            #expect(StatusStyle.style(for: wire) == StatusStyle.style(for: expected))
        }
    }

    // MARK: - Token scales are well-formed

    @Test("spacing scale is exactly 2·4·8·12·16·20·24 and strictly increasing")
    func spacingScaleIsMonotonic() {
        let scale: [CGFloat] = [
            DS.Space.xs2, DS.Space.xs4, DS.Space.sm8,
            DS.Space.md12, DS.Space.lg16, DS.Space.xl20, DS.Space.xxl24,
        ]
        #expect(scale == [2, 4, 8, 12, 16, 20, 24])
        #expect(isStrictlyIncreasing(scale))
    }

    @Test("radius scale is strictly increasing sm8 < md12 < lg16 < pill")
    func radiusScaleIsMonotonic() {
        let scale: [CGFloat] = [DS.Radius.sm8, DS.Radius.md12, DS.Radius.lg16, DS.Radius.pill]
        #expect(scale == [8, 12, 16, 999])
        #expect(isStrictlyIncreasing(scale))
    }

    @Test("opacity dimming tokens are within (0, 1)")
    func opacityTokensAreFractions() {
        for value in [DS.Opacity.stale, DS.Opacity.exited, DS.Opacity.pressed] {
            #expect(value > 0 && value < 1)
        }
    }

    @Test("minimum hit target meets the 44pt HIG floor")
    func hitTargetMeetsHIG() {
        #expect(DS.Layout.minHitTarget >= 44)
    }

    // MARK: - Motion honors Reduce Motion

    @Test("motion collapses to nil under Reduce Motion, animates otherwise")
    func motionGating() {
        #expect(DS.Motion.gated(DS.Motion.base, reduceMotion: true) == nil)
        #expect(DS.Motion.gated(DS.Motion.base, reduceMotion: false) != nil)
        #expect(DS.Motion.fastDuration < DS.Motion.baseDuration)
    }

    // MARK: - Adaptive accent resolves for both schemes

    @Test("accent is defined and distinct in light vs dark")
    func accentIsAdaptive() {
        let accent = DS.Palette.accentUIColor()
        let dark = accent.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
        let light = accent.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light))

        let darkRGBA = rgba(dark)
        let lightRGBA = rgba(light)
        // Both fully opaque, and visibly different between schemes.
        #expect(darkRGBA.a == 1)
        #expect(lightRGBA.a == 1)
        #expect(!approxEqual(darkRGBA, lightRGBA))
        // Desktop amber gold: dark ≈ #E3A64A (--accent), light ≈ #C9892F (--accent-2).
        assertRGBA(darkRGBA, r: 0xE3, g: 0xA6, b: 0x4A)
        assertRGBA(lightRGBA, r: 0xC9, g: 0x89, b: 0x2F)
    }

    // MARK: - Helpers

    private typealias RGBA = (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)

    private func isStrictlyIncreasing(_ values: [CGFloat]) -> Bool {
        zip(values, values.dropFirst()).allSatisfy { $0 < $1 }
    }

    private func rgba(_ color: Color) -> RGBA {
        rgba(UIColor(color))
    }

    private func rgba(_ color: UIColor) -> RGBA {
        var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
        color.getRed(&r, green: &g, blue: &b, alpha: &a)
        return (r, g, b, a)
    }

    private func approxEqual(_ lhs: RGBA, _ rhs: RGBA, tolerance: CGFloat = 0.02) -> Bool {
        abs(lhs.r - rhs.r) < tolerance
            && abs(lhs.g - rhs.g) < tolerance
            && abs(lhs.b - rhs.b) < tolerance
    }

    private func assertColor(_ color: Color, r: Int, g: Int, b: Int) {
        assertRGBA(rgba(color), r: r, g: g, b: b)
    }

    private func assertRGBA(_ value: RGBA, r: Int, g: Int, b: Int, tolerance: CGFloat = 0.02) {
        #expect(abs(value.r - CGFloat(r) / 255) < tolerance)
        #expect(abs(value.g - CGFloat(g) / 255) < tolerance)
        #expect(abs(value.b - CGFloat(b) / 255) < tolerance)
    }
}
