// GPU device integration tests — require a real GPU adapter.
// Tests create a headless wgpu device and validate shader compilation,
// buffer operations, compute dispatch, and capability detection.
// Tests skip gracefully when no GPU is available (e.g., headless CI).
// Per CODESPEC §1.1: release profile for performance evaluation.
// Per LINTSPEC §4.6: explicit, actionable test names.

use wgpu::util::DeviceExt;

// ── Shader Source Loading ────────────────────────────────────────────────

const GRID_WGSL: &str = include_str!("../shaders/grid.wgsl");
const AXES_WGSL: &str = include_str!("../shaders/axes.wgsl");
const PARTICLE_WGSL: &str = include_str!("../shaders/particle.wgsl");
const PRIMITIVE_WGSL: &str = include_str!("../shaders/primitive.wgsl");
const PROCEDURAL_WGSL: &str = include_str!("../shaders/procedural.wgsl");
const SPRITE_WGSL: &str = include_str!("../shaders/sprite.wgsl");
const TWEEN_WGSL: &str = include_str!("../shaders/tween.wgsl");

// Physics shaders (require physics_common.wgsl prefix for type definitions).
const PHYSICS_COMMON: &str = include_str!("../shaders/physics/physics_common.wgsl");
const PARTICLE_COLLISION: &str = include_str!("../shaders/physics/particle_collision.wgsl");
const PARTICLE_COMPACT: &str = include_str!("../shaders/physics/particle_compact.wgsl");
const PARTICLE_INDIRECT: &str = include_str!("../shaders/physics/particle_indirect.wgsl");
const PARTICLE_SIMULATE: &str = include_str!("../shaders/physics/particle_simulate.wgsl");
const PARTICLE_SPAWN: &str = include_str!("../shaders/physics/particle_spawn.wgsl");
const PARTICLE_PROMOTE: &str = include_str!("../shaders/physics/particle_promote.wgsl");
const PARTICLE_BILLBOARD: &str = include_str!("../shaders/physics/particle_billboard.wgsl");
const PARTICLE_BEAM: &str = include_str!("../shaders/physics/particle_beam.wgsl");
const PARTICLE_MESH: &str = include_str!("../shaders/physics/particle_mesh.wgsl");
const PARTICLE_MESHLET: &str = include_str!("../shaders/physics/particle_meshlet.wgsl");
const PARTICLE_POINTCLOUD: &str = include_str!("../shaders/physics/particle_pointcloud.wgsl");
const PARTICLE_RIBBON: &str = include_str!("../shaders/physics/particle_ribbon.wgsl");
const PARTICLE_SHARD: &str = include_str!("../shaders/physics/particle_shard.wgsl");
const PARTICLE_STRETCHED: &str = include_str!("../shaders/physics/particle_stretched.wgsl");

// ── Headless GPU Helper ──────────────────────────────────────────────────

/// Attempt to create a headless wgpu device. Returns None if no GPU is available.
fn try_gpu() -> Option<(wgpu::Device, wgpu::Queue, wgpu::Adapter)> {
    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
    let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
        power_preference: wgpu::PowerPreference::HighPerformance,
        compatible_surface: None,
        force_fallback_adapter: false,
    }))
    .ok()?;

    let (features, limits, experimental) = dreamwell_gpu::features::GpuCapabilities::device_descriptor_parts(&adapter);
    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
        label: Some("dreamwell-gpu-test"),
        required_features: features,
        required_limits: limits,
        memory_hints: Default::default(),
        experimental_features: experimental,
        trace: Default::default(),
    }))
    .ok()?;

    Some((device, queue, adapter))
}

macro_rules! require_gpu {
    () => {
        match try_gpu() {
            Some(gpu) => gpu,
            None => {
                eprintln!("gpu: no adapter available — skipping device test");
                return;
            }
        }
    };
}

/// Compile a shader and assert no validation errors via error scopes.
fn compile_shader(device: &wgpu::Device, label: &str, source: &str) {
    let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
    let _module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some(label),
        source: wgpu::ShaderSource::Wgsl(source.into()),
    });
    let error = pollster::block_on(scope.pop());
    assert!(error.is_none(), "shader {}: compilation failed: {:?}", label, error);
}

// ── GPU Adapter Detection ────────────────────────────────────────────────

#[test]
fn gpu_adapter_detection() {
    let (_, _, adapter) = require_gpu!();
    let caps = dreamwell_gpu::features::GpuCapabilities::from_adapter(&adapter);
    let info = adapter.get_info();
    eprintln!(
        "gpu: adapter={}, backend={:?}, driver={}",
        info.name, info.backend, info.driver_info
    );
    eprintln!("gpu: capabilities={}", caps.summary());
    assert!(!caps.adapter_name.is_empty());
}

#[test]
fn gpu_capabilities_report() {
    let (_, _, adapter) = require_gpu!();
    let caps = dreamwell_gpu::features::GpuCapabilities::from_adapter(&adapter);
    // Adapter must have a valid backend.
    let backend = format!("{:?}", caps.backend);
    assert!(
        ["Vulkan", "Dx12", "Metal", "Gl", "BrowserWebGpu"].contains(&backend.as_str()),
        "Unexpected backend: {}",
        backend
    );
    eprintln!(
        "gpu: ray_tracing={}, mesh_shader={}",
        caps.ray_tracing, caps.mesh_shader
    );
    eprintln!(
        "gpu: depth_clip_control={}, timestamp_query={}",
        caps.depth_clip_control, caps.timestamp_query
    );
    eprintln!("gpu: immediates={} (max={})", caps.immediates, caps.max_immediate_size);
}

// ── Core Shader Compilation (semantic validation via naga) ───────────────

#[test]
fn gpu_compile_grid_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "grid", GRID_WGSL);
}

#[test]
fn gpu_compile_axes_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "axes", AXES_WGSL);
}

#[test]
fn gpu_compile_primitive_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "primitive", PRIMITIVE_WGSL);
}

#[test]
fn gpu_compile_particle_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle", PARTICLE_WGSL);
}

#[test]
fn gpu_compile_procedural_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "procedural", PROCEDURAL_WGSL);
}

#[test]
fn gpu_compile_sprite_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "sprite", SPRITE_WGSL);
}

#[test]
fn gpu_compile_tween_shader() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "tween", TWEEN_WGSL);
}

// ── Physics Shader Compilation (composed with physics_common prefix) ─────

fn compose_physics(shader: &str) -> String {
    format!("{}\n{}", PHYSICS_COMMON, shader)
}

#[test]
fn gpu_compile_physics_collision() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_collision", &compose_physics(PARTICLE_COLLISION));
}

#[test]
fn gpu_compile_physics_compact() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_compact", &compose_physics(PARTICLE_COMPACT));
}

#[test]
fn gpu_compile_physics_indirect() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_indirect", &compose_physics(PARTICLE_INDIRECT));
}

#[test]
fn gpu_compile_physics_simulate() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_simulate", &compose_physics(PARTICLE_SIMULATE));
}

#[test]
fn gpu_compile_physics_spawn() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_spawn", &compose_physics(PARTICLE_SPAWN));
}

#[test]
fn gpu_compile_physics_promote() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_promote", &compose_physics(PARTICLE_PROMOTE));
}

#[test]
fn gpu_compile_physics_billboard() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_billboard", &compose_physics(PARTICLE_BILLBOARD));
}

#[test]
fn gpu_compile_physics_beam() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_beam", &compose_physics(PARTICLE_BEAM));
}

#[test]
fn gpu_compile_physics_mesh() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_mesh", &compose_physics(PARTICLE_MESH));
}

#[test]
fn gpu_compile_physics_meshlet() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_meshlet", &compose_physics(PARTICLE_MESHLET));
}

#[test]
fn gpu_compile_physics_pointcloud() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_pointcloud", &compose_physics(PARTICLE_POINTCLOUD));
}

#[test]
fn gpu_compile_physics_ribbon() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_ribbon", &compose_physics(PARTICLE_RIBBON));
}

#[test]
fn gpu_compile_physics_shard() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_shard", &compose_physics(PARTICLE_SHARD));
}

#[test]
fn gpu_compile_physics_stretched() {
    let (device, _, _) = require_gpu!();
    compile_shader(&device, "particle_stretched", &compose_physics(PARTICLE_STRETCHED));
}

// ── Buffer Operations ────────────────────────────────────────────────────

#[test]
fn gpu_create_vertex_buffer() {
    let (device, _, _) = require_gpu!();
    let vertices: Vec<dreamwell_gpu::primitives::PrimitiveVertex> = vec![
        dreamwell_gpu::primitives::PrimitiveVertex {
            position: [0.0, 0.0, 0.0],
            normal: [0.0, 1.0, 0.0],
            color: [1.0, 1.0, 1.0, 1.0],
        },
        dreamwell_gpu::primitives::PrimitiveVertex {
            position: [1.0, 0.0, 0.0],
            normal: [0.0, 1.0, 0.0],
            color: [1.0, 0.0, 0.0, 1.0],
        },
        dreamwell_gpu::primitives::PrimitiveVertex {
            position: [0.0, 1.0, 0.0],
            normal: [0.0, 1.0, 0.0],
            color: [0.0, 1.0, 0.0, 1.0],
        },
    ];
    let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("test_vertex_buffer"),
        contents: bytemuck::cast_slice(&vertices),
        usage: wgpu::BufferUsages::VERTEX,
    });
    assert_ne!(buffer.size(), 0);
    assert_eq!(buffer.size(), (3 * 40) as u64);
}

#[test]
fn gpu_create_uniform_buffer() {
    let (device, _, _) = require_gpu!();
    let data = [0u8; 256];
    let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("test_uniform_buffer"),
        contents: &data,
        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
    });
    assert_eq!(buffer.size(), 256);
}

#[test]
fn gpu_create_storage_buffer() {
    let (device, queue, _) = require_gpu!();

    // Write data to storage buffer.
    let input: Vec<u32> = (0..64).collect();
    let storage_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("test_storage"),
        contents: bytemuck::cast_slice(&input),
        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
    });
    assert_eq!(storage_buf.size(), 64 * 4);

    // Create staging buffer for readback.
    let staging_buf = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("test_staging"),
        size: (64 * 4) as u64,
        usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
        mapped_at_creation: false,
    });
    assert_eq!(staging_buf.size(), 64 * 4);

    // Copy storage → staging and submit.
    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
        label: Some("test_readback"),
    });
    encoder.copy_buffer_to_buffer(&storage_buf, 0, &staging_buf, 0, (64 * 4) as u64);
    queue.submit(std::iter::once(encoder.finish()));
}

// ── Primitive Mesh GPU Upload ────────────────────────────────────────────

#[test]
fn gpu_upload_all_primitives() {
    let (device, _, _) = require_gpu!();
    use dreamwell_gpu::primitives::*;

    for shape in PrimitiveShape::ALL {
        let mesh = shape.generate(DEFAULT_PRIMITIVE_COLOR);
        assert!(!mesh.vertices.is_empty(), "Shape {} has 0 vertices", shape.name());

        let vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some(shape.name()),
            contents: bytemuck::cast_slice(&mesh.vertices),
            usage: wgpu::BufferUsages::VERTEX,
        });
        assert!(vb.size() > 0, "Shape {} vertex buffer empty", shape.name());

        let ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some(shape.name()),
            contents: bytemuck::cast_slice(&mesh.indices),
            usage: wgpu::BufferUsages::INDEX,
        });
        assert!(ib.size() > 0, "Shape {} index buffer empty", shape.name());
    }
}

// ── Feature-Gated Tests ─────────────────────────────────────────────────

#[test]
fn gpu_timestamp_query_support() {
    let (_, _, adapter) = require_gpu!();
    let caps = dreamwell_gpu::features::GpuCapabilities::from_adapter(&adapter);
    // Just report — not all GPUs support timestamp queries.
    eprintln!("gpu: timestamp_query={}", caps.timestamp_query);
}

#[test]
fn gpu_device_limits_within_spec() {
    let (device, _, _) = require_gpu!();
    let limits = device.limits();
    // Per wgpu defaults, these minimums must hold.
    assert!(limits.max_texture_dimension_2d >= 2048);
    assert!(limits.max_buffer_size >= 256 * 1024 * 1024);
    assert!(limits.max_bind_groups >= 4);
    assert!(limits.max_compute_workgroup_size_x >= 256);
    eprintln!(
        "gpu: max_texture_2d={}, max_buffer={}MB, max_compute_wg_x={}",
        limits.max_texture_dimension_2d,
        limits.max_buffer_size / (1024 * 1024),
        limits.max_compute_workgroup_size_x,
    );
}

// ── Animation Batch Throughput ──────────────────────────────────────────

#[test]
fn gpu_animation_build_inputs_throughput() {
    use dreamwell_gpu::animation::*;
    use dreamwell_gpu::tween::TWEEN_LINEAR;

    let mut batch = AnimationBatch::default();

    // Create a clip with many keyframes per track.
    let keyframes: Vec<Keyframe> = (0..1000)
        .map(|i| Keyframe {
            time_ms: i * 10,
            value: [i as f32, 0.0, 0.0, 1.0],
        })
        .collect();

    let clip = AnimationClip {
        name: "stress".into(),
        duration_ms: 10000,
        tracks: vec![
            AnimationTrack {
                property: "pos_x".into(),
                keyframes: keyframes.clone(),
                tween_mode: TWEEN_LINEAR,
            },
            AnimationTrack {
                property: "pos_y".into(),
                keyframes: keyframes.clone(),
                tween_mode: TWEEN_LINEAR,
            },
            AnimationTrack {
                property: "rot_z".into(),
                keyframes: keyframes.clone(),
                tween_mode: TWEEN_LINEAR,
            },
            AnimationTrack {
                property: "scale".into(),
                keyframes,
                tween_mode: TWEEN_LINEAR,
            },
        ],
        looping: true,
    };

    let clip_idx = batch.add_clip(clip);

    // Spawn 100 simultaneous instances.
    for t in 0..100 {
        batch.play(clip_idx, t * 50).expect("under capacity");
    }

    let start = std::time::Instant::now();
    let iterations = 1000;
    let mut inputs = Vec::new();
    for i in 0..iterations {
        batch.build_inputs(5000 + i * 10, &mut inputs);
        assert_eq!(inputs.len(), 400); // 100 instances × 4 tracks
    }
    let elapsed = start.elapsed();
    let per_call_us = elapsed.as_micros() as f64 / iterations as f64;
    eprintln!(
        "gpu: animation build_inputs: {} calls in {:.1}ms ({:.1}us/call, {} inputs/call)",
        iterations,
        elapsed.as_secs_f64() * 1000.0,
        per_call_us,
        400
    );
    // Throughput sanity: must complete in reasonable time (< 100us per call).
    assert!(per_call_us < 1000.0, "build_inputs too slow: {:.0}us/call", per_call_us);
}
