"""
Regression tests for HIP/AMD codegen fixes in TileLang.

Covers seven bug fixes across five source files:

  Fix 1 (reduce.h)            warp_reduce 5-step butterfly with width=32
  Fix 2 (codegen_hip.cc,      ShuffleNode bfloat16x2/float16x2 packing;
          common.h)            uint1 bf16x2 math overloads
  Fix 3 (allocate.py,         T.alloc_var(init=<literal>) emits a correctly
          codegen_hip.cc)      initialised scalar on HIP
  Fix 4 (codegen_hip.cc)      T.sync_warp() lowered to no-op on HIP
  Fix 5 (codegen_hip.cc,      T.sync_grid() lowered to cooperative groups
          rt_mod_hip.cc,       grid barrier; runtime launch infrastructure
          stubs/)              added
  Fix 6 (pipeline_planning.cc) T.Pipelined(num_stages>1) falls back to a
                               plain sequential loop on ROCM to avoid LDS
                               overflow (hipModuleLaunchKernel EINVAL)
  Fix 7 (codegen_hip.cc)      Packed int4 shared allocations round their
                              int32 storage-word extent up instead of down
"""

import pytest
import torch
import tilelang
import tilelang.testing
import tilelang.language as T


# ---------------------------------------------------------------------------
# Fix 7 — src/rocm/codegen/codegen_hip.cc
#   Packed scalar int4/uint4 use one int32 storage word per eight elements.
# ---------------------------------------------------------------------------


@tilelang.testing.requires_rocm
@pytest.mark.parametrize(
    "dtype,storage_type",
    [(T.int4, "int"), (T.dtype("uint4"), "uint")],
)
def test_static_packed_int4_shared_allocation_rounds_up(dtype, storage_type):
    @T.prim_func
    def kernel():
        with T.Kernel(1, threads=1):
            A_shared = T.alloc_shared((127,), dtype, scope="shared")
            A_shared[126] = T.cast(1, dtype)

    artifact = tilelang.lower(kernel, target="hip")

    assert f"{storage_type} A_shared[16];" in artifact.kernel_source


# ---------------------------------------------------------------------------
# Fix 1 — src/tl_templates/hip/reduce.h
#   warp_reduce: 5-step butterfly with explicit width=32
#
# Symptom: On CDNA (wave64) with 32 active threads the old 6-step butterfly
#   called __shfl_xor(value, 32) without a width argument, reading uninitialised
#   VGPRs in lanes 32-63.  This produced NaN or garbage in every reduction that
#   went through warp_reduce (reduce_max, reduce_sum, AllReduce).
#
# Fix: remove the step-32 shuffle; add width=32 to every remaining step.
#   __shfl_xor(v, N, 32) restricts the butterfly to the lower 32-lane group,
#   matching CUDA warp semantics on CDNA wave64 and RDNA wave32 alike.
#   With 64 threads and width=32 the wavefront splits into two independent
#   32-lane groups — correct for kernels that assume logical warp_size=32.
# ---------------------------------------------------------------------------


@tilelang.testing.requires_rocm
@pytest.mark.parametrize("n_tokens,n_experts", [(64, 8), (128, 16), (512, 32)])
def test_warp_reduce_no_nan(n_tokens, n_experts):
    """
    32-thread-per-block reduce_max / reduce_sum must not produce NaN on CDNA.

    Old: __shfl_xor(v, 32) with 32 active threads reads uninit VGPRs → NaN.
    New: 5-step with width=32 stays in [0,31] group → correct, no NaN.
    """
    assert n_experts <= 32

    @tilelang.jit
    def gate_reduce(n_tok: int, n_exp: int):
        @T.prim_func
        def kernel(
            logits: T.Tensor((n_tok, n_exp), T.float32),
            out_max: T.Tensor((n_tok,), T.float32),
            out_sum: T.Tensor((n_tok,), T.float32),
        ) -> None:
            with T.Kernel(n_tok, threads=32) as pid:
                lf = T.alloc_fragment(n_exp, T.float32)
                T.copy(logits[pid, 0], lf)
                mx = T.alloc_fragment(1, T.float32)
                T.reduce_max(lf, mx, dim=0)
                sm = T.alloc_fragment(1, T.float32)
                T.reduce_sum(lf, sm, dim=0)
                if T.get_thread_binding() == 0:
                    out_max[pid] = mx[0]
                    out_sum[pid] = sm[0]

        return kernel

    logits = torch.randn(n_tokens, n_experts, dtype=torch.float32, device="cuda")
    out_max = torch.zeros(n_tokens, dtype=torch.float32, device="cuda")
    out_sum = torch.zeros(n_tokens, dtype=torch.float32, device="cuda")

    gate_reduce(n_tokens, n_experts)(logits, out_max, out_sum)
    torch.cuda.synchronize()

    assert not out_max.isnan().any(), "reduce_max NaN — __shfl_xor(v,32) uninit VGPR bug"
    assert not out_sum.isnan().any(), "reduce_sum NaN — __shfl_xor(v,32) uninit VGPR bug"
    torch.testing.assert_close(out_max, logits.max(dim=1).values, atol=1e-5, rtol=1e-5)
    torch.testing.assert_close(out_sum, logits.sum(dim=1), atol=1e-4, rtol=1e-4)


@tilelang.testing.requires_rocm
def test_warp_reduce_correctness_32_threads():
    """
    32-thread reduce_sum over 32 elements must return the exact sum on CDNA.

    Exercises the warp-level shuffle path directly.  With the old step-32
    shuffle, uninitialised VGPR reads on CDNA produced garbage.
    """
    N = 32

    @tilelang.jit
    def reduce_kernel():
        @T.prim_func
        def kernel(
            x: T.Tensor((N,), T.float32),
            out: T.Tensor((1,), T.float32),
        ) -> None:
            with T.Kernel(1, threads=N) as _:
                frag = T.alloc_fragment((N,), T.float32)
                T.copy(x, frag)
                s = T.alloc_fragment((1,), T.float32)
                T.reduce_sum(frag, s, dim=0)
                if T.get_thread_binding() == 0:
                    out[0] = s[0]

        return kernel

    x = torch.arange(1, N + 1, dtype=torch.float32, device="cuda")
    out = torch.zeros(1, dtype=torch.float32, device="cuda")
    reduce_kernel()(x, out)
    torch.cuda.synchronize()

    assert not out[0].isnan(), "reduce_sum NaN — warp_reduce VGPR bug on CDNA"
    torch.testing.assert_close(out[0], x.sum(), atol=1e-4, rtol=1e-4)


@tilelang.testing.requires_rocm
def test_warp_reduce_with_64_threads_two_groups():
    """
    With 64 threads and width=32 the wavefront splits into two independent
    32-lane groups — each group's lane 0 holds its partial sum.

    Old: __shfl_xor(v, 32) without width mixed the groups → wrong result.
    New: width=32 confines each shuffle to its own 32-lane group → correct.
    """
    N, n_exp = 64, 4

    @tilelang.jit
    def two_warp_reduce():
        @T.prim_func
        def kernel(
            x: T.Tensor((N, n_exp), T.float32),
            out: T.Tensor((N,), T.float32),
        ) -> None:
            with T.Kernel(1, threads=N) as _:
                tx = T.get_thread_binding()
                frag = T.alloc_fragment(n_exp, T.float32)
                T.copy(x[tx, 0], frag)
                s = T.alloc_fragment(1, T.float32)
                T.reduce_sum(frag, s, dim=0)
                out[tx] = s[0]

        return kernel

    x = torch.ones(N, n_exp, dtype=torch.float32, device="cuda")
    out = torch.zeros(N, dtype=torch.float32, device="cuda")
    two_warp_reduce()(x, out)
    torch.cuda.synchronize()

    assert not out.isnan().any(), "NaN in two-warp reduce — width=32 fix not applied"


# ---------------------------------------------------------------------------
# Fix 2 — src/rocm/codegen/codegen_hip.cc (VisitExpr_ ShuffleNode)
#         src/tl_templates/hip/common.h (uint1 bfloat16x2 math overloads)
#
# Symptom: Packing two bfloat16 scalars into a bfloat16x2 ShuffleNode caused
#   CodeGenC to emit `uint1(a, b)` — invalid HIP constructor → compile error.
#
# Fix (codegen_hip.cc): Override VisitExpr_(ShuffleNode) to emit
#   `uint1{__pack_bfloat162(a, b)}` / `uint1{__pack_half2(a, b)}`.
# Fix (common.h): Add abs2/max2/min2/add2/mul2 overloads for uint1 as a
#   packed bfloat16x2 carrier.
# ---------------------------------------------------------------------------


@tilelang.testing.requires_rocm
def test_bfloat16_shuffle_codegen_and_correctness():
    """
    bfloat16 fragment warp-reduction: source must use __pack_bfloat162
    (not invalid `uint1(a,b)`) and the result must be numerically correct.
    """
    n_tok, n_exp = 16, 8

    @tilelang.jit
    def bf16_reduce(n_t: int, n_e: int):
        @T.prim_func
        def kernel(
            x: T.Tensor((n_t, n_e), T.bfloat16),
            out: T.Tensor((n_t,), T.float32),
        ) -> None:
            with T.Kernel(n_t, threads=32) as pid:
                frag = T.alloc_fragment(n_e, T.bfloat16)
                T.copy(x[pid, 0], frag)
                frag_f32 = T.alloc_fragment(n_e, T.float32)
                for i in T.Parallel(n_e):
                    frag_f32[i] = T.cast(frag[i], T.float32)
                s = T.alloc_fragment(1, T.float32)
                T.reduce_sum(frag_f32, s, dim=0)
                if T.get_thread_binding() == 0:
                    out[pid] = s[0]

        return kernel

    kernel = bf16_reduce(n_tok, n_exp)

    # Source check: no invalid two-argument constructor
    src = kernel.get_kernel_source()
    assert "uint1(a" not in src and "uint1(b" not in src, "Old `uint1(a, b)` constructor found — ShuffleNode fix not applied"

    # Runtime correctness
    x = torch.randn(n_tok, n_exp, dtype=torch.bfloat16, device="cuda")
    out = torch.zeros(n_tok, dtype=torch.float32, device="cuda")
    kernel(x, out)
    torch.cuda.synchronize()
    assert not out.isnan().any(), "bf16 ShuffleNode reduction NaN"
    torch.testing.assert_close(out, x.float().sum(dim=1), atol=5e-2, rtol=1e-2)


@tilelang.testing.requires_rocm
def test_float16_shuffle_correctness():
    """
    float16 fragment warp-reduction exercises the __pack_half2 path.
    Analogous to the bfloat16 test but for float16x2 packing.
    """
    n_tok, n_exp = 64, 8

    @tilelang.jit
    def f16_reduce(n_t: int, n_e: int):
        @T.prim_func
        def kernel(
            x: T.Tensor((n_t, n_e), T.float16),
            out: T.Tensor((n_t,), T.float32),
        ) -> None:
            with T.Kernel(n_t, threads=32) as pid:
                frag = T.alloc_fragment(n_e, T.float16)
                T.copy(x[pid, 0], frag)
                frag_f32 = T.alloc_fragment(n_e, T.float32)
                for i in T.Parallel(n_e):
                    frag_f32[i] = T.cast(frag[i], T.float32)
                s = T.alloc_fragment(1, T.float32)
                T.reduce_sum(frag_f32, s, dim=0)
                if T.get_thread_binding() == 0:
                    out[pid] = s[0]

        return kernel

    x = torch.randn(n_tok, n_exp, dtype=torch.float16, device="cuda")
    out = torch.zeros(n_tok, dtype=torch.float32, device="cuda")
    f16_reduce(n_tok, n_exp)(x, out)
    torch.cuda.synchronize()
    assert not out.isnan().any(), "float16 ShuffleNode reduction NaN"
    torch.testing.assert_close(out, x.float().sum(dim=1), atol=1e-1, rtol=1e-2)


# ---------------------------------------------------------------------------
# Fix 3 — tilelang/language/allocate.py + src/rocm/codegen/codegen_hip.cc
#   T.alloc_var(init=<literal>) initialisation on HIP;
#   local.var scalar declaration and GetBufferRef bare-name return
#
# Symptom (allocate.py): int/float literals used block_attr("tl.local_var_init")
#   which the HIP backend silently ignored → variable uninitialised at runtime.
# Symptom (codegen_hip.cc): AllocateNode emitted `type vid[1];` for local.var;
#   alloc_storage_scope_ was not updated → GetBufferRef fell through to an
#   invalid pointer-cast path → compile failure.
#
# Fix (allocate.py): always route init through T.buffer_store → explicit
#   BufferStore TIR node → assignment statement in every backend.
# Fix (codegen_hip.cc): emit `type vid = init;`; register alloc_storage_scope_
#   so GetBufferRef returns the bare name `vid`.
# ---------------------------------------------------------------------------


@tilelang.jit(
    pass_configs={
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
        tilelang.PassConfigKey.TL_DISABLE_THREAD_STORAGE_SYNC: True,
    }
)
def _kernel_alloc_var_init():
    """Kernel that initialises a local int32 variable to 7 and writes it out."""

    @T.prim_func
    def main(Out: T.Tensor((64,), "int32")):
        with T.Kernel(1, threads=64):
            tx = T.get_thread_binding()
            counter = T.alloc_var(T.int32, init=7)
            Out[tx] = counter

    return main


@tilelang.testing.requires_rocm
def test_alloc_var_init_in_hip_source():
    """Init value must appear as `= 7;` in the generated HIP source."""
    src = _kernel_alloc_var_init().get_kernel_source()
    assert "= 7;" in src, (
        f"T.alloc_var(T.int32, init=7) should generate '= 7;' in HIP source, but it was not found.\nGenerated source:\n{src}"
    )


@tilelang.testing.requires_rocm
def test_alloc_var_init_no_array_subscript_in_hip_source():
    """local.var must be declared as a scalar (no `counter[` array syntax)."""
    src = _kernel_alloc_var_init().get_kernel_source()
    assert "counter[" not in src, (
        f"local.var should be emitted as a scalar (e.g. 'int counter = 7'), but array-style access was found:\n{src}"
    )


@tilelang.testing.requires_rocm
def test_alloc_var_init_correctness():
    """All output elements must equal 7 — the initialised value."""
    out = torch.zeros(64, dtype=torch.int32, device="cuda")
    _kernel_alloc_var_init()(out)
    assert torch.all(out == 7), f"Expected all 7, got: {out}"


@tilelang.jit(
    pass_configs={
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
        tilelang.PassConfigKey.TL_DISABLE_THREAD_STORAGE_SYNC: True,
    }
)
def _kernel_multi_alloc_var_init():
    """Two local variables with different init values, summed into output."""

    @T.prim_func
    def main(Out: T.Tensor((32,), "int32")):
        with T.Kernel(1, threads=32):
            tx = T.get_thread_binding()
            a = T.alloc_var(T.int32, init=3)
            b = T.alloc_var(T.int32, init=4)
            Out[tx] = a + b

    return main


@tilelang.testing.requires_rocm
def test_multi_alloc_var_init_in_hip_source():
    """Both init values must appear in the HIP source."""
    src = _kernel_multi_alloc_var_init().get_kernel_source()
    assert src.count("= 3;") >= 1, f"Init value 3 not found in HIP source:\n{src}"
    assert src.count("= 4;") >= 1, f"Init value 4 not found in HIP source:\n{src}"


@tilelang.testing.requires_rocm
def test_multi_alloc_var_init_correctness():
    """Sum of two initialised local variables must equal 7 (3+4)."""
    out = torch.zeros(32, dtype=torch.int32, device="cuda")
    _kernel_multi_alloc_var_init()(out)
    assert torch.all(out == 7), f"Expected all 7 (3+4), got: {out}"


@tilelang.jit(
    pass_configs={
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
        tilelang.PassConfigKey.TL_DISABLE_THREAD_STORAGE_SYNC: True,
    }
)
def _kernel_alloc_var_count():
    """Counter initialised to 0, incremented 5 times in a loop."""

    @T.prim_func
    def main(Out: T.Tensor((32,), "int32")):
        with T.Kernel(1, threads=32):
            tx = T.get_thread_binding()
            count = T.alloc_var(T.int32, init=0)
            for _ in T.unroll(5):
                count += 1
            Out[tx] = count

    return main


@tilelang.testing.requires_rocm
def test_alloc_var_zero_init_correctness():
    """Variable initialised to 0 and incremented 5 times must equal 5."""
    out = torch.zeros(32, dtype=torch.int32, device="cuda")
    _kernel_alloc_var_count()(out)
    assert torch.all(out == 5), f"Expected all 5, got: {out}"


@tilelang.testing.requires_rocm
@pytest.mark.parametrize(
    "init_val,dtype_str",
    [
        (0, "int32"),
        (7, "int32"),
        (-3, "int32"),
        (0.0, "float32"),
        (1.0, "float32"),
        (-0.5, "float32"),
    ],
)
def test_alloc_var_literal_init_is_reliable(init_val, dtype_str):
    """
    alloc_var with any literal init must produce that exact value on HIP.

    Old: int/float literals → block_attr (silently ignored) → uninit.
    New: always T.buffer_store → `vid = init_val;` in generated HIP C.
    """
    tl_dtype = T.int32 if dtype_str == "int32" else T.float32
    torch_dtype = torch.int32 if dtype_str == "int32" else torch.float32
    N = 32

    @tilelang.jit
    def var_init_kernel(iv, tld):
        @T.prim_func
        def kernel(out: T.Tensor((N,), tld)) -> None:
            with T.Kernel(1, threads=N) as _:
                v = T.alloc_var(tld, init=iv)
                for i in T.Parallel(N):
                    out[i] = v

        return kernel

    out = torch.zeros(N, dtype=torch_dtype, device="cuda")
    var_init_kernel(init_val, tl_dtype)(out)
    torch.cuda.synchronize()

    expected = torch.full((N,), init_val, dtype=torch_dtype, device="cuda")
    if dtype_str == "int32":
        assert torch.equal(out, expected), f"alloc_var(init={init_val}) got {out[0].item()}, expected {init_val}"
    else:
        torch.testing.assert_close(out, expected, atol=0, rtol=0)


@tilelang.testing.requires_rocm
def test_alloc_var_inf_init():
    """
    alloc_var(init=-T.infinity(T.float32)) — the pattern used for top1_var /
    top2_var in MoE topk gate kernels — must produce -inf on HIP.
    """
    N = 32

    @tilelang.jit
    def inf_init_kernel():
        @T.prim_func
        def kernel(out: T.Tensor((N,), T.float32)) -> None:
            with T.Kernel(1, threads=N) as _:
                v = T.alloc_var(T.float32, init=-T.infinity(T.float32))
                for i in T.Parallel(N):
                    out[i] = v

        return kernel

    out = torch.zeros(N, dtype=torch.float32, device="cuda")
    inf_init_kernel()(out)
    torch.cuda.synchronize()
    assert out.isinf().all() and (out < 0).all(), f"alloc_var(init=-inf) got {out[0].item()}, expected -inf"


@tilelang.testing.requires_rocm
def test_alloc_var_init_zero_persists_across_serial_loop():
    """
    count_var = T.alloc_var(T.int32, init=0) must start at 0 and accumulate
    correctly.  This is the exact pattern used by count_var in MoE kernels.
    """
    N = 8

    @tilelang.jit
    def serial_count_kernel():
        @T.prim_func
        def kernel(out: T.Tensor((1,), T.int32)) -> None:
            with T.Kernel(1, threads=1) as _:
                count_var = T.alloc_var(T.int32, init=0)
                for _ in T.serial(N):
                    count_var = count_var + 1
                out[0] = count_var

        return kernel

    out = torch.zeros(1, dtype=torch.int32, device="cuda")
    serial_count_kernel()(out)
    torch.cuda.synchronize()
    assert out[0].item() == N, f"count_var: got {out[0].item()}, expected {N} — init=0 not applied (block_attr bug)"


@tilelang.testing.requires_rocm
def test_local_var_scalar_codegen():
    """
    local.var must be emitted and accessed as a plain scalar on HIP.

    Before: alloc_storage_scope_ not registered → GetBufferRef fell through to
            an invalid pointer-cast path → compile failure.
    After:  `type vid = init;` emitted; GetBufferRef returns bare `vid`.
    """
    N = 32

    @tilelang.jit
    def local_var_scalar():
        @T.prim_func
        def kernel(out: T.Tensor((N,), T.int32)) -> None:
            with T.Kernel(1, threads=N) as _:
                v = T.alloc_var(T.int32, init=5)
                if T.get_thread_binding() == 0:
                    v = v + 1
                for i in T.Parallel(N):
                    out[i] = v

        return kernel

    out = torch.zeros(N, dtype=torch.int32, device="cuda")
    local_var_scalar()(out)
    torch.cuda.synchronize()
    assert out[0].item() == 6, f"local.var scalar: got {out[0].item()}, expected 6 (5+1)"


@tilelang.testing.requires_rocm
def test_local_var_float_init_readable():
    """
    local.var with float32 literal init must be readable on HIP.
    Before the alloc_storage_scope_ fix, GetBufferRef emitted invalid code.
    """

    @tilelang.jit
    def float_init_readback():
        @T.prim_func
        def kernel(out: T.Tensor((1,), T.float32)) -> None:
            with T.Kernel(1, threads=32) as _:
                v = T.alloc_var(T.float32, init=3.14)
                if T.get_thread_binding() == 0:
                    out[0] = v

        return kernel

    out = torch.zeros(1, dtype=torch.float32, device="cuda")
    float_init_readback()(out)
    torch.cuda.synchronize()
    torch.testing.assert_close(out[0].item(), 3.14, atol=1e-5, rtol=0)


# ---------------------------------------------------------------------------
# Fix 4 — src/rocm/codegen/codegen_hip.cc
#   T.sync_warp() → no-op on HIP
#
# Symptom: tl::sync_warp() had no handler → codegen assertion failure or
#   undefined symbol at link time.
# Fix: emit an empty statement; AMD wavefronts execute in lockstep so
#   intra-wavefront convergence is guaranteed by hardware.
# ---------------------------------------------------------------------------


@tilelang.jit(
    pass_configs={
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
        tilelang.PassConfigKey.TL_DISABLE_THREAD_STORAGE_SYNC: True,
    }
)
def _kernel_sync_warp_codegen():
    """Minimal kernel that exercises T.sync_warp()."""

    @T.prim_func
    def main(A: T.Tensor((32,), "float32"), B: T.Tensor((32,), "float32")):
        with T.Kernel(1, threads=32):
            tx = T.get_thread_binding()
            A_shared = T.alloc_shared((32,), "float32")
            A_shared[tx] = A[tx]
            T.sync_warp()
            B[tx] = A_shared[tx] * 2.0

    return main


@tilelang.testing.requires_rocm
def test_sync_warp_no_syncwarp_in_hip_source():
    """__syncwarp must NOT appear in the generated HIP source."""
    src = _kernel_sync_warp_codegen().get_kernel_source()
    assert "__syncwarp" not in src, f"T.sync_warp() should be a no-op on HIP, but __syncwarp was found in the generated source:\n{src}"


@tilelang.testing.requires_rocm
def test_sync_warp_correctness():
    """Kernel using T.sync_warp() must produce correct results on HIP."""
    A = torch.arange(32, dtype=torch.float32, device="cuda")
    B = torch.zeros(32, dtype=torch.float32, device="cuda")
    _kernel_sync_warp_codegen()(A, B)
    torch.testing.assert_close(B, A * 2.0)


@tilelang.testing.requires_rocm
def test_sync_warp_inside_conditional():
    """
    T.sync_warp() inside a conditional branch (pattern from moe/common.py
    get_topk_group_idx).  Verifies compilation and deterministic output.
    """
    N, M = 32, 8

    @tilelang.jit
    def sync_warp_cond_kernel():
        @T.prim_func
        def kernel(
            x: T.Tensor((N,), T.float32),
            out: T.Tensor((M,), T.float32),
        ) -> None:
            with T.Kernel(1, threads=N) as _:
                shmem = T.alloc_shared((M,), T.float32)
                tx = T.get_thread_binding()
                if tx < M:
                    shmem[tx] = x[tx]
                T.sync_warp()
                for i in T.Parallel(M):
                    out[i] = shmem[i]

        return kernel

    x = torch.randn(N, dtype=torch.float32, device="cuda")
    out = torch.zeros(M, dtype=torch.float32, device="cuda")
    sync_warp_cond_kernel()(x, out)
    torch.cuda.synchronize()
    torch.testing.assert_close(out, x[:M])


# ---------------------------------------------------------------------------
# Fix 5 — src/rocm/codegen/codegen_hip.cc,
#          src/rocm/codegen/rt_mod_hip.cc,
#          src/rocm/stubs/hip.cc,
#          src/rocm/stubs/hip.h
#   T.sync_grid() → cooperative_groups::this_grid().sync()
#
# Symptom: tl::sync_grid() had no handler → same assertion / link failure.
# Fix: emit cooperative_groups call; add need_cooperative_groups_ flag to
#   conditionally include hip_cooperative_groups.h; add runtime launch
#   infrastructure (hipModuleLaunchCooperativeKernel stubs).
# ---------------------------------------------------------------------------


@tilelang.jit(
    pass_configs={
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
        tilelang.PassConfigKey.TL_DISABLE_THREAD_STORAGE_SYNC: True,
    }
)
def _kernel_sync_grid_codegen():
    """Kernel that calls T.sync_grid() to trigger cooperative groups codegen."""

    @T.prim_func
    def main(A: T.Tensor((32,), "float32")):
        with T.Kernel(1, threads=32):
            tx = T.get_thread_binding()
            T.sync_grid()
            A[tx] = T.float32(tx)

    return main


@tilelang.testing.requires_rocm
def test_sync_grid_cooperative_groups_in_hip_source():
    """
    T.sync_grid() must emit cooperative_groups::this_grid().sync() and
    include <hip/hip_cooperative_groups.h> in the generated HIP source.

    Note: runtime execution requires hipModuleLaunchCooperativeKernel which
    is added via the stub infrastructure; this test validates codegen only.
    """
    src = _kernel_sync_grid_codegen().get_kernel_source()
    assert "this_grid().sync()" in src, f"T.sync_grid() should generate 'this_grid().sync()' but not found:\n{src}"
    assert "cooperative_groups" in src, f"T.sync_grid() should include cooperative_groups but not found:\n{src}"


# ---------------------------------------------------------------------------
# Fix 6 — src/transform/pipeline_planning.cc
#   Skip T.Pipelined(num_stages>1) pipeline planning on ROCM
#
# Symptom: Double-buffering doubled the LDS allocation for every shared buffer
#   inside the loop body, exhausting the per-workgroup LDS budget and causing
#   hipModuleLaunchKernel to return HIPERRORINVALIDVALUE.  LDS limits:
#     gfx942 (CDNA3 / MI300X): 64 KB;  gfx950 (CDNA4 / MI350): 160 KB (#2058)
#   Even with gfx950's larger budget, double-buffering large shared tiles can
#   still exceed 160 KB, and the HIP async-copy infrastructure has no ROCM
#   equivalent, so the planner cannot safely pipeline on any ROCM target.
#
# Fix: when TargetIsRocm() && num_stages > 1, skip pipeline planning and fall
#   back to a plain sequential loop with synchronous T.copy — always LDS-safe.
# ---------------------------------------------------------------------------


@tilelang.testing.requires_rocm
@pytest.mark.parametrize("num_stages", [1, 2, 3])
def test_pipelined_no_lds_overflow(num_stages):
    """
    T.Pipelined(num_stages=N) must not raise hipModuleLaunchKernel EINVAL and
    must produce the correct result regardless of N.

    Old: num_stages=2 doubled LDS → EINVAL (64 KB on gfx942, 160 KB on gfx950).
    New: multi-stage loops fall back to plain sequential on ROCM.
    """
    M, K, blk = 32, 256, 64

    @tilelang.jit
    def kernel(
        x: T.Tensor((M, K), T.float32),
        out: T.Tensor((M,), T.float32),
    ) -> None:
        with T.Kernel(M, threads=64) as pid:
            acc = T.alloc_fragment((1,), T.float32)
            xs = T.alloc_shared((blk,), T.float32)
            xl = T.alloc_fragment((blk,), T.float32)
            s = T.alloc_fragment((1,), T.float32)
            T.clear(acc)
            for k in T.Pipelined(K // blk, num_stages=num_stages):
                T.copy(x[pid, k * blk], xs, disable_tma=True)
                T.copy(xs, xl, disable_tma=True)
                T.reduce_sum(xl, s, dim=0)
                acc[0] = acc[0] + s[0]
            out[pid] = acc[0]

    x = torch.ones(M, K, dtype=torch.float32, device="cuda")
    out = torch.zeros(M, dtype=torch.float32, device="cuda")
    kernel(x, out)
    torch.cuda.synchronize()
    torch.testing.assert_close(out, torch.full((M,), float(K), device="cuda"), atol=1e-4, rtol=0)


@tilelang.testing.requires_rocm
@pytest.mark.parametrize("num_stages", [2, 3])
def test_pipelined_multi_stage_fp16_gemm(num_stages):
    """
    FP16 GEMM with T.Pipelined(num_stages>1) must launch and produce correct
    results on ROCM — the most common pattern that triggered the LDS overflow
    (A_s bM×bK + B_s bK×bN doubled per pipeline stage).
    """
    M, N, K = 128, 128, 128
    bM, bN, bK = 64, 64, 32

    @tilelang.jit
    def kernel(
        A: T.Tensor((M, K), T.float16),
        B: T.Tensor((K, N), T.float16),
        C: T.Tensor((M, N), T.float32),
    ) -> None:
        with T.Kernel(T.ceildiv(N, bN), T.ceildiv(M, bM), threads=128) as (bx, by):
            A_s = T.alloc_shared((bM, bK), T.float16)
            B_s = T.alloc_shared((bK, bN), T.float16)
            C_l = T.alloc_fragment((bM, bN), T.float32)
            T.clear(C_l)
            for k in T.Pipelined(K // bK, num_stages=num_stages):
                T.copy(A[by * bM, k * bK], A_s)
                T.copy(B[k * bK, bx * bN], B_s)
                T.gemm(A_s, B_s, C_l)
            T.copy(C_l, C[by * bM, bx * bN])

    A = torch.randn(M, K, dtype=torch.float16, device="cuda")
    B = torch.randn(K, N, dtype=torch.float16, device="cuda")
    C = torch.zeros(M, N, dtype=torch.float32, device="cuda")
    kernel(A, B, C)
    torch.cuda.synchronize()
    torch.testing.assert_close(C, A.float() @ B.float(), atol=1.0, rtol=5e-2)


# ---------------------------------------------------------------------------
# Fix 7 — src/backend/rocm/codegen/intrin_rule_hip.cc
#   Scalarize vector math intrinsics on HIP
#
# Symptom: A vectorized math op (e.g. tirx.exp2 on float32x4 from a 2D
#   fragment + T.Parallel) was left unlowered.  HIPMath/FloatSuffix return
#   "" for non-scalar dtypes, so DispatchPureExtern returned the op
#   unchanged and CodeGenTileLangHIP -> CodeGenC then threw
#       tvm.error.InternalError: Unresolved call ir.Op(name="tirx.exp2")
#   at codegen_c.cc:771.
#
# Fix: DispatchPureExternScalarized resolves the extern name from the
#   *element* dtype and emits a vector call_pure_extern; the existing
#   CodeGenTileLangHIP::PrintCallExtern then lowers it to one scalar call
#   per lane via PrintVecElemLoad / PrintVecElemStore, which is carrier-
#   aware (handles float32x8 -> ulonglong4, float16x4 -> uint2, etc.).
# ---------------------------------------------------------------------------


@tilelang.jit
def _kernel_vector_exp2_codegen():
    """T.vectorized(VEC) makes T.exp2 lower as tirx.exp2 on float32xVEC."""
    N, VEC = 256, 4

    @T.prim_func
    def main(
        A: T.Tensor((N,), "float32"),
        B: T.Tensor((N,), "float32"),
    ):
        with T.Kernel(N // VEC, threads=1) as bx:
            for v in T.vectorized(VEC):
                B[bx * VEC + v] = T.exp2(A[bx * VEC + v])

    return main


@tilelang.testing.requires_rocm
def test_vector_math_scalarized_in_hip_source():
    """Vectorized T.exp2 must lower to one scalar exp2f per lane on HIP."""
    src = _kernel_vector_exp2_codegen().get_kernel_source()
    assert "tirx.exp2" not in src, (
        "Vectorized tirx.exp2 was left unlowered (would emit 'Unresolved call' from CodeGenC). Generated HIP source:\n" + src
    )
    assert src.count("exp2f") >= 4, (
        "Expected one 'exp2f' per lane (>=4) in HIP source for vectorized "
        f"T.exp2 over float32x4, got {src.count('exp2f')}. "
        "Generated HIP source:\n" + src
    )


if __name__ == "__main__":
    tilelang.testing.main()
