// Extra coverage for `lesavka-uvc` control/error branches.
//
// Scope: keep additive branch tests in a separate file so each test module
// remains under the 500 LOC contract.
// Targets: `server/src/bin/lesavka-uvc.rs`.
// Why: preserve expanded UVC branch coverage while satisfying test module contracts.

mod uvc_binary_extra {
    #![allow(warnings)]
    #![allow(clippy::all)]
    #![allow(dead_code)]
    #![allow(unused_imports)]
    #![allow(unused_variables)]

    include!(env!("LESAVKA_SERVER_UVC_BIN_SRC"));

    use serial_test::serial;
    use std::fs;
    use std::path::PathBuf;
    use temp_env::with_var;
    use tempfile::NamedTempFile;

    fn sample_cfg() -> UvcConfig {
        UvcConfig {
            width: 1280,
            height: 720,
            fps: 25,
            interval: 400_000,
            max_packet: 1024,
            frame_size: 1_843_200,
            bulk: true,
        }
    }

    fn sample_interfaces() -> UvcInterfaces {
        UvcInterfaces {
            control: UVC_STRING_CONTROL_IDX,
            streaming: UVC_STRING_STREAMING_IDX,
        }
    }

    #[test]
    fn handle_setup_stalls_non_streaming_set_cur_and_non_in_requests() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let mut pending = None;

        let set_cur_other_iface = UsbCtrlRequest {
            b_request_type: 0x00,
            b_request: UVC_SET_CUR,
            w_value: (0xFEu16) << 8,
            w_index: 0x00FF,
            w_length: 8,
        };
        handle_setup(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            set_cur_other_iface,
            true,
        );
        assert!(pending.is_none());

        let non_in_non_set_cur = UsbCtrlRequest {
            b_request_type: 0x00,
            b_request: UVC_GET_CUR,
            w_value: (UVC_VS_PROBE_CONTROL as u16) << 8,
            w_index: interfaces.streaming as u16,
            w_length: 8,
        };
        handle_setup(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            non_in_non_set_cur,
            true,
        );
        assert!(pending.is_none());
    }

    #[test]
    fn handle_setup_rejects_oversized_set_cur_payload() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let mut pending = None;
        let oversized = UsbCtrlRequest {
            b_request_type: 0x00,
            b_request: UVC_SET_CUR,
            w_value: (UVC_VS_PROBE_CONTROL as u16) << 8,
            w_index: interfaces.streaming as u16,
            w_length: (UVC_DATA_SIZE as u16).saturating_add(1),
        };
        handle_setup(-1, 0, &mut state, &mut pending, interfaces, oversized, true);
        assert!(pending.is_none());
    }

    #[test]
    fn handle_setup_stalls_unknown_in_selector() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let mut pending = None;
        let req = UsbCtrlRequest {
            b_request_type: USB_DIR_IN,
            b_request: UVC_GET_CUR,
            w_value: (0xFEu16) << 8,
            w_index: interfaces.streaming as u16,
            w_length: 8,
        };
        handle_setup(-1, 0, &mut state, &mut pending, interfaces, req, true);
        assert!(pending.is_none());
    }

    #[test]
    fn handle_data_ignores_missing_pending_and_negative_lengths() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let mut pending = None;
        handle_data(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            UvcRequestData {
                length: 8,
                data: [0u8; UVC_DATA_SIZE],
            },
            true,
        );

        pending = Some(PendingRequest {
            interface: interfaces.streaming,
            selector: UVC_VS_PROBE_CONTROL,
            expected_len: STREAM_CTRL_SIZE_11,
        });
        handle_data(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            UvcRequestData {
                length: -1,
                data: [0u8; UVC_DATA_SIZE],
            },
            true,
        );
        assert!(pending.is_none());
    }

    #[test]
    fn handle_data_ignores_non_streaming_pending_requests() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let mut pending = Some(PendingRequest {
            interface: interfaces.control,
            selector: UVC_VS_PROBE_CONTROL,
            expected_len: STREAM_CTRL_SIZE_11,
        });
        let mut payload = [0u8; UVC_DATA_SIZE];
        payload[2] = 1;
        handle_data(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            UvcRequestData {
                length: STREAM_CTRL_SIZE_11 as i32,
                data: payload,
            },
            true,
        );
        assert!(pending.is_none());
        assert_eq!(state.probe, state.default);
    }

    #[test]
    fn build_in_response_returns_none_for_unknown_selector() {
        let state = UvcState::new(sample_cfg());
        let interfaces = sample_interfaces();
        let response = build_in_response(
            &state,
            interfaces,
            interfaces.streaming,
            0xFE,
            UVC_GET_CUR,
            8,
        );
        assert!(response.is_none());
    }

    #[test]
    fn sanitize_streaming_control_keeps_defaults_for_short_payload() {
        let state = UvcState::new(sample_cfg());
        let short = [0u8; 8];
        let out = sanitize_streaming_control(&short, &state);
        assert_eq!(out, state.default);
    }

    #[test]
    fn control_length_updates_only_for_supported_changed_sizes() {
        let mut state = UvcState::new(sample_cfg());
        let original = state.ctrl_len;

        maybe_update_ctrl_len(&mut state, 99, true);
        assert_eq!(state.ctrl_len, original);

        maybe_update_ctrl_len(&mut state, original as u16, true);
        assert_eq!(state.ctrl_len, original);

        let next = if original == STREAM_CTRL_SIZE_11 {
            STREAM_CTRL_SIZE_15
        } else {
            STREAM_CTRL_SIZE_11
        };
        maybe_update_ctrl_len(&mut state, next as u16, true);
        assert_eq!(state.ctrl_len, next);
    }

    #[test]
    fn streaming_response_handles_info_commit_and_unknown_selectors() {
        let state = UvcState::new(sample_cfg());

        assert_eq!(
            build_streaming_response(&state, UVC_VS_PROBE_CONTROL, UVC_GET_INFO),
            Some(vec![0x03])
        );
        assert_eq!(
            build_streaming_response(&state, UVC_VS_COMMIT_CONTROL, UVC_GET_CUR)
                .map(|payload| payload.len()),
            Some(state.ctrl_len)
        );
        assert!(build_streaming_response(&state, 0xFE, UVC_GET_CUR).is_none());
    }

    #[test]
    fn sanitize_streaming_control_accepts_set_bits_and_payload_limits() {
        let state = UvcState::new(sample_cfg());
        let mut payload = [0u8; UVC_DATA_SIZE];
        payload[2] = 1;
        payload[3] = 2;
        payload[4..8].copy_from_slice(&333_333u32.to_le_bytes());
        payload[22..26].copy_from_slice(&(state.cfg.max_packet + 500).to_le_bytes());

        let out = sanitize_streaming_control(&payload, &state);
        assert_eq!(out[2], 1);
        assert_eq!(out[3], 1);
        assert_eq!(read_le32(&out, 4), state.cfg.interval);
        assert_eq!(read_le32(&out, 18), state.cfg.frame_size);
        assert_eq!(read_le32(&out, 22), state.cfg.max_packet);
    }

    #[test]
    fn sanitize_streaming_control_keeps_zero_fields_at_defaults() {
        let state = UvcState::new(sample_cfg());
        let mut payload = [0u8; UVC_DATA_SIZE];
        payload[2] = 0;
        payload[3] = 0;
        payload[4..8].copy_from_slice(&0u32.to_le_bytes());
        payload[22..26].copy_from_slice(&0u32.to_le_bytes());

        let out = sanitize_streaming_control(&payload, &state);
        assert_eq!(out[2], state.default[2]);
        assert_eq!(out[3], state.default[3]);
        assert_eq!(read_le32(&out, 4), read_le32(&state.default, 4));
        assert_eq!(read_le32(&out, 22), read_le32(&state.default, 22));
    }

    #[test]
    #[cfg(coverage)]
    fn parse_device_arg_accepts_flags_and_positional_paths() {
        assert_eq!(
            parse_device_arg(&["--device".to_string(), "/dev/video7".to_string()]),
            Some("/dev/video7".to_string())
        );
        assert_eq!(
            parse_device_arg(&["-d".to_string(), "/dev/video8".to_string()]),
            Some("/dev/video8".to_string())
        );
        assert_eq!(
            parse_device_arg(&["--debug".to_string(), "/dev/video9".to_string()]),
            Some("/dev/video9".to_string())
        );
        assert_eq!(parse_device_arg(&["--device".to_string()]), None);
    }

    #[test]
    fn handle_data_clamps_commit_interval_and_ignores_unknown_streaming_selector() {
        let interfaces = sample_interfaces();
        let mut state = UvcState::new(sample_cfg());
        let original_commit = state.commit;
        let mut payload = [0u8; UVC_DATA_SIZE];
        payload[4..8].copy_from_slice(&222_222u32.to_le_bytes());

        let mut pending = Some(PendingRequest {
            interface: interfaces.streaming,
            selector: UVC_VS_COMMIT_CONTROL,
            expected_len: STREAM_CTRL_SIZE_11,
        });
        handle_data(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            UvcRequestData {
                length: STREAM_CTRL_SIZE_11 as i32,
                data: payload,
            },
            true,
        );
        assert!(pending.is_none());
        assert_eq!(state.commit, original_commit);
        assert_eq!(read_le32(&state.commit, 4), state.cfg.interval);

        let after_commit = state.commit;
        let mut pending = Some(PendingRequest {
            interface: interfaces.streaming,
            selector: 0xFE,
            expected_len: STREAM_CTRL_SIZE_11,
        });
        handle_data(
            -1,
            0,
            &mut state,
            &mut pending,
            interfaces,
            UvcRequestData {
                length: STREAM_CTRL_SIZE_11 as i32,
                data: payload,
            },
            true,
        );
        assert!(pending.is_none());
        assert_eq!(state.commit, after_commit);
    }

    #[test]
    fn io_helpers_return_none_for_empty_or_missing_input() {
        let empty = NamedTempFile::new().expect("tmp");
        fs::write(empty.path(), "\n").expect("write empty");
        assert_eq!(read_u32_first(empty.path().to_str().expect("path")), None);

        let missing = PathBuf::from(format!(
            "/tmp/lesavka-missing-fifo-{}-{}",
            std::process::id(),
            std::thread::current().name().unwrap_or("anon")
        ));
        assert_eq!(read_fifo_min(missing.to_str().expect("missing")), None);
    }

    #[test]
    #[serial]
    fn uvc_freshness_defaults_bound_live_video_backlog() {
        with_var("LESAVKA_UVC_BUFFER_COUNT", None::<&str>, || {
            with_var("LESAVKA_UVC_IDLE_PUMP_MS", None::<&str>, || {
                with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", None::<&str>, || {
                    assert_eq!(uvc_buffer_count(), 4);
                    assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(2));
                    assert_eq!(uvc_queue_period(30), None);
                    assert_eq!(
                        frame_spool_max_age(),
                        Some(std::time::Duration::from_millis(1_000))
                    );
                });
            });
        });
    }

    #[test]
    #[serial]
    fn uvc_freshness_env_clamps_buffers_and_allows_disabling_frame_ttl() {
        with_var("LESAVKA_UVC_BUFFER_COUNT", Some("99"), || {
            with_var("LESAVKA_UVC_IDLE_PUMP_MS", Some("11"), || {
                with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", Some("0"), || {
                    with_var("LESAVKA_UVC_QUEUE_PACING", Some("1"), || {
                        assert_eq!(uvc_buffer_count(), 8);
                        assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(11));
                        assert_eq!(
                            uvc_queue_period(30),
                            Some(std::time::Duration::from_nanos(33_333_333))
                        );
                        assert_eq!(frame_spool_max_age(), None);
                    });
                });
            });
        });
    }

    #[test]
    fn frame_spool_staleness_uses_mtime_and_respects_disabled_ttl() {
        let frame = NamedTempFile::new().expect("tmp frame");
        fs::write(frame.path(), [0xff, 0xd8, 0xff, 0xd9]).expect("write frame");

        assert!(!frame_spool_is_stale(frame.path(), None));
        let missing = PathBuf::from("/tmp/lesavka-definitely-missing-frame.mjpg");
        assert!(frame_spool_is_stale(
            missing.as_path(),
            Some(std::time::Duration::from_millis(1))
        ));
        std::thread::sleep(std::time::Duration::from_millis(2));
        assert!(frame_spool_is_stale(
            frame.path(),
            Some(std::time::Duration::from_millis(1))
        ));
    }

    #[test]
    fn uvc_frame_refresh_rejects_frames_that_would_be_truncated() {
        let frame = NamedTempFile::new().expect("tmp frame");
        let mut stream = UvcVideoStream::new(-1);
        stream.frame_path = frame.path().to_path_buf();
        stream.latest_frame = vec![0xff, 0xd8, 0x11, 0xff, 0xd9];
        stream.buffers.push(MmapBuffer {
            ptr: std::ptr::null_mut(),
            len: 8,
        });

        fs::write(
            frame.path(),
            [0xff, 0xd8, 1, 2, 3, 4, 5, 6, 7, 8, 0xff, 0xd9],
        )
        .expect("write oversize frame");
        stream.refresh_latest_frame();

        assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);
        assert_eq!(stream.stats.rejected_oversize, 1);
        assert_eq!(stream.stats.fallback_idle, 1);
        assert_eq!(stream.stats.last_rejected_oversize_bytes, 12);
        assert_eq!(stream.stats.last_rejected_oversize_cap, 8);
        assert_eq!(stream.frame_payload_limit(), 8);
        assert_eq!(stream.frame_for_buffer(4), MINIMAL_MJPEG_FRAME);
        assert_eq!(
            UvcVideoStream::new(-1).frame_for_buffer(64 * 1024),
            IDLE_MJPEG_FRAME
        );
        assert!(IDLE_MJPEG_FRAME.len() > 4);
    }

    #[test]
    fn compute_payload_cap_clamps_limit_pct_bounds() {
        with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", None::<&str>, || {
            with_var("LESAVKA_UVC_LIMIT_PCT", Some("0"), || {
                let cap = compute_payload_cap(false);
                if let Some(cap) = cap {
                    assert!(cap.pct >= 1);
                }
            });
            with_var("LESAVKA_UVC_LIMIT_PCT", Some("250"), || {
                let cap = compute_payload_cap(true);
                if let Some(cap) = cap {
                    assert!(cap.pct <= 100);
                }
            });
        });
    }

    #[test]
    #[serial]
    fn main_returns_error_for_non_uvc_device_node() {
        with_var("LESAVKA_UVC_DEV", Some("/dev/null"), || {
            with_var("LESAVKA_UVC_BLOCKING", Some("1"), || {
                let result = main();
                assert!(
                    result.is_err(),
                    "non-UVC node should fail during event subscribe"
                );
            });
        });
    }
}
