use super::{UpstreamMediaRuntime, play, runtime_without_offsets};
use serial_test::serial;
use std::time::Duration;

#[test]
#[serial(upstream_media_runtime)]
fn first_stream_starts_a_new_shared_session() {
    let runtime = UpstreamMediaRuntime::new();
    let camera = runtime.activate_camera();
    let microphone = runtime.activate_microphone();

    assert_eq!(camera.session_id, 1);
    assert_eq!(microphone.session_id, 1);
    assert!(runtime.is_camera_active(camera.generation));
    assert!(runtime.is_microphone_active(microphone.generation));
}

#[test]
#[serial(upstream_media_runtime)]
fn replacing_one_kind_keeps_the_session_but_preempts_the_old_owner() {
    let runtime = UpstreamMediaRuntime::new();
    let first = runtime.activate_microphone();
    let second = runtime.activate_microphone();

    assert_eq!(first.session_id, second.session_id);
    assert!(!runtime.is_microphone_active(first.generation));
    assert!(runtime.is_microphone_active(second.generation));
}

#[test]
#[serial(upstream_media_runtime)]
fn replacing_one_kind_resets_stale_timing_anchors_before_repairing() {
    temp_env::with_var(
        "LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_MS",
        Some("0"),
        || {
            let runtime = runtime_without_offsets();
            let first_camera = runtime.activate_camera();
            let first_microphone = runtime.activate_microphone();

            assert!(matches!(
                runtime.plan_video_pts(1_000_000, 16_666),
                super::UpstreamPlanDecision::AwaitingPair
            ));
            let stale_audio = play(runtime.plan_audio_pts(1_000_000));
            let stale_video = play(runtime.plan_video_pts(1_000_000, 16_666));
            assert_eq!(stale_audio.local_pts_us, stale_video.local_pts_us);

            let replacement_camera = runtime.activate_camera();
            assert_eq!(replacement_camera.session_id, first_camera.session_id);
            assert!(!runtime.is_camera_active(first_camera.generation));
            assert!(runtime.is_microphone_active(first_microphone.generation));

            assert!(matches!(
                runtime.plan_video_pts(6_700_000_000, 16_666),
                super::UpstreamPlanDecision::AwaitingPair
            ));
            let repaired_audio = play(runtime.plan_audio_pts(6_700_010_000));
            assert!(matches!(
                runtime.plan_video_pts(6_700_000_000, 16_666),
                super::UpstreamPlanDecision::DropBeforeOverlap
            ));
            let repaired_video = play(runtime.plan_video_pts(6_700_026_666, 16_666));

            assert_eq!(repaired_audio.local_pts_us, 0);
            assert_eq!(repaired_video.local_pts_us, 16_666);
            assert!(
                repaired_audio.due_at > tokio::time::Instant::now(),
                "replacement stream should get a fresh playout budget instead of inheriting stale lateness"
            );
            assert!(
                repaired_video.due_at > tokio::time::Instant::now(),
                "replacement video should also get a fresh playout budget after re-pairing"
            );
        },
    );
}

#[test]
#[serial(upstream_media_runtime)]
fn closing_the_last_stream_resets_the_next_session_anchor() {
    let runtime = UpstreamMediaRuntime::new();
    let camera = runtime.activate_camera();
    let microphone = runtime.activate_microphone();
    runtime.close_camera(camera.generation);
    runtime.close_microphone(microphone.generation);

    let next = runtime.activate_camera();
    assert_eq!(next.session_id, 2);
}

#[test]
#[serial(upstream_media_runtime)]
fn first_packets_wait_for_the_counterpart_before_pairing() {
    let runtime = runtime_without_offsets();
    let _camera = runtime.activate_camera();
    let _microphone = runtime.activate_microphone();

    assert!(matches!(
        runtime.plan_video_pts(1_000_000, 16_666),
        super::UpstreamPlanDecision::AwaitingPair
    ));

    let audio_first = play(runtime.plan_audio_pts(1_000_000));
    let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));

    assert_eq!(audio_first.local_pts_us, 0);
    assert_eq!(video_first.local_pts_us, 0);
    assert_eq!(audio_first.due_at, video_first.due_at);
}

#[test]
#[serial(upstream_media_runtime)]
fn overlap_waits_for_camera_startup_grace_before_establishing_the_shared_base() {
    temp_env::with_var(
        "LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_MS",
        Some("250"),
        || {
            let runtime = UpstreamMediaRuntime::new();
            let _camera = runtime.activate_camera();
            let _microphone = runtime.activate_microphone();

            assert!(matches!(
                runtime.plan_video_pts(1_000_000, 16_666),
                super::UpstreamPlanDecision::AwaitingPair
            ));
            assert!(matches!(
                runtime.plan_audio_pts(1_000_000),
                super::UpstreamPlanDecision::AwaitingPair
            ));
            assert!(matches!(
                runtime.plan_video_pts(1_200_000, 16_666),
                super::UpstreamPlanDecision::AwaitingPair
            ));

            let video_ready = play(runtime.plan_video_pts(1_250_000, 16_666));
            let audio_ready = play(runtime.plan_audio_pts(1_260_000));

            assert_eq!(video_ready.local_pts_us, 0);
            assert_eq!(audio_ready.local_pts_us, 10_000);
        },
    );
}

#[test]
#[serial(upstream_media_runtime)]
fn pairing_window_does_not_expire_into_one_sided_playout_while_camera_warms_up() {
    temp_env::with_var(
        "LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_MS",
        Some("250"),
        || {
            temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || {
                let runtime = UpstreamMediaRuntime::new();
                let _camera = runtime.activate_camera();
                let _microphone = runtime.activate_microphone();

                assert!(matches!(
                    runtime.plan_video_pts(1_000_000, 16_666),
                    super::UpstreamPlanDecision::AwaitingPair
                ));
                assert!(matches!(
                    runtime.plan_audio_pts(1_000_000),
                    super::UpstreamPlanDecision::AwaitingPair
                ));

                std::thread::sleep(Duration::from_millis(30));

                assert!(matches!(
                    runtime.plan_audio_pts(1_010_000),
                    super::UpstreamPlanDecision::AwaitingPair
                ));

                let video_ready = play(runtime.plan_video_pts(1_250_000, 16_666));
                let audio_ready = play(runtime.plan_audio_pts(1_260_000));

                assert_eq!(video_ready.local_pts_us, 0);
                assert_eq!(audio_ready.local_pts_us, 10_000);
            });
        },
    );
}

#[test]
#[serial(upstream_media_runtime)]
fn overlap_pairing_drops_leading_packets_before_the_shared_base() {
    let runtime = UpstreamMediaRuntime::new();
    let _camera = runtime.activate_camera();
    let _microphone = runtime.activate_microphone();

    assert!(matches!(
        runtime.plan_audio_pts(1_000_000),
        super::UpstreamPlanDecision::AwaitingPair
    ));

    let video_first = play(runtime.plan_video_pts(1_300_000, 16_666));
    assert_eq!(video_first.local_pts_us, 0);

    assert!(matches!(
        runtime.plan_audio_pts(1_000_000),
        super::UpstreamPlanDecision::DropBeforeOverlap
    ));

    let audio_next = play(runtime.plan_audio_pts(1_310_000));
    let video_next = play(runtime.plan_video_pts(1_333_333, 16_666));
    assert_eq!(audio_next.local_pts_us, 10_000);
    assert_eq!(video_next.local_pts_us, 33_333);
}

#[test]
#[serial(upstream_media_runtime)]
fn shared_clock_keeps_each_kind_monotonic_when_remote_pts_repeat() {
    let runtime = UpstreamMediaRuntime::new();
    let _camera = runtime.activate_camera();
    let _microphone = runtime.activate_microphone();

    assert!(matches!(
        runtime.plan_video_pts(50_000, 16_666),
        super::UpstreamPlanDecision::AwaitingPair
    ));
    let _audio = play(runtime.plan_audio_pts(50_000));
    let first = play(runtime.plan_video_pts(50_000, 16_666));
    let repeated = play(runtime.plan_video_pts(50_000, 16_666));

    assert_eq!(first.local_pts_us, 0);
    assert_eq!(repeated.local_pts_us, 16_666);
}

#[test]
#[serial(upstream_media_runtime)]
fn close_ignores_superseded_generation_values() {
    let runtime = UpstreamMediaRuntime::new();
    let first = runtime.activate_camera();
    let second = runtime.activate_camera();
    runtime.close_camera(first.generation);

    assert!(runtime.is_camera_active(second.generation));
    runtime.close(super::UpstreamMediaKind::Camera, second.generation);
    let next = runtime.activate_camera();
    assert_eq!(next.session_id, 2);
}

#[test]
#[serial(upstream_media_runtime)]
fn soft_microphone_recovery_preserves_camera_and_shared_clock() {
    let runtime = runtime_without_offsets();
    let camera = runtime.activate_camera();
    let microphone = runtime.activate_microphone();

    assert!(matches!(
        runtime.plan_video_pts(1_000_000, 16_666),
        super::UpstreamPlanDecision::AwaitingPair
    ));
    let audio_first = play(runtime.plan_audio_pts(1_000_000));
    let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
    assert_eq!(audio_first.local_pts_us, video_first.local_pts_us);

    runtime.soft_recover_microphone();

    assert!(runtime.is_camera_active(camera.generation));
    assert!(!runtime.is_microphone_active(microphone.generation));
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_time()
        .build()
        .expect("runtime");
    assert!(
        rt.block_on(runtime.wait_for_audio_master(video_first.local_pts_us, video_first.due_at)),
        "video should not wait on a soft-recovered microphone slot"
    );

    let next_microphone = runtime.activate_microphone();
    assert_eq!(next_microphone.session_id, camera.session_id);
    let recovered_audio = play(runtime.plan_audio_pts(1_010_000));
    assert_eq!(recovered_audio.local_pts_us, 10_000);
}
