use super::*;

/// Keeps `with_clean_offset_env` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync.
/// Inputs are the typed parameters; output is the return value or side effect.
fn with_clean_offset_env(test: impl FnOnce()) {
    temp_env::with_vars(
        [
            ("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
            ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", None::<&str>),
            (
                "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US",
                None::<&str>,
            ),
            (
                "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
                None::<&str>,
            ),
            ("LESAVKA_UVC_WIDTH", None::<&str>),
            ("LESAVKA_UVC_HEIGHT", None::<&str>),
            ("LESAVKA_UVC_FPS", None::<&str>),
            ("LESAVKA_UVC_INTERVAL", None::<&str>),
            ("LESAVKA_CAM_CODEC", None::<&str>),
            ("LESAVKA_UPLINK_CAMERA_CODEC", None::<&str>),
            ("LESAVKA_UPLINK_AUDIO_CODEC", None::<&str>),
            ("LESAVKA_CALIBRATION_AUDIO_CODEC", None::<&str>),
            ("LESAVKA_CALIBRATION_PROFILE", None::<&str>),
            (
                "LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
                None::<&str>,
            ),
            (
                "LESAVKA_UPSTREAM_MJPEG_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
                None::<&str>,
            ),
            (
                "LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_MODE_OFFSETS_US",
                None::<&str>,
            ),
            (
                "LESAVKA_UPSTREAM_MJPEG_OPUS_VIDEO_PLAYOUT_OFFSET_US",
                None::<&str>,
            ),
        ],
        test,
    );
}

#[test]
/// Keeps `runtime_uses_baked_mode_offsets_before_calibration_store_loads` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync.
/// Inputs are the typed parameters; output is the return value or side effect.
fn runtime_uses_baked_mode_offsets_before_calibration_store_loads() {
    for (width, height, fps, expected_video_offset_us) in [
        ("1280", "720", "20", 162_659),
        ("1280", "720", "30", 135_090),
        ("1920", "1080", "20", 160_045),
        ("1920", "1080", "30", 127_952),
    ] {
        with_clean_offset_env(|| {
            temp_env::with_vars(
                [
                    ("LESAVKA_UVC_WIDTH", Some(width)),
                    ("LESAVKA_UVC_HEIGHT", Some(height)),
                    ("LESAVKA_UVC_FPS", Some(fps)),
                ],
                || {
                    let runtime = UpstreamMediaRuntime::new();
                    assert_eq!(
                        runtime.playout_offsets(),
                        (expected_video_offset_us, 0),
                        "{width}x{height}@{fps} should use its baked startup offset"
                    );
                },
            );
        });
    }
}

#[test]
/// Keeps `runtime_prefers_mode_offset_map_over_scalar_fallback` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync.
/// Inputs are the typed parameters; output is the return value or side effect.
fn runtime_prefers_mode_offset_map_over_scalar_fallback() {
    with_clean_offset_env(|| {
        temp_env::with_vars(
            [
                ("LESAVKA_UVC_WIDTH", Some("1280")),
                ("LESAVKA_UVC_HEIGHT", Some("720")),
                ("LESAVKA_UVC_FPS", Some("30")),
                ("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", Some("999999")),
                (
                    "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
                    Some("1280x720@30=135090"),
                ),
            ],
            || {
                let runtime = UpstreamMediaRuntime::new();
                assert_eq!(runtime.playout_offsets(), (135_090, 0));
            },
        );
    });
}

#[test]
fn runtime_can_start_with_opus_specific_audio_calibration_profile() {
    with_clean_offset_env(|| {
        temp_env::with_vars(
            [
                ("LESAVKA_UVC_WIDTH", Some("1280")),
                ("LESAVKA_UVC_HEIGHT", Some("720")),
                ("LESAVKA_UVC_FPS", Some("30")),
                ("LESAVKA_UPLINK_CAMERA_CODEC", Some("hevc")),
                ("LESAVKA_UPLINK_AUDIO_CODEC", Some("opus")),
                (
                    "LESAVKA_UPSTREAM_HEVC_OPUS_AUDIO_PLAYOUT_MODE_OFFSETS_US",
                    Some("1280x720@30=24000"),
                ),
            ],
            || {
                let runtime = UpstreamMediaRuntime::new();
                assert_eq!(runtime.playout_offsets(), (110_000, 24_000));
            },
        );
    });
}

#[test]
fn runtime_starts_mjpeg_opus_with_blind_client_rct_offsets() {
    with_clean_offset_env(|| {
        temp_env::with_vars(
            [
                ("LESAVKA_UVC_WIDTH", Some("1280")),
                ("LESAVKA_UVC_HEIGHT", Some("720")),
                ("LESAVKA_UVC_FPS", Some("30")),
                ("LESAVKA_UPLINK_CAMERA_CODEC", Some("mjpeg")),
                ("LESAVKA_UPLINK_AUDIO_CODEC", Some("opus")),
            ],
            || {
                let runtime = UpstreamMediaRuntime::new();
                assert_eq!(runtime.playout_offsets(), (513_850, -34_199));
            },
        );
    });
}

#[test]
/// Keeps `runtime_records_client_and_sink_timing_for_upstream_snapshots` explicit because the blind client-to-RCT probe depends on this telemetry to explain freshness losses.
/// Inputs are paired camera/microphone timing samples plus sink handoff marks; output is a live snapshot with skew, queue, late, and freeze fields populated.
fn runtime_records_client_and_sink_timing_for_upstream_snapshots() {
    with_clean_offset_env(|| {
        let runtime = UpstreamMediaRuntime::new();
        let camera = runtime.activate_camera();
        let microphone = runtime.activate_microphone();
        assert!(runtime.is_camera_active(camera.generation));
        assert!(runtime.is_microphone_active(microphone.generation));

        runtime.set_playout_offsets(12_000, -3_000);
        assert_eq!(runtime.playout_offsets(), (12_000, -3_000));

        runtime.record_client_timing(
            UpstreamMediaKind::Camera,
            UpstreamClientTiming {
                capture_pts_us: 100_000,
                send_pts_us: 120_000,
                queue_depth: 2,
                queue_age_ms: 20,
            },
        );
        runtime.record_client_timing(
            UpstreamMediaKind::Microphone,
            UpstreamClientTiming {
                capture_pts_us: 106_000,
                send_pts_us: 130_000,
                queue_depth: 5,
                queue_age_ms: 35,
            },
        );

        let due = tokio::time::Instant::now() - std::time::Duration::from_millis(2);
        runtime.mark_video_presented(10_000, due);
        runtime.mark_audio_presented(11_500, due);
        runtime.record_video_freeze("test freeze");

        let snapshot = runtime.snapshot();
        assert_eq!(snapshot.phase, "healing");
        assert_eq!(snapshot.client_capture_skew_ms, Some(6.0));
        assert_eq!(snapshot.client_send_skew_ms, Some(10.0));
        assert_eq!(snapshot.camera_client_queue_age_ms, Some(20.0));
        assert!(
            snapshot.live_lag_ms.is_some_and(|lag| lag >= 20.0),
            "upstream live lag should include the client queue age lower bound"
        );
        assert_eq!(snapshot.microphone_client_queue_age_ms, Some(35.0));
        assert_eq!(snapshot.last_video_presented_pts_us, Some(10_000));
        assert_eq!(snapshot.last_audio_presented_pts_us, Some(11_500));
        assert_eq!(snapshot.planner_skew_ms, Some(1.5));
        assert_eq!(snapshot.video_freezes, 1);
        assert_eq!(snapshot.last_reason, "test freeze");
        assert_eq!(snapshot.client_timing_window_samples, 1);
        assert!(snapshot.sink_handoff_window_samples >= 1);

        runtime.close_camera(camera.generation);
        runtime.close_microphone(microphone.generation);
        assert!(!runtime.is_camera_active(camera.generation));
        assert!(!runtime.is_microphone_active(microphone.generation));
    });
}

#[test]
/// Keeps `runtime_public_mapping_helpers_cover_legacy_and_bundled_paths` explicit because these helpers are the thin boundary between transport packets and the shared playout clock.
/// Inputs are legacy and bundled remote timestamps; output proves both paths map onto fresh monotonic local PTS values.
fn runtime_public_mapping_helpers_cover_legacy_and_bundled_paths() {
    with_clean_offset_env(|| {
        let runtime = UpstreamMediaRuntime::new();

        assert_eq!(runtime.map_video_pts(1_000, 0), Some(0));
        assert_eq!(runtime.map_video_pts(1_000, 33_333), Some(33_333));
        assert_eq!(runtime.map_audio_pts(1_500), Some(500));

        let bundle_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10);
        let decision =
            runtime.plan_bundled_pts(UpstreamMediaKind::Camera, 2_000, 0, 1_000, bundle_epoch);

        match decision {
            UpstreamPlanDecision::Play(plan) => {
                assert!(plan.local_pts_us >= 1_000);
                assert_eq!(plan.source_lag, std::time::Duration::ZERO);
            }
            other => panic!("expected bundled play decision, got {other:?}"),
        }
    });
}

#[test]
/// Keeps `bundled_session_activation_resets_stale_playout_epoch` explicit because reconnecting a v2 webcam+mic stream must not inherit stale clocks from a superseded stream.
/// Inputs are two bundled activations with an old presented frame; output proves the new stream starts at local PTS zero and retires the old generations.
fn bundled_session_activation_resets_stale_playout_epoch() {
    with_clean_offset_env(|| {
        let runtime = UpstreamMediaRuntime::new();
        let first = runtime.activate_bundled_session();
        assert_eq!(first.camera.session_id, first.microphone.session_id);

        let first_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10);
        let first_decision = runtime.plan_bundled_pts(
            UpstreamMediaKind::Camera,
            500_000,
            33_333,
            100_000,
            first_epoch,
        );
        let first_plan = match first_decision {
            UpstreamPlanDecision::Play(plan) => plan,
            other => panic!("expected first bundled play decision, got {other:?}"),
        };
        assert_eq!(first_plan.local_pts_us, 400_000);
        runtime.mark_video_presented(first_plan.local_pts_us, first_plan.due_at);
        assert_eq!(
            runtime.snapshot().last_video_presented_pts_us,
            Some(400_000)
        );

        let second = runtime.activate_bundled_session();
        assert_ne!(first.camera.session_id, second.camera.session_id);
        assert_eq!(second.camera.session_id, second.microphone.session_id);
        assert!(!runtime.is_camera_active(first.camera.generation));
        assert!(!runtime.is_microphone_active(first.microphone.generation));
        assert!(runtime.is_camera_active(second.camera.generation));
        assert!(runtime.is_microphone_active(second.microphone.generation));
        assert_eq!(runtime.snapshot().last_video_presented_pts_us, None);

        let second_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10);
        let second_decision = runtime.plan_bundled_pts(
            UpstreamMediaKind::Camera,
            1_000,
            33_333,
            1_000,
            second_epoch,
        );
        match second_decision {
            UpstreamPlanDecision::Play(plan) => assert_eq!(plan.local_pts_us, 0),
            other => panic!("expected fresh bundled play decision, got {other:?}"),
        }
    });
}

#[test]
/// Keeps `runtime_soft_microphone_recovery_cycles_only_the_microphone_generation` explicit because a failed UAC handoff should not disturb active camera playout.
/// Inputs are an active camera lease and a soft microphone recovery request; output keeps camera active while cycling microphone state.
fn runtime_soft_microphone_recovery_cycles_only_the_microphone_generation() {
    with_clean_offset_env(|| {
        let runtime = UpstreamMediaRuntime::new();
        let camera = runtime.activate_camera();
        let microphone = runtime.activate_microphone();

        runtime.soft_recover_microphone();

        assert!(runtime.is_camera_active(camera.generation));
        assert!(!runtime.is_microphone_active(microphone.generation));
        runtime.close_camera(camera.generation);
    });
}

#[test]
/// Keeps `runtime_soft_microphone_recovery_preserves_calibration_offsets` explicit because the manual and automatic audio healing paths must not masquerade as A/V calibration.
/// Inputs are active upstream leases plus non-default playout offsets; output keeps the offsets and camera generation intact while retiring only the microphone generation.
fn runtime_soft_microphone_recovery_preserves_calibration_offsets() {
    with_clean_offset_env(|| {
        let runtime = UpstreamMediaRuntime::new();
        let camera = runtime.activate_camera();
        let microphone = runtime.activate_microphone();

        runtime.set_playout_offsets(135_090, 7_000);
        runtime.soft_recover_microphone();

        assert_eq!(runtime.playout_offsets(), (135_090, 7_000));
        assert!(runtime.is_camera_active(camera.generation));
        assert!(!runtime.is_microphone_active(microphone.generation));
        runtime.close_camera(camera.generation);
    });
}
