mod common;

use accesskit::{Rect, Role};
use bone_jig::{RunStatus, TreeDump};
use common::{find_artifact, run_library_scenario};

const WINDOW_W: f64 = 1920.0;
const WINDOW_H: f64 = 1080.0;
const PX_TOLERANCE: f64 = 0.5;

fn surface_dump(scenario: &str) -> TreeDump {
    let dir = match tempfile::tempdir() {
        Ok(d) => d,
        Err(e) => panic!("tempdir: {e}"),
    };
    let result = run_library_scenario(scenario, dir.path());
    assert_eq!(result.status, RunStatus::Passed, "{scenario}: {result:?}");
    let tree_path = find_artifact(dir.path(), ".tree.json");
    match TreeDump::load(&tree_path) {
        Ok(dump) => dump,
        Err(e) => panic!("{e}"),
    }
}

fn the_one(dump: &TreeDump, role: Role, label: &str) -> Rect {
    let matches = dump.matching(role, label);
    match matches.as_slice() {
        [node] => node.bounds.unwrap_or_else(|| {
            panic!(
                "{role:?} {label:?} carries no bounds; tree:\n{}",
                dump.to_text()
            )
        }),
        other => panic!(
            "expected exactly one {role:?} {label:?}, found {}; tree:\n{}",
            other.len(),
            dump.to_text(),
        ),
    }
}

fn is_selected(dump: &TreeDump, role: Role, label: &str) -> bool {
    let matches = dump.matching(role, label);
    match matches.as_slice() {
        [node] => node.states.selected == Some(true),
        other => panic!(
            "expected exactly one {role:?} {label:?}, found {}; tree:\n{}",
            other.len(),
            dump.to_text(),
        ),
    }
}

#[track_caller]
fn assert_px(actual: f64, expected: f64, what: &str) {
    assert!(
        (actual - expected).abs() <= PX_TOLERANCE,
        "{what}: {actual} expected {expected}",
    );
}

fn assert_window_chrome(dump: &TreeDump) {
    let menu = the_one(dump, Role::MenuBar, "Menu Bar");
    assert_px(menu.x0, 0.0, "menu bar left edge");
    assert_px(menu.y0, 0.0, "menu bar top edge");
    assert_px(menu.x1, WINDOW_W, "menu bar right edge");
    assert_px(menu.height(), 24.0, "menu bar height");
    let status = the_one(dump, Role::Status, "Status Bar");
    assert_px(status.x0, 0.0, "status bar left edge");
    assert_px(status.x1, WINDOW_W, "status bar right edge");
    assert_px(status.y1, WINDOW_H, "status bar bottom edge");
    assert_px(status.height(), 22.0, "status bar height");
}

#[test]
fn shell_chrome_bands_and_tree_metrics() {
    let dump = surface_dump("parity_shell.ron");
    assert_window_chrome(&dump);
    assert!(is_selected(&dump, Role::Tab, "Features"));
    let ribbon = the_one(&dump, Role::TabPanel, "Ribbon");
    assert_px(ribbon.y0, 24.0, "ribbon top edge");
    assert_px(ribbon.height(), 82.0, "ribbon band height");
    assert_px(ribbon.x1, WINDOW_W, "ribbon right edge");
    let tree = the_one(&dump, Role::Tree, "Feature Tree");
    assert_px(tree.x1, 222.0, "feature tree right edge");
    let front = the_one(&dump, Role::TreeItem, "Front Plane");
    let right = the_one(&dump, Role::TreeItem, "Right Plane");
    let top = the_one(&dump, Role::TreeItem, "Top Plane");
    assert_px(right.y0 - front.y0, 20.0, "feature tree row pitch");
    assert_px(top.y0 - right.y0, 20.0, "feature tree row pitch");
    let strip = the_one(&dump, Role::TabList, "Left Pane");
    assert_px(strip.height(), 28.0, "left pane tab strip height");
    let tab = the_one(&dump, Role::Tab, "Feature Tree");
    assert_px(tab.width(), 28.0, "left pane tab width");
    let heads_up = the_one(&dump, Role::Toolbar, "View Heads-Up Toolbar");
    assert_px(heads_up.y0, 114.0, "heads-up toolbar top edge");
    assert_px(heads_up.height(), 30.0, "heads-up toolbar height");
    assert!(
        heads_up.x0 > tree.x1,
        "heads-up toolbar floats over the viewport, not the left pane",
    );
    let status = the_one(&dump, Role::Status, "Status Bar");
    let doc_tabs = the_one(&dump, Role::TabList, "Document Tabs");
    assert_px(doc_tabs.x0, 0.0, "document tabs left edge");
    assert_px(
        doc_tabs.y1,
        status.y0,
        "document tabs sit directly above status bar",
    );
    assert_px(doc_tabs.height(), 22.0, "document tabs strip height");
    let model = the_one(&dump, Role::Tab, "Model");
    assert_px(model.x0, 0.0, "model tab is left-aligned");
}

#[test]
fn property_manager_pane_replaces_the_tree() {
    let dump = surface_dump("parity_property_manager.ron");
    assert_window_chrome(&dump);
    assert!(is_selected(&dump, Role::Tab, "Property Manager"));
    let pane = the_one(&dump, Role::Form, "Property Manager");
    assert_px(pane.x1, 222.0, "property pane right edge");
    assert!(
        dump.matching(Role::Tree, "Feature Tree").is_empty(),
        "tree must yield to the property pane; tree:\n{}",
        dump.to_text(),
    );
    let header = the_one(&dump, Role::Pane, "Extrude");
    assert_px(header.y0, 142.0, "property header top edge");
    assert_px(header.height(), 44.0, "property header height");
    let direction = the_one(&dump, Role::Pane, "Direction 1");
    assert_px(direction.x1, 222.0, "direction group right edge");
    assert!(
        direction.y0 > header.y1,
        "direction group sits below header"
    );
    let scope = the_one(&dump, Role::Pane, "Feature Scope");
    assert!(
        scope.y0 > direction.y1,
        "feature scope sits below direction group",
    );
    let accept = the_one(&dump, Role::Button, "Accept");
    let cancel = the_one(&dump, Role::Button, "Cancel");
    assert_px(accept.width(), 36.0, "accept button width");
    assert_px(cancel.width(), 36.0, "cancel button width");
}

#[test]
fn revolve_pane_opens_with_direction_and_scope() {
    let dump = surface_dump("parity_revolve.ron");
    assert_window_chrome(&dump);
    let header = the_one(&dump, Role::Pane, "Revolve");
    assert_px(header.height(), 44.0, "property header height");
    let direction = the_one(&dump, Role::Pane, "Direction 1");
    assert!(
        direction.y0 > header.y1,
        "direction group sits below the revolve header",
    );
    let scope = the_one(&dump, Role::Pane, "Feature Scope");
    assert!(
        scope.y0 > direction.y1,
        "feature scope sits below the direction group",
    );
    assert!(
        dump.matching(Role::Tree, "Feature Tree").is_empty(),
        "tree must yield to the revolve property pane; tree:\n{}",
        dump.to_text(),
    );
}

#[test]
fn sweep_pane_opens_with_profile_path_and_scope() {
    let dump = surface_dump("parity_sweep.ron");
    assert_window_chrome(&dump);
    let header = the_one(&dump, Role::Pane, "Sweep");
    assert_px(header.height(), 44.0, "property header height");
    let profile_path = the_one(&dump, Role::Pane, "Profile and Path");
    assert!(
        profile_path.y0 > header.y1,
        "profile and path group sits below the sweep header",
    );
    let options = the_one(&dump, Role::Pane, "Options");
    assert!(
        options.y0 > profile_path.y1,
        "options group sits below the profile and path group",
    );
    let guides = the_one(&dump, Role::Pane, "Guide Curves");
    assert!(
        guides.y0 > options.y1,
        "guide curves group sits below the options group",
    );
    let scope = the_one(&dump, Role::Pane, "Feature Scope");
    assert!(
        scope.y0 > guides.y1,
        "feature scope sits below the guide curves group",
    );
    assert!(
        dump.matching(Role::Tree, "Feature Tree").is_empty(),
        "tree must yield to the sweep property pane; tree:\n{}",
        dump.to_text(),
    );
}

#[test]
fn cut_extrude_pane_opens_with_direction_and_scope() {
    let dump = surface_dump("parity_cut_extrude.ron");
    assert_window_chrome(&dump);
    let header = the_one(&dump, Role::Pane, "Extrude");
    let direction = the_one(&dump, Role::Pane, "Direction 1");
    assert!(
        direction.y0 > header.y1,
        "direction group sits below the cut header",
    );
    let scope = the_one(&dump, Role::Pane, "Feature Scope");
    assert!(
        scope.y0 > direction.y1,
        "feature scope sits below the direction group",
    );
}

#[test]
fn sketch_mode_activates_the_sketch_ribbon() {
    let dump = surface_dump("parity_sketch_mode.ron");
    assert_window_chrome(&dump);
    assert!(is_selected(&dump, Role::Tab, "Sketch"));
    assert!(is_selected(&dump, Role::TreeItem, "Sketch1"));
    let entities = the_one(&dump, Role::Toolbar, "Entities");
    let relations = the_one(&dump, Role::Toolbar, "Relations");
    let dimensions = the_one(&dump, Role::Toolbar, "Dimensions");
    assert_px(entities.y0, 32.0, "entities toolbar top edge");
    assert!(relations.x0 > entities.x1, "relations sit after entities");
    assert!(
        dimensions.x0 > relations.x1,
        "dimensions sit after relations"
    );
    let _ = the_one(&dump, Role::Button, "Accept");
    let _ = the_one(&dump, Role::Button, "Cancel");
}

#[test]
fn sketch_entities_stay_in_sketch_mode() {
    let dump = surface_dump("parity_sketch_entities.ron");
    assert_window_chrome(&dump);
    assert!(is_selected(&dump, Role::Tab, "Sketch"));
    let _ = the_one(&dump, Role::Toolbar, "Relations");
}

#[test]
fn options_dialog_is_centered() {
    let dump = surface_dump("parity_options_dialog.ron");
    assert_window_chrome(&dump);
    let dialog = the_one(&dump, Role::Dialog, "Selection options");
    assert_px(
        dialog.x0,
        WINDOW_W - dialog.x1,
        "dialog horizontal centering",
    );
    assert_px(dialog.y0, WINDOW_H - dialog.y1, "dialog vertical centering");
}
