//! v3.7 c2 — dogfood usecase: search person + spatial/index modifier.
//!
//! Port reference: `insight/tools/maestro/flows/main/search-person.yaml`.
//! Exercises the People-tab search flow, a back-nav round-trip, and a
//! spatial+index selector probe: tap the first recent-people row that
//! sits below the "Today|Yesterday" section header.
//!
//! # Why this dogfood
//!
//! cap-gap discovery: this is the first dogfood that exercises spatial
//! (`below`) + index (`nth(0)`) modifiers end-to-end on a real screen.
//! Every prior dogfood (lang switch, device detail, Merlin chat,
//! search-vehicle, search-modal) stayed on bare `text` / `id` selectors.
//! If the resolver's spatial+index pipeline misfires on real-sim data
//! (e.g. section header bounds wrong, sorting non-deterministic, list
//! virtualisation hides expected rows), the failure surfaces here. The
//! tap is `optional` semantically — if the recent-people list is empty
//! we accept the empty-state outcome instead.
//!
//! Also exercises:
//!
//! - back navigation via `App.go_back()` (maps to maestro `pressKey: back`)
//! - id-based fill from c1 dispatch (driver.fill with id input-search)
//! - regex `wait_for` for both result and empty-state branches
//!
//! # Reversibility
//!
//! Pure read — search queries are transient. Optional tap-into-detail
//! returns to a leaf screen we don't mutate; the example doesn't drive
//! anything destructive.

use smix_sdk::{App, ExpectationFailure, Modifiers, Pattern, Selector, id, text, text_regex};
use std::time::Duration;

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port: u16 = std::env::var("SMIX_RUNNER_PORT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(22087);
    let udid = std::env::var("SMIX_UDID").ok();

    println!("smix dogfood — insight search person (v3.7 c2)");
    println!("===============================================");
    println!("runner port: {port}");
    println!("udid:        {:?}", udid);
    println!();

    let mut app = App::connect_to_runner(port).await?;
    if let Some(u) = udid {
        app = app.with_udid(u);
    }

    let bundle_id =
        std::env::var("INSIGHT_BUNDLE_ID").unwrap_or_else(|_| "com.focusai.app.mobile".to_string());
    println!("foregrounding {bundle_id}");
    app.foreground(&bundle_id).await?;
    tokio::time::sleep(Duration::from_secs(2)).await;

    println!("ensure_login: starting");
    ensure_login(&app).await?;
    println!("ensure_login: done\n");

    println!("reset_to_home");
    reset_to_home(&app).await?;
    println!("reset_to_home: done\n");

    println!("close_dev_menu_if_open");
    close_dev_menu_if_open(&app).await?;
    println!("close_dev_menu_if_open: done\n");

    match search_person_flow(&app).await {
        Ok(()) => {
            println!("\n✓ search person flow passed (1-round dogfood)");
            println!("\n🎉 v3.7 c2 dogfood — search person ✅");
            Ok(())
        }
        Err(e) => {
            println!("\n✗ search person flow failed");
            for line in e.to_prompt().lines() {
                println!("    {line}");
            }
            Err("dogfood search person FAIL".into())
        }
    }
}

// ---- inline helpers (each example self-contained) -----------------------

async fn ensure_login(app: &App) -> Result<(), ExpectationFailure> {
    // Check both the login screen text AND the dev-bubble id. The login
    // screen flashes briefly on RN bundle re-init even when AsyncStorage
    // has cached auth — see "Log in to Insight" being found by /find for
    // a single tick while the home screen is still mounting. Insist on
    // the dev-bubble before walking the login flow; if it's gone, the
    // session is already authenticated.
    let login_text_visible = app.find(&text("Log in to Insight")).await?;
    let dev_bubble_visible = app.find(&id("dev-bubble")).await?;
    if !(login_text_visible && dev_bubble_visible) {
        println!("  already logged in (login screen not stable)");
        return Ok(());
    }
    println!("  tapping dev-bubble");
    app.tap(&id("dev-bubble")).await?;
    tokio::time::sleep(Duration::from_millis(1500)).await;
    println!("  tapping 'Login'");
    app.tap(&text("Login")).await?;
    println!("  waiting for home tab");
    app.wait_for(
        &text_regex("Open menu|Device|Search|Merlin|Alerts|Tracking"),
        Duration::from_secs(30),
    )
    .await?;
    Ok(())
}

async fn reset_to_home(app: &App) -> Result<(), ExpectationFailure> {
    for _ in 0..3 {
        let drawer_open = app
            .wait_for(&text("Dashboard"), Duration::from_millis(200))
            .await
            .is_ok();
        if !drawer_open {
            break;
        }
        let _ = app.go_back().await;
        tokio::time::sleep(Duration::from_millis(400)).await;
    }
    if app.find(&text("Device")).await? {
        let _ = app.tap(&text("Device")).await;
        tokio::time::sleep(Duration::from_millis(800)).await;
    }
    // Home-screen ready indicator. v3.5+ baseline used `text("Open menu")`
    // (the drawer hamburger's accessibilityLabel). Insight RN bundle has since
    // dropped that label on some screens (e.g. Merlin tab's header lacks it),
    // so fall back to any of the bottom-tab labels — at least one is always
    // visible on a home tab.
    app.wait_for(
        &text_regex("Open menu|Device|Search|Merlin|Alerts|Tracking"),
        Duration::from_secs(5),
    )
    .await?;
    Ok(())
}

async fn close_dev_menu_if_open(app: &App) -> Result<(), ExpectationFailure> {
    let open = app
        .wait_for(&text("Reboot"), Duration::from_millis(300))
        .await
        .is_ok();
    if open {
        println!("  dev menu visible, tapping dev-bubble");
        app.tap(&id("dev-bubble")).await?;
        tokio::time::sleep(Duration::from_millis(800)).await;
    } else {
        println!("  dev menu already dismissed");
    }
    Ok(())
}

async fn go_to_search(app: &App) -> Result<(), ExpectationFailure> {
    for _ in 0..5 {
        if app
            .wait_for(&text_regex("People|Vehicle"), Duration::from_millis(500))
            .await
            .is_ok()
        {
            return Ok(());
        }
        let _ = app.tap(&text("Search")).await;
        tokio::time::sleep(Duration::from_secs(2)).await;
    }
    app.wait_for(&text_regex("People|Vehicle"), Duration::from_secs(5))
        .await?;
    Ok(())
}

// ---- search-person flow (port of flows/main/search-person.yaml) ---------

async fn search_person_flow(app: &App) -> Result<(), ExpectationFailure> {
    println!("step 1: tap Search bottom tab → People/Vehicle visible");
    go_to_search(app).await?;

    println!("step 2: ensure People sub-tab (default selection)");
    app.wait_for(&text("People"), Duration::from_secs(30))
        .await?;

    println!("step 3: open search modal (id btn-search-open)");
    app.tap(&id("btn-search-open")).await?;

    println!("step 4: wait Search modal header (≤5s)");
    app.wait_for(&text("Search"), Duration::from_secs(5))
        .await?;

    println!("step 5: tap input field (id input-search)");
    app.tap(&id("input-search")).await?;
    tokio::time::sleep(Duration::from_millis(500)).await;

    println!("step 6: fill \"john\" (id-selector → c1 host-resolve + focus dispatch)");
    app.fill(&id("input-search"), "john").await?;
    tokio::time::sleep(Duration::from_millis(400)).await;

    println!("step 7: tap submit (id btn-search-submit)");
    app.tap(&id("btn-search-submit")).await?;
    tokio::time::sleep(Duration::from_millis(600)).await;

    println!("step 8: wait for result or empty-state (regex No results|person|People, ≤30s)");
    app.wait_for(
        &text_regex("No results|person|People"),
        Duration::from_secs(30),
    )
    .await?;

    println!("step 9: go back to People recent list (app.go_back ≃ maestro pressKey:back)");
    app.go_back().await?;
    tokio::time::sleep(Duration::from_millis(600)).await;

    println!(
        "step 10: wait recent list or empty state (regex No people found|Today|Yesterday, ≤30s)"
    );
    app.wait_for(
        &text_regex("No people found|Today|Yesterday"),
        Duration::from_secs(30),
    )
    .await?;

    println!("step 11: probe first recent row below Today|Yesterday header (optional)");
    // Selector: any text candidate that sits geometrically below the
    // "Today|Yesterday" section header, picked at index 0 (= the
    // topmost row in DFS pre-order after the spatial filter).
    let row_below_header = Selector::Text {
        text: Pattern::regex(".+"), // any non-empty text
        modifiers: Modifiers {
            below: Some(Box::new(Selector::Text {
                text: Pattern::regex("Today|Yesterday"),
                modifiers: Modifiers::default(),
            })),
            nth: Some(0),
            ..Modifiers::default()
        },
    };
    // optional in maestro semantics — if list is empty, the resolver
    // never finds a candidate. Treat as a soft probe: log + continue.
    match app.tap(&row_below_header).await {
        Ok(()) => {
            println!("  tapped first recent row (list has data)");
            tokio::time::sleep(Duration::from_millis(800)).await;
        }
        Err(_) => {
            println!("  no row to tap (empty state or no candidate below header)");
        }
    }

    println!("step 12: verify playback/detail opened or empty state held");
    // The maestro yaml asserts literal "Playback|No people found", but on
    // the live insight build the playback view's a11y tree never carries
    // a "Playback" string — it shows the device camera id (e.g.
    // "VC642XJECHVC") + a timestamp + chrome like "Reset zoom" /
    // "Video quality". Widen the regex to accept either the empty-state
    // copy or any of the playback-chrome strings — that's what "the
    // detail view opened" actually looks like on this app.
    app.wait_for(
        &text_regex("Playback|No people found|Reset zoom|Video quality"),
        Duration::from_secs(30),
    )
    .await?;

    Ok(())
}
