use super::*;
use crate::launcher::{
    devices::{CameraMode, DeviceCatalog},
    preview::PreviewBinding,
    state::{BreakoutSizePreset, LauncherState, PreviewSourceSize},
    ui_components::build_launcher_view,
};
use gtk::prelude::*;
use serial_test::serial;
use std::{
    cell::RefCell,
    collections::BTreeMap,
    rc::Rc,
    time::{Duration, Instant},
};

fn present_and_settle(window: &gtk::ApplicationWindow) {
    window.present();
    let deadline = Instant::now() + Duration::from_millis(450);
    while Instant::now() < deadline {
        while glib::MainContext::default().iteration(false) {}
        std::thread::sleep(Duration::from_millis(15));
    }
}

#[test]
fn local_test_detail_mentions_idle_and_running_modes() {
    assert!(local_test_detail(false, false, false, false).contains("idle"));
    let running = local_test_detail(true, true, false, false);
    assert!(running.contains("camera preview"));
    assert!(running.contains("mic monitor"));
}

#[test]
fn gpio_power_label_tracks_detected_devices() {
    let mut power = CapturePowerStatus::default();
    assert_eq!(gpio_power_label(&power), "Unavailable");

    power.available = true;
    assert_eq!(gpio_power_label(&power), "Power Off");

    power.enabled = true;
    assert_eq!(gpio_power_label(&power), "No Eyes");

    power.detected_devices = 1;
    assert_eq!(gpio_power_label(&power), "1 Eye");

    power.detected_devices = 2;
    assert_eq!(gpio_power_label(&power), "2 Eyes");
}

#[gtk::test]
#[serial]
fn launcher_shell_measures_inside_a_1080p_desktop_budget() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-layout-budget")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let state = LauncherState::new();
    let view = build_launcher_view(
        &app,
        "http://127.0.0.1:50051",
        &DeviceCatalog::default(),
        &state,
    );
    present_and_settle(&view.window);

    let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1);
    let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920);

    assert!(
        min_width <= 1560 && view.window.width() <= 1560,
        "launcher width budget regressed: min={min_width}, natural={natural_width}"
    );
    assert!(
        min_height <= 960 && view.window.height() <= 960,
        "launcher height budget regressed: min={min_height}, natural={natural_height}"
    );
}

#[gtk::test]
#[serial]
fn populated_launcher_shell_measures_inside_a_1080p_desktop_budget() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-layout-budget-populated")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let catalog = realistic_device_catalog();
    let mut state = LauncherState::new();
    state.apply_catalog_defaults(&catalog);

    let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state);
    present_and_settle(&view.window);

    let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1);
    let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920);

    assert!(
        min_width <= 1560 && view.window.width() <= 1560,
        "populated launcher width budget regressed: min={min_width}, natural={natural_width}"
    );
    assert!(
        min_height <= 960 && view.window.height() <= 960,
        "populated launcher height budget regressed: min={min_height}, natural={natural_height}"
    );
}

#[gtk::test]
#[serial]
fn populated_launcher_runtime_widgets_stay_compact() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-layout-widget-budget")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let catalog = realistic_device_catalog();
    let mut state = LauncherState::new();
    state.apply_catalog_defaults(&catalog);

    let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state);
    present_and_settle(&view.window);

    let (camera_min_w, camera_nat_w, _, _) = view
        .device_stage
        .camera_preview
        .measure(gtk::Orientation::Horizontal, -1);
    let (camera_min_h, camera_nat_h, _, _) = view
        .device_stage
        .camera_preview
        .measure(gtk::Orientation::Vertical, 420);
    let (testing_panel_min_h, testing_panel_nat_h, _, _) = view
        .device_stage
        .preview_panel
        .measure(gtk::Orientation::Vertical, 520);
    let (left_min_w, left_nat_w, _, _) = view.widgets.display_panes[0]
        .root
        .measure(gtk::Orientation::Horizontal, -1);
    let (left_min_h, left_nat_h, _, _) = view.widgets.display_panes[0]
        .root
        .measure(gtk::Orientation::Vertical, 720);
    let (server_min_w, server_nat_w, _, _) =
        view.server_entry.measure(gtk::Orientation::Horizontal, -1);

    assert!(
        camera_min_w >= 400 && camera_nat_w >= 400,
        "camera preview width regressed: min={camera_min_w}, natural={camera_nat_w}"
    );
    assert!(
        camera_min_h >= 225 && camera_nat_h >= 225,
        "camera preview height regressed: min={camera_min_h}, natural={camera_nat_h}"
    );
    assert!(
        testing_panel_min_h <= 420 && testing_panel_nat_h <= 420,
        "device testing panel height regressed: min={testing_panel_min_h}, natural={testing_panel_nat_h}"
    );
    assert!(
        left_min_w <= 700 && left_nat_w <= 720,
        "eye pane width regressed: min={left_min_w}, natural={left_nat_w}"
    );
    assert!(
        left_min_h <= 700 && left_nat_h <= 700,
        "eye pane height regressed: min={left_min_h}, natural={left_nat_h}"
    );
    assert!(
        server_min_w <= 168 && server_nat_w <= 180,
        "server entry width regressed: min={server_min_w}, natural={server_nat_w}"
    );
    assert!(
        (view.device_stage.devices_panel.height() - view.device_stage.preview_panel.height()).abs()
            <= 2,
        "device staging and upstream media heights diverged: staging={}, upstream={}",
        view.device_stage.devices_panel.height(),
        view.device_stage.preview_panel.height()
    );
    assert_eq!(
        view.device_stage
            .camera_preview_stack
            .visible_child_name()
            .as_deref(),
        Some("idle"),
        "idle launcher should show the placeholder webcam surface until preview wakes up"
    );
    view.device_stage
        .camera_preview_stack
        .set_visible_child_name("live");
    present_and_settle(&view.window);
    assert!(
        view.widgets.display_panes[0].preview_frame.height()
            >= view.device_stage.camera_preview_frame.height(),
        "eye preview should stay at least as tall as the webcam preview: eye={}, webcam={}",
        view.widgets.display_panes[0].preview_frame.height(),
        view.device_stage.camera_preview_frame.height()
    );
    assert!(
        view.widgets.display_panes[0].preview_frame.width()
            >= view.device_stage.camera_preview_frame.width(),
        "eye preview should stay at least as wide as the webcam preview: eye={}, webcam={}",
        view.widgets.display_panes[0].preview_frame.width(),
        view.device_stage.camera_preview_frame.width()
    );
    assert!(
        view.widgets.display_panes[0].root.width()
            - view.widgets.display_panes[0].preview_frame.width()
            <= 60,
        "eye preview stopped filling its card width: card={}, preview={}",
        view.widgets.display_panes[0].root.width(),
        view.widgets.display_panes[0].preview_frame.width()
    );
    assert!(
        view.device_stage.camera_preview_frame.height() >= 225,
        "webcam preview should use the taller upstream media space: {}",
        view.device_stage.camera_preview_frame.height()
    );
}

#[gtk::test]
#[serial]
fn breakout_size_changes_resize_the_open_popout_window() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-breakout-resize")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let window = gtk::ApplicationWindow::builder()
        .application(&app)
        .default_width(540)
        .default_height(304)
        .build();
    let root = gtk::Box::new(gtk::Orientation::Vertical, 0);
    let frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false);
    let picture = gtk::Picture::new();
    frame.set_child(Some(&picture));
    root.append(&frame);
    window.set_child(Some(&root));

    let handle = PopoutWindowHandle {
        window,
        root,
        frame: frame.clone(),
        picture: picture.clone(),
        status_label: gtk::Label::new(None),
        binding: PreviewBinding::test_stub(),
    };

    apply_popout_window_size(
        &handle,
        BreakoutSizeChoice {
            preset: BreakoutSizePreset::P720,
            width: 1280,
            height: 720,
        },
        PreviewSourceSize {
            width: 1920,
            height: 1080,
            fps: 60,
        },
    );

    assert_eq!(handle.root.width_request(), 1280);
    assert_eq!(handle.root.height_request(), 720);
    assert_eq!(handle.frame.width_request(), 1280);
    assert_eq!(handle.frame.height_request(), 720);
    assert_eq!(handle.picture.width_request(), 1280);
    assert_eq!(handle.picture.height_request(), 720);
}

#[test]
fn server_chip_state_tracks_connection_not_just_reachability() {
    let mut state = LauncherState::new();
    assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
    assert_eq!(server_version_label(&state), "-");

    state.set_server_available(true);
    state.set_server_version(Some("0.13.1".to_string()));
    assert_eq!(server_light_state(&state, false), StatusLightState::Live);
    assert_eq!(server_version_label(&state), "v0.13.1");

    assert_eq!(
        server_light_state(&state, true),
        StatusLightState::Connected
    );

    state.set_server_version(Some("v0.12.5".to_string()));
    assert_eq!(server_light_state(&state, false), StatusLightState::Warning);
    assert_eq!(server_light_state(&state, true), StatusLightState::Caution);
    assert_eq!(server_version_label(&state), "v0.12.5");

    state.set_server_version(Some("  ".to_string()));
    assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
    assert_eq!(server_version_label(&state), "-");
}

#[test]
fn capture_power_detail_mentions_detected_eyes_when_powered() {
    let power = CapturePowerStatus {
        available: true,
        enabled: true,
        detail: "active/running".to_string(),
        detected_devices: 1,
        ..Default::default()
    };

    assert!(capture_power_detail(&power).contains("1 eye detected"));
}

#[test]
fn compact_device_name_prefers_basename_when_available() {
    assert_eq!(compact_device_name("/dev/video0"), "video0");
    assert_eq!(compact_device_name("alsa_input.usb"), "alsa_input.usb");
}

#[test]
fn strip_ansi_sequences_removes_terminal_codes() {
    let raw = "\u{1b}[32mINFO\u{1b}[0m hello";
    assert_eq!(strip_ansi_sequences(raw), "INFO hello");
}

#[test]
fn classify_log_tags_assigns_prefix_and_severity_colors() {
    let tags = classify_log_tags("[relay] WARN pipeline failed");
    assert!(tags.contains(&"log-relay"));
    assert!(tags.contains(&"log-error") || tags.contains(&"log-warn"));
}

#[test]
#[doc = "Verifies the default console filter hides relay INFO noise."]
fn session_log_filter_hides_noisy_info_by_default_but_keeps_errors() {
    assert!(!should_show_session_log_line(
        "[relay] 2026-04-22T23:20:17Z  INFO ThreadId(01) audio packet received packet=3000",
        ConsoleLogLevel::Warn
    ));
    assert!(!should_show_session_log_line(
        "[relay] 2026-04-22T23:20:17Z  INFO ThreadId(04) decoded audio level rms=-32",
        ConsoleLogLevel::Warn
    ));
    assert!(should_show_session_log_line(
        "[relay] 2026-04-22T23:20:17Z  WARN pipeline is recovering",
        ConsoleLogLevel::Warn
    ));
    assert!(should_show_session_log_line(
        "[relay] 2026-04-22T23:20:17Z  INFO ❌ connect failed",
        ConsoleLogLevel::Error
    ));
    assert!(should_show_session_log_line(
        "[launcher] Relay connected with inputs routed to remote.",
        ConsoleLogLevel::Error
    ));
    assert!(should_show_session_log_line(
        "[relay] 2026-04-22T23:20:17Z  INFO audio packet received",
        ConsoleLogLevel::Info
    ));
}

#[test]
fn write_audio_gain_request_formats_live_control_file() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("gain.control");
    write_audio_gain_request(&path, 425).expect("write gain");
    let raw = std::fs::read_to_string(path).expect("read gain");
    assert!(raw.starts_with("4.250 "), "{raw}");
}

#[test]
fn write_mic_gain_request_formats_live_control_file() {
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("mic-gain.control");
    write_mic_gain_request(&path, 325).expect("write gain");
    let raw = std::fs::read_to_string(path).expect("read gain");
    assert!(raw.starts_with("3.250 "), "{raw}");
}

#[gtk::test]
#[serial]
fn dock_all_displays_to_preview_closes_popouts_and_resets_surfaces() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-dock")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let state = Rc::new(RefCell::new(LauncherState::new()));
    state
        .borrow_mut()
        .set_display_surface(0, DisplaySurface::Window);
    state
        .borrow_mut()
        .set_display_surface(1, DisplaySurface::Window);
    let state_snapshot = state.borrow().clone();
    let view = build_launcher_view(
        &app,
        "http://127.0.0.1:50051",
        &DeviceCatalog::default(),
        &state_snapshot,
    );
    present_and_settle(&view.window);
    let child_proc = Rc::new(RefCell::new(None::<RelayChild>));

    let left_binding = PreviewBinding::test_stub();
    let right_binding = PreviewBinding::test_stub();
    {
        let mut popouts = view.popouts.borrow_mut();
        popouts[0] = Some(PopoutWindowHandle {
            window: gtk::ApplicationWindow::builder()
                .application(&app)
                .title("Left")
                .build(),
            root: gtk::Box::new(gtk::Orientation::Vertical, 0),
            frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
            picture: gtk::Picture::new(),
            status_label: gtk::Label::new(None),
            binding: left_binding,
        });
        popouts[1] = Some(PopoutWindowHandle {
            window: gtk::ApplicationWindow::builder()
                .application(&app)
                .title("Right")
                .build(),
            root: gtk::Box::new(gtk::Orientation::Vertical, 0),
            frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
            picture: gtk::Picture::new(),
            status_label: gtk::Label::new(None),
            binding: right_binding,
        });
    }

    dock_all_displays_to_preview(&state, &child_proc, &view.popouts, &view.widgets);

    assert!(view.popouts.borrow().iter().all(|handle| handle.is_none()));
    assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview);
    assert_eq!(state.borrow().display_surface(1), DisplaySurface::Preview);
}

#[gtk::test]
#[serial]
fn dock_all_displays_to_preview_handles_reentrant_close_callbacks() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-reentrant-dock")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let state = Rc::new(RefCell::new(LauncherState::new()));
    state
        .borrow_mut()
        .set_display_surface(0, DisplaySurface::Window);
    let state_snapshot = state.borrow().clone();
    let view = build_launcher_view(
        &app,
        "http://127.0.0.1:50051",
        &DeviceCatalog::default(),
        &state_snapshot,
    );
    let child_proc = Rc::new(RefCell::new(None::<RelayChild>));

    let popouts = Rc::clone(&view.popouts);
    let window = gtk::ApplicationWindow::builder()
        .application(&app)
        .title("Reentrant")
        .build();
    {
        let popouts = Rc::clone(&popouts);
        window.connect_close_request(move |_| {
            let _ = popouts.borrow_mut()[0].take();
            glib::Propagation::Proceed
        });
    }
    {
        let mut slot = popouts.borrow_mut();
        slot[0] = Some(PopoutWindowHandle {
            window,
            root: gtk::Box::new(gtk::Orientation::Vertical, 0),
            frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
            picture: gtk::Picture::new(),
            status_label: gtk::Label::new(None),
            binding: PreviewBinding::test_stub(),
        });
    }

    dock_all_displays_to_preview(&state, &child_proc, &popouts, &view.widgets);

    assert!(popouts.borrow().iter().all(|handle| handle.is_none()));
    assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview);
}

#[gtk::test]
#[serial]
fn dock_display_to_preview_restores_closed_eye_placeholder_when_relay_is_idle() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-dock-placeholder")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let state = Rc::new(RefCell::new(LauncherState::new()));
    state
        .borrow_mut()
        .set_display_surface(1, DisplaySurface::Window);
    let state_snapshot = state.borrow().clone();
    let view = build_launcher_view(
        &app,
        "http://127.0.0.1:50051",
        &DeviceCatalog::default(),
        &state_snapshot,
    );
    let child_proc = Rc::new(RefCell::new(None::<RelayChild>));

    *view.widgets.display_panes[1].preview_binding.borrow_mut() = Some(PreviewBinding::test_stub());
    view.widgets.display_panes[1]
        .preview_placeholder
        .set_visible(false);

    {
        let mut popouts = view.popouts.borrow_mut();
        popouts[1] = Some(PopoutWindowHandle {
            window: gtk::ApplicationWindow::builder()
                .application(&app)
                .title("Right")
                .build(),
            root: gtk::Box::new(gtk::Orientation::Vertical, 0),
            frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
            picture: gtk::Picture::new(),
            status_label: gtk::Label::new(None),
            binding: PreviewBinding::test_stub(),
        });
    }

    dock_display_to_preview(&state, &child_proc, &view.popouts, &view.widgets, 1);

    present_and_settle(&view.window);

    assert_eq!(state.borrow().display_surface(1), DisplaySurface::Preview);
    assert!(view.popouts.borrow()[1].is_none());
    assert!(
        view.widgets.display_panes[1].picture.paintable().is_none(),
        "idle docked pane should not retain a stale preview texture"
    );
    assert!(
        view.widgets.display_panes[1]
            .preview_placeholder
            .is_visible(),
        "closed-eye placeholder should return when docking back after disconnect"
    );
    assert_eq!(
        view.widgets.display_panes[1].stream_status.text(),
        "Connect relay to preview."
    );
}

fn realistic_device_catalog() -> DeviceCatalog {
    DeviceCatalog {
        cameras: vec!["usb-046d_Logitech_BRIO_5F6EB379-video-index0".to_string()],
        camera_modes: [(
            "usb-046d_Logitech_BRIO_5F6EB379-video-index0".to_string(),
            vec![
                CameraMode::new(1920, 1080, 30),
                CameraMode::new(1280, 720, 30),
            ],
        )]
        .into_iter()
        .collect::<BTreeMap<_, _>>(),
        microphones: vec![
            "alsa_input.usb-Focusrite_Scarlett_2i2_USB_Y7ABC12345-00.analog-stereo".to_string(),
        ],
        speakers: vec![
            "alsa_output.pci-0000_00_1f.3.analog-stereo".to_string(),
            "bluez_output.80_C3_BA_76_26_AB.1".to_string(),
        ],
        keyboards: vec!["usb-Corsair_K70_RGB_PRO_Mechanical_Gaming_Keyboard-event-kbd".to_string()],
        mice: vec!["usb-Logitech_G502_X_LIGHTSPEED_Gaming_Mouse-event-mouse".to_string()],
    }
}

#[gtk::test]
#[serial]
fn shutdown_launcher_runtime_closes_preview_bindings_and_popouts() {
    if gtk::gdk::Display::default().is_none() {
        return;
    }

    let app = gtk::Application::builder()
        .application_id("dev.lesavka.test-shutdown")
        .build();
    let _ = app.register(None::<&gtk::gio::Cancellable>);

    let state = Rc::new(RefCell::new(LauncherState::new()));
    let state_snapshot = state.borrow().clone();
    let view = build_launcher_view(
        &app,
        "http://127.0.0.1:50051",
        &DeviceCatalog::default(),
        &state_snapshot,
    );
    let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
    let tests = Rc::new(RefCell::new(DeviceTestController::new()));

    let left_binding = PreviewBinding::test_stub();
    let right_binding = PreviewBinding::test_stub();
    *view.widgets.display_panes[0].preview_binding.borrow_mut() = Some(left_binding.clone());
    *view.widgets.display_panes[1].preview_binding.borrow_mut() = Some(right_binding.clone());

    {
        let mut popouts = view.popouts.borrow_mut();
        popouts[0] = Some(PopoutWindowHandle {
            window: gtk::ApplicationWindow::builder()
                .application(&app)
                .title("Left")
                .build(),
            root: gtk::Box::new(gtk::Orientation::Vertical, 0),
            frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
            picture: gtk::Picture::new(),
            status_label: gtk::Label::new(None),
            binding: PreviewBinding::test_stub(),
        });
    }

    *view.diagnostics_popout.borrow_mut() = Some(
        gtk::ApplicationWindow::builder()
            .application(&app)
            .title("Diagnostics")
            .build(),
    );
    *view.log_popout.borrow_mut() = Some(
        gtk::ApplicationWindow::builder()
            .application(&app)
            .title("Log")
            .build(),
    );

    shutdown_launcher_runtime(
        &child_proc,
        &tests,
        None,
        &view.widgets,
        &view.popouts,
        &view.diagnostics_popout,
        &view.log_popout,
    );

    assert!(view.popouts.borrow().iter().all(|handle| handle.is_none()));
    assert!(
        view.widgets.display_panes[0]
            .preview_binding
            .borrow()
            .is_none()
    );
    assert!(
        view.widgets.display_panes[1]
            .preview_binding
            .borrow()
            .is_none()
    );
    assert!(view.diagnostics_popout.borrow().is_none());
    assert!(view.log_popout.borrow().is_none());
}
