/*
 * Copyright 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.webgpu

import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import androidx.webgpu.helper.WebGpu
import androidx.webgpu.helper.createWebGpu
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.Executors
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test

@Suppress("UNUSED_VARIABLE")
@SmallTest
class RenderBundleEncoderTest {
  private val dispatcher: CoroutineDispatcher = Executors.newSingleThreadExecutor { runnable ->
    Thread(runnable, "Test-WebGPU-Thread")
  }.asCoroutineDispatcher()
  private val testScope = CoroutineScope(dispatcher)
  private lateinit var device: GPUDevice
  private lateinit var webGpu: WebGpu
  private lateinit var defaultColorPipeline: GPURenderPipeline
  private lateinit var shaderModule: GPUShaderModule
  private lateinit var layout: GPUPipelineLayout
  private val kColorFormat = TextureFormat.RGBA8Unorm

  @Before
  fun setup(): Unit = runBlocking {
    val gpu = createWebGpu(dispatcher)
    webGpu = gpu
    device = gpu.device
    testScope.launch {
      webGpu.processEventsLoop()
    }

    val unused = webGpu.execute {
      shaderModule = device.createShaderModule(
        GPUShaderModuleDescriptor(
          shaderSourceWGSL = GPUShaderSourceWGSL(
            """@vertex fn vsMain() -> @builtin(position) vec4<f32> {
                          return vec4<f32>(0.0, 0.0, 0.0, 1.0);
                      }
                      @fragment fn fsMain() -> @location(0) vec4<f32> {
                          return vec4<f32>(1.0, 0.0, 0.0, 1.0);
                      }
                   """.trimIndent()
          )
        )
      )

      layout = device.createPipelineLayout(GPUPipelineLayoutDescriptor())

      defaultColorPipeline = device.createRenderPipeline(
        GPURenderPipelineDescriptor(
          layout = layout,
          vertex = GPUVertexState(module = shaderModule, entryPoint = "vsMain"),
          fragment = GPUFragmentState(
            module = shaderModule,
            entryPoint = "fsMain",
            targets = arrayOf(
              GPUColorTargetState(format = kColorFormat)
            )
          ),
          primitive = GPUPrimitiveState(topology = PrimitiveTopology.TriangleList),
        )
      )
    }
  }

  @After
  fun teardown() {
    if (::webGpu.isInitialized) {
      webGpu.close()
    }
    testScope.cancel()
    (dispatcher as? ExecutorCoroutineDispatcher)?.close()
  }

  companion object {
    private const val BIND_GROUP_SHADER_CODE = """
            @group(0) @binding(0) var<uniform> u : f32;
            @vertex fn vs() -> @builtin(position) vec4f { return vec4f(0,0,0,1); }
            @fragment fn fs() -> @location(0) vec4f { return vec4f(u,0,0,1); }
        """
  }

  /** Helper to create a GPURenderBundleEncoder with default color format. */
  private fun createDefaultBundleEncoder(): GPURenderBundleEncoder {
    return device.createRenderBundleEncoder(
      GPURenderBundleEncoderDescriptor(
        colorFormats = intArrayOf(kColorFormat)
      )
    )
  }

  /** Helper to create an index buffer with padding. */
  private fun createIndexBuffer(indices: ShortArray): GPUBuffer {
    val dataSize = (indices.size * Short.SIZE_BYTES).toLong()
    val paddedSize = (dataSize + 3) and -4L
    val buffer = device.createBuffer(
      GPUBufferDescriptor(
        size = paddedSize,
        usage = BufferUsage.Index or BufferUsage.CopyDst
      )
    )
    val data = ByteBuffer.allocateDirect(paddedSize.toInt()).order(ByteOrder.nativeOrder())
    data.asShortBuffer().put(indices)
    device.queue.writeBuffer(buffer, 0, data)
    return buffer
  }

  /** Helper to create an indirect buffer. */
  private fun createIndirectBuffer(data: IntArray): GPUBuffer {
    val byteBuffer = ByteBuffer.allocateDirect(data.size * Int.SIZE_BYTES)
      .order(ByteOrder.nativeOrder())
    byteBuffer.asIntBuffer().put(data)
    val buffer = device.createBuffer(
      GPUBufferDescriptor(
        size = byteBuffer.capacity().toLong(),
        usage = BufferUsage.Indirect or BufferUsage.CopyDst
      )
    )
    device.queue.writeBuffer(buffer, 0, byteBuffer)
    return buffer
  }

  @Test
  fun testInsertDebugMarker() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.insertDebugMarker("Marker Inside Bundle")
        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val error = device.popErrorScope()
        assertEquals(ErrorType.NoError, error)
      }
    }
  }

  @Test
  fun testPopDebugGroupWithoutPushFails() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.popDebugGroup()  // Invalid call.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()  // Deferred error caught here.
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
      }
    }
  }

  @Test
  fun testPushAndPopDebugGroupSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.pushDebugGroup("BundleGroup")
        bundleEncoder.popDebugGroup()  // Valid pair.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()  // Should succeed.
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
      }
    }
  }

  @Test
  fun testDrawWithoutPipelineFails() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.draw(3)  // Invalid: pipeline not set.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
      }
    }
  }

  @Test
  fun testDrawWithPipelineSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)  // Valid.
        bundleEncoder.draw(3)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
      }
    }
  }

  @Test
  fun testSetVertexBufferInvalidUsageFails() {
    runBlocking {
      val unused = webGpu.execute {
        val invalidBuffer = device.createBuffer(
          GPUBufferDescriptor(size = 16, usage = BufferUsage.CopyDst)
        )
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setVertexBuffer(0, invalidBuffer)  // Invalid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
        invalidBuffer.destroy()
      }
    }
  }

  @Test
  fun testSetVertexBufferValidSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val validBuffer = device.createBuffer(
          GPUBufferDescriptor(size = 16, usage = BufferUsage.Vertex)
        )
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setVertexBuffer(0, validBuffer)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
        validBuffer.destroy()
      }
    }
  }

  @Test
  fun testDrawIndexedWithoutIndexBufferFails() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.drawIndexed(3)  // Invalid: index buffer not set.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
      }
    }
  }

  @Test
  fun testDrawIndexedValidSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val indexBuffer = createIndexBuffer(shortArrayOf(0, 1, 2))
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.setIndexBuffer(indexBuffer, IndexFormat.Uint16)  // Valid.
        bundleEncoder.drawIndexed(3)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        val bundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
        indexBuffer.destroy()
        bundle.close()
      }
    }
  }

  @Test
  fun testDrawIndirectInvalidBufferFails() {
    runBlocking {
      val unused = webGpu.execute {
        val invalidBuffer = device.createBuffer(
          GPUBufferDescriptor(size = 16, usage = BufferUsage.CopyDst)
        )
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.drawIndirect(invalidBuffer, 0)  // Invalid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
        invalidBuffer.destroy()
      }
    }
  }

  @Test
  fun testDrawIndirectValidSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val indirectBuffer = createIndirectBuffer(intArrayOf(3, 1, 0, 0))
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.drawIndirect(indirectBuffer, 0)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        val bundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
        indirectBuffer.destroy()
        bundle.close()
      }
    }
  }

  @Test
  fun testDrawIndexedIndirectWithoutIndexBufferFails() {
    runBlocking {
      val unused = webGpu.execute {
        val indirectBuffer = createIndirectBuffer(intArrayOf(3, 1, 0, 0, 0))
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.drawIndexedIndirect(indirectBuffer, 0)  // Invalid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()

        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
        indirectBuffer.destroy()
      }
    }
  }

  @MediumTest
  @Test
  fun testDrawIndexedIndirectValidSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        val indirectBuffer = createIndirectBuffer(intArrayOf(3, 1, 0, 0, 0))
        val indexBuffer = createIndexBuffer(shortArrayOf(0, 1, 2))
        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(defaultColorPipeline)
        bundleEncoder.setIndexBuffer(indexBuffer, IndexFormat.Uint16)  // Valid.
        bundleEncoder.drawIndexedIndirect(indirectBuffer, 0)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
        indirectBuffer.destroy()
        indexBuffer.destroy()
      }
    }
  }

  @Test
  fun testDrawWithBindGroupRequiredButNotSetFails() {
    runBlocking {
      val unused = webGpu.execute {
        // Create pipeline requiring a bind group locally
        val bgl = device.createBindGroupLayout(
          GPUBindGroupLayoutDescriptor(
            entries = arrayOf(
              GPUBindGroupLayoutEntry(
                0,
                ShaderStage.Fragment,
                buffer = GPUBufferBindingLayout(BufferBindingType.Uniform)
              )
            )
          )
        )
        val layout =
          device.createPipelineLayout(GPUPipelineLayoutDescriptor(bindGroupLayouts = arrayOf(bgl)))
        val module = device.createShaderModule(
          GPUShaderModuleDescriptor(
            shaderSourceWGSL = GPUShaderSourceWGSL(BIND_GROUP_SHADER_CODE)
          )
        )
        val bgPipeline = device.createRenderPipeline(
          GPURenderPipelineDescriptor(
            layout = layout,
            vertex = GPUVertexState(module, "vs"),
            fragment = GPUFragmentState(
              module,
              "fs",
              targets = arrayOf(GPUColorTargetState(kColorFormat))
            ),
            primitive = GPUPrimitiveState(topology = PrimitiveTopology.TriangleList)
          )
        )

        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(bgPipeline)  // Requires bind group 0.
        bundleEncoder.draw(3)  // Invalid: Bind group 0 not set.

        device.pushErrorScope(ErrorFilter.Validation)
        val unusedRenderBundle = bundleEncoder.finish()
        val unusedAssert = assertThrowsSuspend(ValidationException::class.java) {
          val unusedPop = device.popErrorScope()
        }
      }
    }
  }

  /**
   * Verifies that recording draw commands in a RenderBundle succeeds
   * when a required BindGroup is correctly set.
   */
  @MediumTest
  @Test
  fun testDrawWithBindGroupSetSucceeds() {
    runBlocking {
      val unused = webGpu.execute {
        // Create the uniform buffer resource needed by the shader.
        val buffer = device.createBuffer(GPUBufferDescriptor(size = 4, usage = BufferUsage.Uniform))

        // Define the layout for the bind group, matching the shader.
        val bgl = device.createBindGroupLayout(
          GPUBindGroupLayoutDescriptor(
            entries = arrayOf(
              GPUBindGroupLayoutEntry(
                0,
                ShaderStage.Fragment,
                buffer = GPUBufferBindingLayout(BufferBindingType.Uniform)
              )
            )
          )
        )

        // Create the actual bind group, linking the buffer to binding 0 according to the layout.
        val bindGroup = device.createBindGroup(
          GPUBindGroupDescriptor(
            layout = bgl,
            entries = arrayOf(GPUBindGroupEntry(0, buffer))
          )
        )

        // Define the pipeline to expect the bind group layout 'bgl' at index 0.
        val layout =
          device.createPipelineLayout(GPUPipelineLayoutDescriptor(bindGroupLayouts = arrayOf(bgl)))
        val module = device.createShaderModule(
          GPUShaderModuleDescriptor(
            shaderSourceWGSL = GPUShaderSourceWGSL(BIND_GROUP_SHADER_CODE)
          )
        )
        // Create the render pipeline, linking the shader, layout, and required state.
        val bgPipeline = device.createRenderPipeline(
          GPURenderPipelineDescriptor(
            layout = layout,
            vertex = GPUVertexState(module, "vs"),
            fragment = GPUFragmentState(
              module,
              "fs",
              targets = arrayOf(GPUColorTargetState(kColorFormat))
            ),
            primitive = GPUPrimitiveState(topology = PrimitiveTopology.TriangleList)
          )
        )

        val bundleEncoder = createDefaultBundleEncoder()
        bundleEncoder.setPipeline(bgPipeline)
        bundleEncoder.setBindGroup(0, bindGroup)  // Valid.
        bundleEncoder.draw(3)  // Valid.

        device.pushErrorScope(ErrorFilter.Validation)
        // Finish recording. Validation occurs here.
        val unusedRenderBundle = bundleEncoder.finish()
        val error = device.popErrorScope()

        assertEquals(ErrorType.NoError, error)
      }
    }
  }

  @Test
  fun testOperationAfterFinishFails() {
    runBlocking {
      val unused = webGpu.execute {
        val bundleEncoder = createDefaultBundleEncoder()
        val bundle = bundleEncoder.finish()  // Encoder is now consumed.
        bundle.close()  // Close the bundle itself.

        assertThrows(ValidationException::class.java) {
          bundleEncoder.draw(3)
        }
      }
    }
  }
}