use super::*;
use crate::launcher::state::{
    CaptureSizePreset, DeviceSelection, DisplaySurface, FeedSourcePreset, LauncherState,
    WebcamTransport,
};
use crate::uplink_telemetry::UpstreamStreamTelemetry;

fn sample(n: u64) -> PerformanceSample {
    PerformanceSample {
        rtt_ms: 20.0 + n as f32,
        probe_spread_ms: 3.0 + n as f32,
        input_latency_ms: 10.0 + n as f32,
        probe_loss_pct: n as f32,
        client_process_cpu_pct: 12.5 + n as f32,
        server_process_cpu_pct: 22.5 + n as f32,
        video_loss_pct: (n as f32) * 0.5,
        left_receive_fps: 30.0,
        left_present_fps: 29.0,
        left_server_fps: 30.0,
        left_stream_spread_ms: 4.0,
        left_packet_gap_peak_ms: 55.0,
        left_present_gap_peak_ms: 60.0,
        left_queue_depth: n as u32,
        left_queue_peak: n as u32,
        left_server_source_gap_peak_ms: 42.0,
        left_server_send_gap_peak_ms: 48.0,
        left_server_queue_peak: n as u32 + 1,
        left_server_encoder_label: "x264enc".to_string(),
        left_decoder_label: "decodebin".to_string(),
        left_stream_caps_label:
            "video/x-h264, width=(int)1920, height=(int)1080, framerate=(fraction)60/1".to_string(),
        left_decoded_caps_label:
            "video/x-raw, format=(string)NV12, width=(int)1920, height=(int)1080".to_string(),
        left_rendered_caps_label:
            "video/x-raw, format=(string)RGBA, width=(int)1920, height=(int)1080".to_string(),
        right_receive_fps: 30.0,
        right_present_fps: 28.0,
        right_server_fps: 30.0,
        right_stream_spread_ms: 5.0,
        right_packet_gap_peak_ms: 65.0,
        right_present_gap_peak_ms: 75.0,
        right_queue_depth: n as u32,
        right_queue_peak: n as u32,
        right_server_source_gap_peak_ms: 51.0,
        right_server_send_gap_peak_ms: 58.0,
        right_server_queue_peak: n as u32 + 1,
        right_server_encoder_label: "source-pass-through".to_string(),
        right_decoder_label: "decodebin".to_string(),
        right_stream_caps_label:
            "video/x-h264, width=(int)1920, height=(int)1080, framerate=(fraction)60/1".to_string(),
        right_decoded_caps_label:
            "video/x-raw, format=(string)NV12, width=(int)1920, height=(int)1080".to_string(),
        right_rendered_caps_label:
            "video/x-raw, format=(string)RGBA, width=(int)1920, height=(int)1080".to_string(),
        upstream_camera: UpstreamStreamTelemetry {
            enabled: true,
            connected: true,
            reconnect_count: 2,
            queue_depth: 3,
            queue_peak: 7,
            latest_enqueue_age_ms: 14.0,
            enqueue_age_peak_ms: 48.0,
            enqueue_block_peak_ms: 5.0,
            packets_enqueued: 120,
            packets_streamed: 118,
            dropped_packets: 0,
            dropped_queue_full_packets: 0,
            dropped_stale_packets: 0,
            latest_delivery_age_ms: 22.0,
            delivery_age_peak_ms: 61.0,
            last_error: String::new(),
        },
        upstream_microphone: UpstreamStreamTelemetry {
            enabled: true,
            connected: true,
            reconnect_count: 1,
            queue_depth: 2,
            queue_peak: 5,
            latest_enqueue_age_ms: 11.0,
            enqueue_age_peak_ms: 22.0,
            enqueue_block_peak_ms: 3.0,
            packets_enqueued: 220,
            packets_streamed: 216,
            dropped_packets: 1,
            dropped_queue_full_packets: 1,
            dropped_stale_packets: 0,
            latest_delivery_age_ms: 19.0,
            delivery_age_peak_ms: 37.0,
            last_error: String::new(),
        },
        upstream_mode: "bundled-webcam-media".to_string(),
        dropped_frames: n,
        queue_depth: n as u32,
    }
}

#[test]
fn diagnostics_log_keeps_only_latest_samples_with_capacity() {
    let mut log = DiagnosticsLog::new(2);
    log.record(sample(1));
    log.record(sample(2));
    log.record(sample(3));

    let kept: Vec<u64> = log.iter().map(|item| item.dropped_frames).collect();
    assert_eq!(kept, vec![2, 3]);
    assert_eq!(log.latest().map(|s| s.dropped_frames), Some(3));
}

#[test]
fn diagnostics_log_enforces_minimum_capacity() {
    let mut log = DiagnosticsLog::new(0);
    log.record(sample(1));
    log.record(sample(2));
    assert_eq!(log.len(), 1);
    assert_eq!(log.latest().map(|s| s.dropped_frames), Some(2));
}

#[test]
fn snapshot_report_contains_state_fields_and_samples() {
    let mut state = LauncherState::new();
    state.devices = DeviceSelection {
        camera: Some("/dev/video0".to_string()),
        microphone: Some("alsa_input.usb".to_string()),
        speaker: Some("alsa_output.usb".to_string()),
        keyboard: Some("/dev/input/event10".to_string()),
        mouse: Some("/dev/input/event11".to_string()),
    };
    state.push_note("first note");

    let mut log = DiagnosticsLog::new(4);
    log.record(sample(7));

    let report = SnapshotReport::from_state(&state, &log, quality_probe_command().to_string());
    assert_eq!(report.selected_camera.as_deref(), Some("/dev/video0"));
    assert_eq!(
        report.selected_microphone.as_deref(),
        Some("alsa_input.usb")
    );
    assert_eq!(report.selected_speaker.as_deref(), Some("alsa_output.usb"));
    assert_eq!(report.audio_gain_label, "200%");
    assert_eq!(
        report.selected_keyboard.as_deref(),
        Some("/dev/input/event10")
    );
    assert_eq!(report.selected_mouse.as_deref(), Some("/dev/input/event11"));
    assert_eq!(report.recent_samples.len(), 1);
    assert_eq!(report.notes, vec!["first note".to_string()]);
    assert!(report.status.contains("mode=remote"));
    assert!(report.client_version.starts_with("0."));
    assert_eq!(report.left_feed_source, "Left Eye");
    assert!(
        report
            .left_capture_profile
            .contains("observed 1920x1080 @ 60 fps")
    );
    assert_eq!(report.left_capture_transport, "device H.264 pass-through");
    assert_eq!(report.left_decoder_label, "decodebin");
    assert!(report.left_stream_caps_label.contains("video/x-h264"));
    assert!(report.left_decoded_caps_label.contains("video/x-raw"));
    assert!(report.left_rendered_caps_label.contains("video/x-raw"));
    assert_eq!(report.upstream_camera_transport, "MJPEG");
    assert_eq!(report.upstream_camera.queue_peak, 7);
    assert_eq!(report.upstream_microphone.reconnect_count, 1);
}

#[test]
fn snapshot_report_marks_empty_live_labels_pending() {
    let mut log = DiagnosticsLog::new(1);
    let mut sample = sample(0);
    sample.left_decoder_label.clear();
    sample.left_server_encoder_label.clear();
    sample.left_stream_caps_label.clear();
    sample.left_decoded_caps_label.clear();
    sample.left_rendered_caps_label.clear();
    sample.right_decoder_label.clear();
    sample.right_server_encoder_label.clear();
    sample.right_stream_caps_label.clear();
    sample.right_decoded_caps_label.clear();
    sample.right_rendered_caps_label.clear();
    log.record(sample);

    let mut state = LauncherState::new();
    state.set_feed_source_preset(1, FeedSourcePreset::OtherEye);
    let report = SnapshotReport::from_state(&state, &log, quality_probe_command().to_string());

    assert_eq!(report.left_decoder_label, "pending");
    assert_eq!(report.left_server_encoder_label, "pending");
    assert_eq!(report.left_stream_caps_label, "pending");
    assert_eq!(report.left_decoded_caps_label, "pending");
    assert_eq!(report.left_rendered_caps_label, "pending");
    assert_eq!(report.right_feed_source, "Left Eye (mirrored)");
    assert_eq!(report.right_decoder_label, "pending");
    assert_eq!(report.right_server_encoder_label, "pending");
    assert_eq!(report.right_stream_caps_label, "pending");
    assert_eq!(report.right_decoded_caps_label, "pending");
    assert_eq!(report.right_rendered_caps_label, "pending");
}

#[test]
fn snapshot_json_is_serializable_and_mentions_probe_command() {
    let report = SnapshotReport::from_state(
        &LauncherState::new(),
        &DiagnosticsLog::new(1),
        quality_probe_command().to_string(),
    );
    let json = report.to_pretty_json().expect("serialize");
    assert!(json.contains("platform_quality_gate.sh"));
    assert!(json.contains("routing"));
    assert!(json.contains("view_mode"));
}

#[test]
fn snapshot_text_mentions_versions_profiles_and_recommendations() {
    let report = SnapshotReport::from_state(
        &LauncherState::new(),
        &DiagnosticsLog::new(1),
        quality_probe_command().to_string(),
    );
    let text = report.to_pretty_text();
    assert!(text.contains("Lesavka Diagnostics"));
    assert!(text.contains("client: v"));
    assert!(text.contains("left eye"));
    assert!(text.contains("source:"));
    assert!(text.contains("transport:"));
    assert!(text.contains("live: decoder="));
    assert!(text.contains("stream caps:"));
    assert!(text.contains("decoded caps:"));
    assert!(text.contains("rendered caps:"));
    assert!(text.contains("media staging"));
    assert!(text.contains("uplink camera:"));
    assert!(text.contains("uplink microphone:"));
    assert!(text.contains("uplink mode:"));
    assert!(text.contains("current UI state"));
    assert!(text.contains("recommendations"));
}

#[test]
#[doc = "Verifies diagnostics text follows live media settings."]
fn snapshot_text_reflects_live_media_control_changes() {
    let mut state = LauncherState::new();
    state.select_camera(Some("/dev/video9".to_string()));
    state.select_camera_quality(Some(crate::launcher::devices::CameraMode::new(
        1920, 1080, 30,
    )));
    state.select_webcam_transport(WebcamTransport::Mjpeg);
    state.select_microphone(Some("alsa_input.usb".to_string()));
    state.select_speaker(Some("alsa_output.usb".to_string()));
    state.set_audio_gain_percent(250);
    state.set_mic_gain_percent(125);
    state.set_camera_channel_enabled(false);
    state.set_microphone_channel_enabled(true);

    let report = SnapshotReport::from_state(
        &state,
        &DiagnosticsLog::new(1),
        quality_probe_command().to_string(),
    );
    let text = report.to_pretty_text();

    assert!(
        text.contains("camera: /dev/video9 | quality=1080p@30 | transport=MJPEG | enabled=false")
    );
    assert!(text.contains("speaker: alsa_output.usb | volume=250% | enabled=true"));
    assert!(text.contains("microphone: alsa_input.usb | gain=125% | enabled=true"));
}

#[test]
fn snapshot_text_renders_recent_samples_and_notes() {
    let mut state = LauncherState::new();
    state.set_server_available(true);
    state.push_note("operator changed camera quality during the run");
    let mut log = DiagnosticsLog::new(2);
    log.record(sample(3));

    let report = SnapshotReport::from_state(&state, &log, quality_probe_command().to_string());
    let text = report.to_pretty_text();

    assert!(text.contains("server: unknown (reachable)"));
    assert!(text.contains("rtt=23.0ms"));
    assert!(text.contains("server=lx264enc:42/48/4"));
    assert!(text.contains("uplink: mode=bundled-webcam-media cam=live queue=3/7"));
    assert!(text.contains("notes"));
    assert!(text.contains("operator changed camera quality during the run"));
}

#[test]
fn recommendations_call_out_upstream_queue_age_and_reconnect_churn() {
    let mut log = DiagnosticsLog::new(1);
    let mut stressed = sample(9);
    stressed.upstream_camera.latest_enqueue_age_ms = 180.0;
    stressed.upstream_camera.enqueue_age_peak_ms = 320.0;
    stressed.upstream_camera.enqueue_block_peak_ms = 55.0;
    stressed.upstream_camera.delivery_age_peak_ms = 420.0;
    stressed.upstream_camera.dropped_stale_packets = 4;
    stressed.upstream_camera.reconnect_count = 4;
    stressed.upstream_microphone.latest_enqueue_age_ms = 120.0;
    stressed.upstream_microphone.enqueue_age_peak_ms = 260.0;
    stressed.upstream_microphone.enqueue_block_peak_ms = 31.0;
    stressed.upstream_microphone.delivery_age_peak_ms = 240.0;
    stressed.upstream_microphone.dropped_queue_full_packets = 2;
    log.record(stressed);

    let report = SnapshotReport::from_state(
        &LauncherState::new(),
        &log,
        quality_probe_command().to_string(),
    );

    assert!(
        report
            .recommendations
            .iter()
            .any(|item| { item.contains("webcam uplink queue is aging packets") })
    );
    assert!(
        report
            .recommendations
            .iter()
            .any(|item| { item.contains("microphone uplink queue is aging live audio") })
    );
    assert!(
        report
            .recommendations
            .iter()
            .any(|item| { item.contains("upstream media loops have already reconnected") })
    );
    assert!(
        report
            .recommendations
            .iter()
            .any(|item| { item.contains("webcam uplink is now choosing freshness") })
    );
    assert!(
        report
            .recommendations
            .iter()
            .any(|item| { item.contains("microphone uplink is dropping or aging live chunks") })
    );
}

#[test]
fn snapshot_report_uses_effective_mirrored_capture_profile() {
    let mut state = LauncherState::new();
    state.set_feed_source_preset(0, FeedSourcePreset::OtherEye);
    state.set_capture_size_preset(1, CaptureSizePreset::P720);

    let report = SnapshotReport::from_state(
        &state,
        &DiagnosticsLog::new(1),
        quality_probe_command().to_string(),
    );

    assert_eq!(report.left_feed_source, "Right Eye (mirrored)");
    assert!(report.left_capture_profile.contains("720p"));
    assert!(report.left_capture_profile.contains("1280x720"));
}

#[test]
fn quality_probe_command_runs_full_platform_gate() {
    let cmd = quality_probe_command();
    assert_eq!(cmd, "scripts/ci/platform_quality_gate.sh");
}

#[test]
fn source_capture_profile_prefers_observed_stream_caps_when_available() {
    let capture = CaptureSizeChoice {
        preset: CaptureSizePreset::P1080,
        width: 1920,
        height: 1080,
        fps: 60,
        max_bitrate_kbit: 18_000,
    };
    let label = capture_profile_label(
        &capture,
        "video/x-h264, width=(int)1920, height=(int)1080, framerate=(fraction)60/1",
    );
    assert_eq!(
        label,
        "1080p | observed 1920x1080 @ 60 fps | bitrate est ~18000 kbit"
    );
}

#[test]
fn capture_profile_falls_back_when_stream_caps_are_incomplete() {
    let capture = CaptureSizeChoice {
        preset: CaptureSizePreset::P1080,
        width: 1920,
        height: 1080,
        fps: 60,
        max_bitrate_kbit: 18_000,
    };
    let label = capture_profile_label(&capture, "video/x-h264, width=(int)1920");
    assert_eq!(
        label,
        "1080p | 1920x1080 | 60 fps | bitrate est ~18000 kbit"
    );
}

#[test]
fn recommendations_do_not_suggest_hardware_decode_when_nvdec_is_active() {
    let mut log = DiagnosticsLog::new(1);
    let mut sample = sample(1);
    sample.client_process_cpu_pct = 96.0;
    sample.left_receive_fps = 40.0;
    sample.left_present_fps = 30.0;
    sample.left_decoder_label = "nvh264dec".to_string();
    sample.right_decoder_label = "nvh264dec".to_string();
    log.record(sample);

    let items = recommendations_for(&LauncherState::new(), &log);
    let joined = items.join("\n");
    assert!(!joined.contains("hardware decoder before adding more bitrate"));
    assert!(!joined.contains("lighter breakout sizes or hardware decode"));
    assert!(joined.contains("cheaper source mode"));
}

#[test]
fn recommendations_cover_video_network_queue_cpu_and_decoder_pressure() {
    let mut state = LauncherState::new();
    state.set_server_available(true);
    state.set_display_surface(0, DisplaySurface::Window);
    state.set_display_surface(1, DisplaySurface::Window);

    let mut sample = sample(12);
    sample.probe_loss_pct = 4.0;
    sample.probe_spread_ms = 22.0;
    sample.video_loss_pct = 3.0;
    sample.dropped_frames = 2;
    sample.left_receive_fps = 58.0;
    sample.left_present_fps = 42.0;
    sample.right_receive_fps = 58.0;
    sample.right_present_fps = 42.0;
    sample.left_packet_gap_peak_ms = 180.0;
    sample.right_packet_gap_peak_ms = 181.0;
    sample.left_present_gap_peak_ms = 250.0;
    sample.right_present_gap_peak_ms = 260.0;
    sample.queue_depth = 9;
    sample.left_queue_peak = 5;
    sample.right_queue_peak = 5;
    sample.left_server_send_gap_peak_ms = 40.0;
    sample.right_server_send_gap_peak_ms = 40.0;
    sample.left_server_source_gap_peak_ms = 130.0;
    sample.right_server_source_gap_peak_ms = 131.0;
    sample.left_server_queue_peak = 5;
    sample.right_server_queue_peak = 5;
    sample.client_process_cpu_pct = 90.0;
    sample.server_process_cpu_pct = 88.0;
    sample.left_decoder_label = "avdec_h264".to_string();
    sample.right_decoder_label = "avdec_h264".to_string();
    sample.left_server_encoder_label = "x264enc".to_string();
    sample.right_server_encoder_label = "x264enc".to_string();

    let mut log = DiagnosticsLog::new(1);
    log.record(sample);
    let joined = recommendations_for(&state, &log).join("\n");

    for needle in [
        "Control-plane probe spread or loss is elevated",
        "Video packets are arriving with gaps",
        "receiving more frames than it is presenting",
        "Present-gap spikes are materially larger",
        "preview queue is backing up",
        "Queue depth is spiking",
        "Client packet-gap spikes are much larger",
        "large source-frame gaps",
        "server-side stream queue is peaking",
        "Client process CPU is high",
        "Server process CPU is high",
        "Device H.264 pass-through is active",
        "At least one eye is falling back",
        "At least one eye is still leaning on `x264enc`",
        "Both eye feeds are broken out",
    ] {
        assert!(joined.contains(needle), "{needle} missing from {joined}");
    }
}

#[test]
fn recommendations_cover_low_receive_fps_and_bursty_gap_without_loss() {
    let mut sample = sample(0);
    sample.video_loss_pct = 0.0;
    sample.dropped_frames = 0;
    sample.left_server_fps = 60.0;
    sample.left_receive_fps = 48.0;
    sample.right_server_fps = 60.0;
    sample.right_receive_fps = 48.0;
    sample.left_packet_gap_peak_ms = 150.0;
    sample.right_packet_gap_peak_ms = 151.0;

    let mut log = DiagnosticsLog::new(1);
    log.record(sample);
    let joined = recommendations_for(&LauncherState::new(), &log).join("\n");

    assert!(joined.contains("Receive fps is well below the target without packet loss"));
    assert!(joined.contains("Packet-gap spikes are high without packet loss"));
}

#[test]
fn hardware_decoder_detection_recognizes_nvdec_labels() {
    let mut sample = sample(1);
    sample.left_decoder_label = "nvh264dec".to_string();
    assert!(sample_uses_hardware_decode(&sample));
    assert!(!sample_uses_software_decode(&sample));
}
