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

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn wait_for_audio_master_releases_video_once_audio_catches_up() {
    let runtime = Arc::new(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));
    runtime.mark_audio_presented(audio_first.local_pts_us, audio_first.due_at);
    let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));

    let waiter = tokio::spawn({
        let runtime = runtime.clone();
        async move {
            runtime
                .wait_for_audio_master(video_first.local_pts_us + 10_000, video_first.due_at)
                .await
        }
    });

    tokio::time::sleep(Duration::from_millis(5)).await;
    let audio_next = play(runtime.plan_audio_pts(1_010_000));
    runtime.mark_audio_presented(audio_next.local_pts_us, audio_next.due_at);

    assert!(waiter.await.expect("audio master waiter should finish"));
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn wait_for_audio_master_allows_configured_positive_audio_delay() {
    let runtime = Arc::new(UpstreamMediaRuntime::new());
    runtime.set_playout_offsets(0, 720_000);
    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));
    runtime.mark_audio_presented(audio_first.local_pts_us, audio_first.due_at);
    let _video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
    let delayed_video = play(runtime.plan_video_pts(1_700_000, 16_666));

    assert_eq!(delayed_video.local_pts_us, 700_000);
    assert!(
        runtime
            .wait_for_audio_master(
                delayed_video.local_pts_us,
                tokio::time::Instant::now() + Duration::from_millis(5)
            )
            .await,
        "a positive audio playout offset should not freeze UVC video while the audio sink sleeps"
    );
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn wait_for_audio_master_times_out_when_audio_never_catches_up() {
    let runtime = Arc::new(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));
    runtime.mark_audio_presented(audio_first.local_pts_us, audio_first.due_at);
    let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));

    let due_at = tokio::time::Instant::now() + Duration::from_millis(20);
    assert!(
        !runtime
            .wait_for_audio_master(video_first.local_pts_us + 100_000, due_at)
            .await
    );
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn wait_for_audio_master_keeps_video_waiting_through_sync_grace() {
    let runtime = Arc::new(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));
    runtime.mark_audio_presented(audio_first.local_pts_us, audio_first.due_at);
    let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));

    let waiter = tokio::spawn({
        let runtime = runtime.clone();
        async move {
            runtime
                .wait_for_audio_master(
                    video_first.local_pts_us + 120_000,
                    tokio::time::Instant::now()
                        .checked_sub(Duration::from_millis(100))
                        .unwrap_or_else(tokio::time::Instant::now),
                )
                .await
        }
    });

    tokio::time::sleep(Duration::from_millis(5)).await;
    let audio_next = play(runtime.plan_audio_pts(1_120_000));
    runtime.mark_audio_presented(audio_next.local_pts_us, audio_next.due_at);

    assert!(
        waiter
            .await
            .expect("audio master waiter should finish inside sync grace")
    );
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn wait_for_audio_master_returns_true_when_no_microphone_stream_is_active() {
    let runtime = Arc::new(UpstreamMediaRuntime::new());
    let camera = runtime.activate_camera();
    let microphone = runtime.activate_microphone();
    runtime.close_microphone(microphone.generation);

    assert!(runtime.is_camera_active(camera.generation));
    assert!(
        runtime
            .wait_for_audio_master(
                123_456,
                tokio::time::Instant::now() + Duration::from_millis(10)
            )
            .await
    );
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn new_microphone_owner_waits_for_the_previous_sink_to_release() {
    let runtime = Arc::new(UpstreamMediaRuntime::new());
    let first = runtime.activate_microphone();
    let first_permit = runtime
        .reserve_microphone_sink(first.generation)
        .await
        .expect("first owner should acquire the sink gate");
    let second = runtime.activate_microphone();

    let waiter = tokio::spawn({
        let runtime = runtime.clone();
        async move {
            runtime
                .reserve_microphone_sink(second.generation)
                .await
                .is_some()
        }
    });

    tokio::time::sleep(Duration::from_millis(25)).await;
    assert!(!waiter.is_finished());

    drop(first_permit);
    assert!(waiter.await.expect("waiter task should finish"));
}

#[tokio::test(flavor = "current_thread")]
#[serial(upstream_media_runtime)]
async fn superseded_microphone_waiter_stands_down_before_opening_a_sink() {
    let runtime = Arc::new(UpstreamMediaRuntime::new());
    let first = runtime.activate_microphone();
    let first_permit = runtime
        .reserve_microphone_sink(first.generation)
        .await
        .expect("first owner should acquire the sink gate");
    let second = runtime.activate_microphone();

    let superseded_waiter = tokio::spawn({
        let runtime = runtime.clone();
        async move {
            runtime
                .reserve_microphone_sink(second.generation)
                .await
                .is_some()
        }
    });

    tokio::time::sleep(Duration::from_millis(25)).await;
    let _third = runtime.activate_microphone();
    drop(first_permit);

    assert!(
        !superseded_waiter
            .await
            .expect("superseded waiter task should finish"),
        "older waiter should stand down instead of opening a sink after supersession"
    );
}
