use super::*;
use serial_test::serial;
use std::fs;
use temp_env::{with_var, with_vars};
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 branch_smoke_covers_low_hit_paths() {
    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,
    });
    handle_data(
        -1,
        0,
        &mut state,
        &mut pending,
        interfaces,
        UvcRequestData {
            length: STREAM_CTRL_SIZE_11 as i32,
            data: [0u8; UVC_DATA_SIZE],
        },
        true,
    );
    assert!(pending.is_none());
    assert!(
        build_in_response(
            &state,
            interfaces,
            interfaces.streaming,
            0xFE,
            UVC_GET_CUR,
            8
        )
        .is_none()
    );
    let short = [0u8; 8];
    let _ = sanitize_streaming_control(&short, &state);
}

#[test]
fn io_helpers_cover_empty_and_missing_sources() {
    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 = format!("/tmp/lesavka-uvc-missing-{}", std::process::id());
    assert_eq!(read_fifo_min(&missing), None);
}

#[test]
fn uvc_frame_max_bytes_defaults_to_freshness_budget_and_allows_override() {
    with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", None::<&str>),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", None::<&str>),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", None::<&str>),
        ],
        || {
            let cfg = sample_cfg();
            assert_eq!(uvc_frame_max_bytes(cfg), 180_000);
        },
    );

    with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("1")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", Some("123456")),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("1")),
        ],
        || {
            assert_eq!(uvc_frame_max_bytes(sample_cfg()), 65_536);
        },
    );

    with_var("LESAVKA_UVC_FRAME_MAX_BYTES", Some("0"), || {
        assert_eq!(uvc_frame_max_bytes(sample_cfg()), 180_000);
    });

    with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("0")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", Some("123456")),
        ],
        || {
            assert_eq!(uvc_frame_max_bytes(sample_cfg()), 123_456);
        },
    );

    with_var("LESAVKA_UVC_STATS_INTERVAL_MS", Some("0"), || {
        assert_eq!(uvc_stats_interval(), None);
    });
}

#[test]
#[serial]
fn uvc_frame_budget_caps_isochronous_transport() {
    with_vars(
        [
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("10000000")),
            ("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT", None::<&str>),
        ],
        || {
            assert_eq!(
                derived_uvc_frame_max_bytes_for_transport(30, 1024, false),
                232_106
            );
            assert_eq!(
                derived_uvc_frame_max_bytes_for_transport(30, 1024, true),
                333_333
            );
        },
    );
}

#[test]
fn uvc_queue_pacing_defaults_off_but_can_be_enabled() {
    with_var("LESAVKA_UVC_QUEUE_PACING", None::<&str>, || {
        assert_eq!(uvc_queue_period(30), None);
    });

    with_var("LESAVKA_UVC_QUEUE_PACING", Some("1"), || {
        assert_eq!(
            uvc_queue_period(30),
            Some(std::time::Duration::from_nanos(33_333_333))
        );
    });
}

#[test]
#[serial]
fn uvc_env_flags_and_bulk_configfs_edges_are_covered() {
    with_var("LESAVKA_UVC_QUEUE_PACING", Some("maybe"), || {
        assert!(env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true));
        assert!(!env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", false));
    });

    let configfs = tempfile::tempdir().expect("tempdir");
    assert!(!uvc_bulk_transfer_enabled_for_base(configfs.path()));
    fs::write(configfs.path().join("streaming_bulk"), "0\n").expect("streaming_bulk zero");
    assert!(!uvc_bulk_transfer_enabled_for_base(configfs.path()));
    fs::write(configfs.path().join("streaming_bulk"), "1\n").expect("streaming_bulk one");
    assert!(uvc_bulk_transfer_enabled_for_base(configfs.path()));
}

#[test]
#[serial]
fn uvc_startup_open_can_opt_into_write_mode() {
    let device = NamedTempFile::new().expect("device");
    with_vars(
        [
            ("LESAVKA_UVC_CONTROL_READ_ONLY", Some("0")),
            ("LESAVKA_UVC_BLOCKING", Some("1")),
        ],
        || {
            let opened = open_with_retry(device.path().to_str().expect("path"));
            assert!(opened.is_ok());
        },
    );
}

#[test]
#[serial]
fn uvc_stats_paths_and_intervals_cover_operator_overrides() {
    let dir = tempfile::tempdir().expect("tempdir");
    let stats_path = dir.path().join("video-stats.json");
    with_var(
        "LESAVKA_UVC_STATS_PATH",
        Some(stats_path.to_str().expect("stats path")),
        || {
            assert_eq!(uvc_stats_path(), Some(stats_path.clone()));
        },
    );

    with_var("LESAVKA_UVC_STATS_INTERVAL_MS", Some("25"), || {
        assert_eq!(
            uvc_stats_interval(),
            Some(std::time::Duration::from_millis(25))
        );
    });
}

#[test]
fn uvc_video_stream_refresh_covers_fresh_invalid_and_missing_frames() {
    let valid = NamedTempFile::new().expect("valid frame");
    fs::write(valid.path(), [0xff, 0xd8, 1, 2, 3, 0xff, 0xd9]).expect("write valid");
    let mut stream = UvcVideoStream::new(-1);
    stream.frame_path = valid.path().to_path_buf();
    stream.refresh_latest_frame();
    assert_eq!(stream.stats.reloaded, 1);
    assert_eq!(stream.stats.latest_bytes, 7);
    assert_eq!(stream.latest_frame, vec![0xff, 0xd8, 1, 2, 3, 0xff, 0xd9]);

    let invalid = NamedTempFile::new().expect("invalid frame");
    fs::write(invalid.path(), b"not-jpeg").expect("write invalid");
    let mut stream = UvcVideoStream::new(-1);
    stream.frame_path = invalid.path().to_path_buf();
    stream.latest_frame = b"bad-cache".to_vec();
    stream.refresh_latest_frame();
    assert_eq!(stream.stats.rejected_invalid, 1);
    assert_eq!(stream.stats.fallback_idle, 1);
    assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);

    let missing = tempfile::tempdir()
        .expect("tempdir")
        .path()
        .join("missing-frame.mjpg");
    let mut stream = UvcVideoStream::new(-1);
    stream.frame_path = missing;
    stream.latest_frame = b"bad-cache".to_vec();
    stream.refresh_latest_frame();
    assert_eq!(stream.stats.fallback_idle, 1);
    assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);

    let tiny_buffer_stream = UvcVideoStream::new(-1);
    assert_eq!(
        tiny_buffer_stream.frame_for_buffer(MINIMAL_MJPEG_FRAME.len()),
        MINIMAL_MJPEG_FRAME
    );
    assert_eq!(
        tiny_buffer_stream.frame_for_buffer(IDLE_MJPEG_FRAME.len()),
        IDLE_MJPEG_FRAME
    );
}

#[test]
fn uvc_video_stream_holds_last_good_frame_during_transient_rejection() {
    let frame = NamedTempFile::new().expect("frame");
    let good = vec![0xff, 0xd8, 1, 2, 3, 0xff, 0xd9];
    fs::write(frame.path(), &good).expect("write good frame");
    let mut stream = UvcVideoStream::new(-1);
    stream.frame_path = frame.path().to_path_buf();
    stream.frame_max_bytes = 8;
    stream.refresh_latest_frame();
    assert_eq!(stream.latest_frame, good);

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

    assert_eq!(stream.latest_frame, good);
    assert_eq!(stream.stats.rejected_oversize, 1);
    assert_eq!(stream.stats.held_last_good, 1);
    assert_eq!(stream.stats.fallback_idle, 0);
}

#[test]
fn uvc_video_stream_uses_idle_only_after_last_good_timeout() {
    let frame = NamedTempFile::new().expect("frame");
    let good = vec![0xff, 0xd8, 1, 2, 3, 0xff, 0xd9];
    fs::write(frame.path(), &good).expect("write good frame");
    let mut stream = UvcVideoStream::new(-1);
    stream.frame_path = frame.path().to_path_buf();
    stream.refresh_latest_frame();
    assert_eq!(stream.latest_frame, good);

    stream.last_verified_mtime = Some(std::time::SystemTime::UNIX_EPOCH);
    fs::write(frame.path(), b"truncated").expect("write invalid frame");
    stream.refresh_latest_frame();

    assert_eq!(stream.stats.rejected_invalid, 1);
    assert_eq!(stream.stats.held_last_good, 0);
    assert_eq!(stream.stats.fallback_idle, 1);
    assert_eq!(stream.latest_frame, IDLE_MJPEG_FRAME);
}

#[test]
#[serial]
fn main_coverage_mode_returns_error_for_non_uvc_node() {
    with_var("LESAVKA_UVC_DEV", Some("/dev/null"), || {
        with_var("LESAVKA_UVC_BLOCKING", Some("1"), || {
            let result = main();
            assert!(result.is_err());
        });
    });
}
