use std::num::NonZeroU64;

use wgpu::{
    BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
    BindingResource, BindingType, BufferBindingType, BufferDescriptor, BufferUsages, CommandBuffer,
    CommandEncoderDescriptor, ComputePassDescriptor, PollType, ShaderStages,
};

use wgpu_test::{gpu_test, GpuTestConfiguration, TestingContext};

fn generate_dummy_work(ctx: &TestingContext) -> CommandBuffer {
    let buffer = ctx.device.create_buffer(&BufferDescriptor {
        label: None,
        size: 16,
        usage: BufferUsages::UNIFORM,
        mapped_at_creation: false,
    });

    let bind_group_layout = ctx
        .device
        .create_bind_group_layout(&BindGroupLayoutDescriptor {
            label: None,
            entries: &[BindGroupLayoutEntry {
                binding: 0,
                visibility: ShaderStages::COMPUTE,
                ty: BindingType::Buffer {
                    ty: BufferBindingType::Uniform,
                    has_dynamic_offset: false,
                    min_binding_size: Some(NonZeroU64::new(16).unwrap()),
                },
                count: None,
            }],
        });

    let bind_group = ctx.device.create_bind_group(&BindGroupDescriptor {
        label: None,
        layout: &bind_group_layout,
        entries: &[BindGroupEntry {
            binding: 0,
            resource: BindingResource::Buffer(buffer.as_entire_buffer_binding()),
        }],
    });

    let mut cmd_buf = ctx
        .device
        .create_command_encoder(&CommandEncoderDescriptor::default());

    let mut cpass = cmd_buf.begin_compute_pass(&ComputePassDescriptor::default());
    cpass.set_bind_group(0, &bind_group, &[]);
    drop(cpass);

    cmd_buf.finish()
}

#[gpu_test]
static WAIT: GpuTestConfiguration = GpuTestConfiguration::new().run_async(|ctx| async move {
    let cmd_buf = generate_dummy_work(&ctx);

    ctx.queue.submit(Some(cmd_buf));
    ctx.async_poll(PollType::wait()).await.unwrap();
});

#[gpu_test]
static DOUBLE_WAIT: GpuTestConfiguration =
    GpuTestConfiguration::new().run_async(|ctx| async move {
        let cmd_buf = generate_dummy_work(&ctx);

        ctx.queue.submit(Some(cmd_buf));
        ctx.async_poll(PollType::wait()).await.unwrap();
        ctx.async_poll(PollType::wait()).await.unwrap();
    });

#[gpu_test]
static WAIT_ON_SUBMISSION: GpuTestConfiguration =
    GpuTestConfiguration::new().run_async(|ctx| async move {
        let cmd_buf = generate_dummy_work(&ctx);

        let index = ctx.queue.submit(Some(cmd_buf));
        ctx.async_poll(PollType::wait_for(index)).await.unwrap();
    });

#[gpu_test]
static DOUBLE_WAIT_ON_SUBMISSION: GpuTestConfiguration =
    GpuTestConfiguration::new().run_async(|ctx| async move {
        let cmd_buf = generate_dummy_work(&ctx);

        let index = ctx.queue.submit(Some(cmd_buf));
        ctx.async_poll(PollType::wait_for(index.clone()))
            .await
            .unwrap();
        ctx.async_poll(PollType::wait_for(index)).await.unwrap();
    });

#[gpu_test]
static WAIT_OUT_OF_ORDER: GpuTestConfiguration =
    GpuTestConfiguration::new().run_async(|ctx| async move {
        let cmd_buf1 = generate_dummy_work(&ctx);
        let cmd_buf2 = generate_dummy_work(&ctx);

        let index1 = ctx.queue.submit(Some(cmd_buf1));
        let index2 = ctx.queue.submit(Some(cmd_buf2));
        ctx.async_poll(PollType::wait_for(index2)).await.unwrap();
        ctx.async_poll(PollType::wait_for(index1)).await.unwrap();
    });

/// Submit a command buffer to the wrong device. A wait poll shouldn't hang.
///
/// We can't catch panics on Wasm, since they get reported directly to the
/// console.
#[gpu_test]
static WAIT_AFTER_BAD_SUBMISSION: GpuTestConfiguration = GpuTestConfiguration::new()
    .parameters(wgpu_test::TestParameters::default().skip(wgpu_test::FailureCase::webgl2()))
    .run_async(wait_after_bad_submission);

async fn wait_after_bad_submission(ctx: TestingContext) {
    let (device2, queue2) =
        wgpu_test::initialize_device(&ctx.adapter, ctx.device_features, ctx.device_limits.clone())
            .await;

    let command_buffer1 = ctx
        .device
        .create_command_encoder(&CommandEncoderDescriptor::default())
        .finish();

    // This should panic, since the command buffer belongs to the wrong
    // device, and queue submission errors seem to be fatal errors?
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        queue2.submit([command_buffer1]);
    }));
    assert!(result.is_err());

    // This should not hang.
    //
    // Specifically, the failed submission should not cause a new fence value to
    // be allocated that will not be signalled until further work is
    // successfully submitted, causing a greater fence value to be signalled.
    device2.poll(wgpu::PollType::Wait).unwrap();
}
