//! v3.11 c1 — dogfood usecase: search tabs (nested tab matrix path).
//!
//! Port reference: `insight/tools/maestro/flows/main/search-tabs.yaml`.
//! Drives the Search → People/Vehicle/Animal main-tab matrix with
//! nested sub-tabs (Profiles/Tracking under People, License under
//! Vehicle, none under Animal).
//!
//! # Why this dogfood
//!
//! First nested tab matrix dogfood. v3.5-v3.10 累计 13 dogfood 触过
//! single-tab nav (search-vehicle Vehicle sub-tab / alerts-counting
//! Counting sub-tab / alerts-events Events sub-tab) but never a
//! cross-level matrix (main tab + per-main-tab sub-tab variations).
//! This validates the resolver against the same text-selector tap
//! being repeatedly issued across distinct tab containers.
//!
//! G2 (v4.4 c1) — `Selector::Role { Role::Tab }` now works for the bottom
//! UITabBar: `derive_roles_recursive` structurally infers Tab for buttons
//! inside a `tabBar` subtree (iOS has no distinct tab ElementType, so
//! rawType alone can't yield it). `go_to_search` taps the bottom tab via
//! `role_named(Role::Tab, "Search")` as the real-sim validation. The
//! horizontal sub-tabs (rawType "other" — RN custom nav, NOT a UITabBar)
//! carry no a11y tab semantic, so they keep the text + spatial-modifier
//! workaround (see step 5).
//!
//! # Reversibility
//!
//! Pure read — tab switching is local view state, no mutation.
//! Bottom-tab Search return is a single tap.

use smix_sdk::{
    App, ExpectationFailure, Modifiers, Pattern, Role, Selector, id, role_named, 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 tabs (v3.11 c1)");
    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());
    // B7 (v4.4 c2): cold-relaunch round-reset (terminate + openurl) instead of
    // foreground. The bottom Device tab runs live-video off a grpc backend that
    // is unavailable on the sim; foreground-reusing a prior round's process lets
    // the failed-video state accumulate into a full-screen "Device unavailable"
    // error (no tab bar), which broke rounds 2/3 (round 1 from a fresh process
    // passed because the video was still loading — tab bar visible — long enough
    // to navigate away). A fresh terminate + relaunch gives every round that same
    // clean window. Same root fix as G5's v3.18 §5.1 cold-relaunch workaround —
    // B7 and G5 are the same round-state-pollution class, closed together here.
    println!("cold-relaunch (terminate + openurl) {bundle_id}");
    let _ = app.terminate(&bundle_id).await;
    tokio::time::sleep(Duration::from_millis(800)).await;
    app.open_url("exp+focus-ai-app://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8081")
        .await?;
    app.wait_for(
        &text_regex("Log in to Insight|Open menu|Device|Login"),
        Duration::from_secs(30),
    )
    .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_tabs_flow(&app).await {
        Ok(()) => {
            println!("\n✓ search tabs flow passed (1-round dogfood)");
            println!("\n🎉 v3.11 c1 dogfood — search tabs ✅");
            Ok(())
        }
        Err(e) => {
            println!("\n✗ search tabs flow failed");
            for line in e.to_prompt().lines() {
                println!("    {line}");
            }
            Err("dogfood search tabs 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(());
        }
        // G2 (v4.4 c1): bottom UITabBar tab via Role::Tab structural inference
        // (button inside tabBar subtree → Role::Tab), replacing the old
        // text("Search") workaround.
        let _ = app.tap(&role_named(Role::Tab, "Search")).await;
        tokio::time::sleep(Duration::from_secs(2)).await;
    }
    app.wait_for(&text_regex("People|Vehicle"), Duration::from_secs(5))
        .await?;
    Ok(())
}

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

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

    println!("step 2: verify People/Vehicle/Animal main tabs visible");
    app.wait_for(&text("People"), Duration::from_secs(5))
        .await?;
    app.wait_for(&text("Vehicle"), Duration::from_secs(5))
        .await?;
    app.wait_for(&text("Animal"), Duration::from_secs(5))
        .await?;

    println!("step 3: verify People sub-tabs (Recent/Profiles/Tracking) visible");
    app.wait_for(&text("Recent"), Duration::from_secs(5))
        .await?;
    app.wait_for(&text("Profiles"), Duration::from_secs(5))
        .await?;
    app.wait_for(&text("Tracking"), Duration::from_secs(5))
        .await?;

    println!("step 4: tap Profiles sub-tab");
    app.tap(&text("Profiles")).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;

    println!(
        "step 5: tap sub-tab \"Tracking\" via Modifiers::above disambiguation (v3.7 c2 spatial cap)"
    );
    // The yaml says `tapOn: "Tracking"` to switch to the People > Tracking
    // sub-tab. "Tracking" is also the bottom-tab name on UITabBar → G7
    // ambiguous label. Two candidates share label="Tracking":
    //   - sub-tab Tracking: rawType="other", y≈140 (horizontal sub-tab nav
    //                       under the People main-tab; RN generic View
    //                       container, NOT a UIScrollView — so the
    //                       v3.14 c1 ancestor=ScrollView pattern that
    //                       alerts_tabs uses does NOT apply here).
    //   - bottom-tab Tracking: rawType="button", y≈795 (UITabBar item).
    //
    // Anchor on `Device` (the first bottom-tab, unique label in tree).
    // `above(Device)` requires candidate center.y < Device center.y:
    //   - sub-tab Tracking center y≈157 < Device center y≈822 → match
    //   - bottom-tab Tracking center y≈822 == Device center y≈822 → reject
    //
    // v3.7 c2 spatial+index baseline cap (host-side resolver pipeline
    // `apply_spatial_filters` segment). Same disambiguation goal as the
    // alerts_tabs step 6 ancestor pattern, different host-side filter
    // (spatial vs structural).
    let sub_tab_tracking = Selector::Text {
        text: Pattern::text("Tracking"),
        modifiers: Modifiers {
            above: Some(Box::new(text("Device"))),
            ..Default::default()
        },
    };
    app.tap(&sub_tab_tracking).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;

    println!("step 6: switch to Vehicle main tab");
    app.tap(&text("Vehicle")).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;

    println!("step 7: verify Vehicle sub-tabs (Recent/License) visible");
    app.wait_for(&text("Recent"), Duration::from_secs(5))
        .await?;
    app.wait_for(&text("License"), Duration::from_secs(5))
        .await?;

    println!("step 8: tap License sub-tab");
    app.tap(&text("License")).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;

    println!("step 9: switch to Animal main tab (no sub-tabs)");
    app.tap(&text("Animal")).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;

    println!("step 10: switch back to People — verify sub-tabs return");
    app.tap(&text("People")).await?;
    tokio::time::sleep(Duration::from_millis(700)).await;
    app.wait_for(&text("Profiles"), Duration::from_secs(5))
        .await?;

    Ok(())
}
