facett-core 0.1.10

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! Theme-level tests: the WCAG contrast gate (all presets, light+dark), serde
//! round-trip (theme + remapped key), and `apply` taking effect + publishing the
//! legacy palette (the COH-1 bridge). The 3-preset effects-off diff (§25) lands
//! fully in M3; M1 asserts the gate + apply + serde here.

use super::*;

/// Every preset light+dark, for gate sweeps.
fn all_presets() -> Vec<Theme> {
    vec![
        Theme::windows_light(),
        Theme::windows_dark(),
        Theme::macos_light(),
        Theme::macos_dark(),
        Theme::device(),
        Theme::skade_vinter(),
        Theme::neutral(),
    ]
}

#[test]
fn wcag_gate_passes_for_every_preset_light_and_dark() {
    for t in all_presets() {
        let p = &t.palette;
        let body = p.body_text_contrast();
        assert!(body >= 4.5, "{}: body text contrast {body:.2} < 4.5", t.name);
        let dim = p.dim_text_contrast();
        assert!(dim >= 3.0, "{}: dim text contrast {dim:.2} < 3.0", t.name);
        let ui = p.ui_boundary_contrast();
        assert!(ui >= 3.0, "{}: UI boundary contrast {ui:.2} < 3.0", t.name);
        let status = p.status_contrast();
        assert!(status >= 4.5, "{}: status on-colour contrast {status:.2} < 4.5", t.name);
    }
}

#[test]
fn every_preset_fully_populates_and_is_distinct() {
    let names = Theme::preset_names();
    let mut sorted = names.clone();
    sorted.sort();
    sorted.dedup();
    assert_eq!(sorted.len(), names.len(), "preset names must be unique");
    // by_name round-trips (fuzzy too).
    for n in &names {
        assert_eq!(Theme::by_name(n).map(|t| t.name), Some(n.clone()));
    }
    assert_eq!(Theme::by_name("Windows Dark").map(|t| t.name), Some("windows-dark".into()));
}

#[test]
fn theme_serde_round_trips_with_a_remapped_key() {
    let mut t = Theme::macos_light();
    t.keymap.set(Action::Find, egui::KeyboardShortcut::new(egui::Modifiers::COMMAND | egui::Modifiers::SHIFT, egui::Key::F));
    let json = serde_json::to_string_pretty(&t).unwrap();
    let back: Theme = serde_json::from_str(&json).unwrap();
    assert_eq!(t, back, "theme must serde round-trip exactly");
    assert_eq!(
        back.keymap.shortcut(Action::Find),
        Some(egui::KeyboardShortcut::new(egui::Modifiers::COMMAND | egui::Modifiers::SHIFT, egui::Key::F))
    );
}

#[test]
fn apply_installs_style_and_publishes_legacy_palette() {
    let theme = Theme::windows_dark();
    let ctx = egui::Context::default();
    theme.apply(&ctx);

    // The egui style picked up our spacing + scroll.
    let style = ctx.style();
    assert_eq!(style.spacing.item_spacing, theme.metrics.item_spacing_vec());
    assert_eq!(style.spacing.scroll.bar_width, theme.scroll.bar_width);
    assert!(style.visuals.override_text_color.is_none(), "§27: no global override_text_color");

    // The legacy palette is published so existing components follow (COH-1).
    let mut got = "";
    let _ = ctx.run(egui::RawInput::default(), |ctx| {
        egui::CentralPanel::default().show(ctx, |ui| {
            got = crate::theme(ui).name;
        });
    });
    assert_eq!(got, "windows-dark");
}

#[test]
fn from_os_falls_back_to_windows_on_linux() {
    use egui::os::OperatingSystem as Os;
    assert_eq!(Theme::from_os(Os::Mac).name, "macos-dark");
    assert_eq!(Theme::from_os(Os::Windows).name, "windows-dark");
    assert_eq!(Theme::from_os(Os::Nix).name, "windows-dark", "Linux → documented default");
}

#[test]
fn device_has_effects_off() {
    let d = Theme::device();
    assert_eq!(d.effects, EffectsPolicy::None);
    assert!(!d.effects.allows_transparency());
    assert_eq!(d.surface, SurfaceSpec::Opaque);
    assert_eq!(d.motion.duration, 0.0, "no decorative motion on Device");
}

/// T1.3 showcase: the full-effects presets must declare a **non-default** glass
/// surface (so the new chrome/bloom render has something to show + the demo can
/// assert it as data), and that glass must still degrade to opaque under Device.
/// FAIL-ON-BUG: an `Opaque` (default) surface on a Full-effects preset trips this.
#[test]
fn full_effects_presets_carry_a_non_default_glass_surface() {
    for t in [Theme::windows_dark(), Theme::macos_dark(), Theme::skade_vinter()] {
        assert_eq!(t.effects, EffectsPolicy::Full, "{}: showcase preset is full-effects", t.name);
        assert_ne!(t.surface, SurfaceSpec::Opaque, "{}: showcase declares a non-default glass surface", t.name);
        // It really is glass (paints a tint) and stays glass under Full.
        assert!(t.surface.tint_color().is_some(), "{}: glass surface paints a tint", t.name);
        assert_ne!(t.surface.resolve(EffectsPolicy::Full), SurfaceSpec::Opaque, "{}: glass under Full", t.name);
        // But it degrades to a crisp opaque card under Device (effects off).
        assert_eq!(
            t.surface.resolve(EffectsPolicy::None),
            SurfaceSpec::Opaque,
            "{}: glass degrades to opaque under Device",
            t.name
        );
    }
    // The light variants inherit the same glass from their dark base.
    assert_ne!(Theme::windows_light().surface, SurfaceSpec::Opaque, "windows-light inherits glass");
    assert_ne!(Theme::macos_light().surface, SurfaceSpec::Opaque, "macos-light inherits glass");
}

#[test]
fn win_mac_parity_same_semantic_surface_different_chrome() {
    // M2 parity: Windows and macOS presets describe the *same semantic surface*
    // (both pass the gate, both bind every action, both fully populate) but differ
    // in the chrome the work order calls for (scroll style, line-nav chord, radii).
    let w = Theme::windows_dark();
    let m = Theme::macos_dark();

    // Same semantic completeness.
    assert!(w.palette.body_text_contrast() >= 4.5 && m.palette.body_text_contrast() >= 4.5);
    for a in Action::ALL {
        assert!(w.keymap.shortcut(*a).is_some() && m.keymap.shortcut(*a).is_some());
    }

    // Different chrome (the deliberate per-OS divergence).
    assert!(!w.scroll.floating && m.scroll.floating, "Windows solid vs macOS floating scrollbars");
    assert_ne!(w.keymap.shortcut(Action::LineStart), m.keymap.shortcut(Action::LineStart), "line nav differs");
    assert!(m.metrics.corner_radius >= w.metrics.corner_radius, "macOS radii softer");
    // Copy is the SAME chord on both (COMMAND auto-maps ⌘/Ctrl).
    assert_eq!(w.keymap.shortcut(Action::Copy), m.keymap.shortcut(Action::Copy));
}

/// The **platform-adaptive native-feel layer** — the Mac vs Windows presets must
/// differ as DATA across every cue the work order calls out (corner radius,
/// surface material, motion duration AND curve, accent tint, font stack, and the
/// `NativeFeel` cues). This delta *is* the adaptive layer; if a refactor collapses
/// the two platforms onto one feel, this fails.
#[test]
fn mac_and_windows_native_feel_differ_as_data() {
    let w = Theme::windows_dark();
    let m = Theme::macos_dark();

    // Geometry / density.
    assert!(m.metrics.corner_radius > w.metrics.corner_radius, "macOS radii softer");
    assert!(m.metrics.window_corner_radius > w.metrics.window_corner_radius, "macOS window radius softer");

    // Material: both Frosted, but vibrancy (mac) is more translucent than Mica (win).
    let tint_alpha = |t: &Theme| t.surface.tint_color().map(|c| c.a()).unwrap_or(255);
    let (mac_a, win_a) = (tint_alpha(&m), tint_alpha(&w));
    assert!(mac_a < win_a, "vibrancy (mac α {mac_a}) is more translucent than Mica (win α {win_a})");

    // Motion personality: macOS is gentler + longer with an OVERSHOOT curve;
    // Windows is snappy with a plain accelerate-decelerate curve.
    assert!(m.motion.duration > w.motion.duration, "macOS motion is longer/gentler");
    assert_ne!(m.motion.curve, w.motion.curve, "motion curve differs per platform");
    assert_eq!(m.motion.curve, crate::effects::Curve::EaseOutBack, "macOS = spring overshoot");
    assert_eq!(w.motion.curve, crate::effects::Curve::EaseInOutCubic, "Windows = connected ease");

    // Typography: platform system-font intent differs.
    assert_ne!(m.typography.ui_font, w.typography.ui_font, "SF vs Segoe UI");
    assert_eq!(m.typography.ui_font, UiFont::SanFrancisco);
    assert_eq!(w.typography.ui_font, UiFont::SegoeUi);

    // The NativeFeel cue deltas (reveal vs rubber-band, controls side, focus ring,
    // accent tint, elevation).
    assert!(w.native.reveal_highlight && !m.native.reveal_highlight, "reveal is Windows-only");
    assert!(m.native.rubber_band && !w.native.rubber_band, "rubber-band is macOS-only");
    assert!(m.native.window_controls.on_left() && !w.native.window_controls.on_left());
    assert!(m.native.focus_ring.glow && !w.native.focus_ring.glow, "mac ring glows; win rect is crisp");
    assert!(m.native.accent_tint > w.native.accent_tint, "macOS leans on accent");
    assert!(w.native.elevation > m.native.elevation, "Windows elevation shadow heavier");
}

/// Under the **macOS** preset the showcase surface is *actually frosted* (real
/// backdrop blur under Full effects, degrading correctly), and under the
/// **Windows** preset the reveal highlight activates on hover (its cue flag is set
/// and an accent reveal lights up when the pointer is on a control).
#[test]
fn mac_frosted_and_windows_reveal_are_live_under_their_presets() {
    use crate::effects::RevealHighlight;

    // macOS: a Frosted surface that stays frosted under Full (real blur).
    let m = Theme::macos_dark();
    assert!(matches!(m.surface, SurfaceSpec::Frosted { .. }), "macOS preset is frosted glass");
    assert_eq!(m.surface.resolve(EffectsPolicy::Full), m.surface, "frosted under Full = real blur");
    // …and macOS does NOT use the Windows reveal cue.
    assert!(!m.native.reveal_highlight);

    // Windows: the reveal cue is on, and a reveal glow built from the accent lights
    // up when the pointer is on the control (and is dark when far / absent).
    let w = Theme::windows_dark();
    assert!(w.native.reveal_highlight, "Windows preset arms the reveal highlight");
    let reveal = RevealHighlight::new(w.palette.accent.to_color32());
    let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(120.0, 32.0));
    assert!(reveal.proximity(rect, Some(egui::pos2(60.0, 16.0))) > 0.0, "reveal lights up on hover");
    assert_eq!(reveal.proximity(rect, None), 0.0, "reveal dark with no pointer");
}

/// `Theme::for_platform` selects the matching native-feel preset, and the demo's
/// runtime toggle order (Mac ⇄ Windows ⇄ Neutral) maps onto three distinct presets.
#[test]
fn for_platform_selects_matching_preset() {
    use crate::look::Platform;
    assert_eq!(Theme::for_platform(Platform::Mac).name, "macos-dark");
    assert_eq!(Theme::for_platform(Platform::Windows).name, "windows-dark");
    assert_eq!(Theme::for_platform(Platform::Neutral).name, "neutral-dark");
    // Each carries its platform's NativeFeel.
    assert_eq!(Theme::for_platform(Platform::Mac).native.platform, Platform::Mac);
    assert_eq!(Theme::for_platform(Platform::Windows).native.platform, Platform::Windows);
    assert_eq!(Theme::for_platform(Platform::Neutral).native.platform, Platform::Neutral);
    // The neutral preset is premium (full effects) but OS-agnostic (no chrome cues).
    let n = Theme::neutral();
    assert_eq!(n.effects, EffectsPolicy::Full, "neutral is premium full-effects");
    assert!(!n.native.reveal_highlight && !n.native.rubber_band, "no OS-specific chrome");
    assert!(matches!(n.native.window_controls, WindowControls::None));
}

#[test]
fn legacy_palette_bridge_carries_semantic_roles() {
    let t = Theme::windows_dark();
    let legacy = t.to_legacy_palette();
    assert_eq!(legacy.bg, t.palette.surface.to_color32());
    assert_eq!(legacy.accent, t.palette.accent.to_color32());
    assert_eq!(legacy.text, t.palette.on_surface.to_color32());
    assert_eq!(legacy.glow, t.palette.glow.to_color32());
}