"""Phase 1 spec test for lazy ``tl.load`` (ADR-0062).

ADR-0062 §D1/§D2: ``tl.load`` is non-blocking. It issues a ``DmaReadCmd``
and returns immediately with a ``TensorHandle`` carrying a pending
``LoadFuture``. The runtime auto-inserts a wait on that future at the
first consuming op (``tl.dot``, MATH ops, ``tl.store``, ``tl.send``,
``tl.copy_to``, ``tl.composite`` operands). Symmetric with the existing
``recv_async`` / ``RecvFuture`` pattern, generalised to HBM loads.

Phase 1 (this commit): tests only — production code lands in Phase 2.
All tests currently fail because:
  - ``TensorHandle`` has no ``pending`` field
  - ``LoadFuture`` class doesn't exist
"""
from __future__ import annotations

from kernbench.common.pe_commands import DmaReadCmd
from kernbench.triton_emu.tl_context import TLContext


def _ctx() -> TLContext:
    """TLContext with a real scratch base so _make_compute_out allocates."""
    return TLContext(
        pe_id=0, num_programs=1, dispatch_cycles=0,
        scratch_base=1 << 24, scratch_size=1 << 20,
    )


# ── 1. tl.load handle carries a `pending` field ──────────────────────


def test_tl_load_handle_carries_pending_field():
    """ADR-0062 §D2: tl.load handle has a ``pending`` attribute that is
    not None — it references the LoadFuture the consumer will await."""
    ctx = _ctx()
    handle = ctx.load(0x1000, shape=(8, 64), dtype="f16")
    assert hasattr(handle, "pending"), (
        "TensorHandle must have a `pending` field per ADR-0062 §D2"
    )
    assert handle.pending is not None, (
        "tl.load handle must carry a non-None pending (LoadFuture)"
    )


# ── 2. Constant / math-output handles have pending=None ──────────────


def test_constant_handle_pending_is_none():
    """Non-loaded handles (tl.zeros, tl.full, _make_compute_out, tl.arange)
    have ``pending=None`` — there is no DMA to wait on. Consumer ops can
    safely skip auto-wait for these inputs."""
    ctx = _ctx()
    z = ctx.zeros((8, 64), dtype="f16")
    assert getattr(z, "pending", "MISSING") is None, (
        "tl.zeros handle must have pending=None"
    )
    f = ctx.full((8, 64), value=0.0, dtype="f16")
    assert getattr(f, "pending", "MISSING") is None, (
        "tl.full handle must have pending=None"
    )
    a = ctx.arange(0, 8, dtype="i32")
    assert getattr(a, "pending", "MISSING") is None, (
        "tl.arange handle must have pending=None"
    )


def test_compute_out_pending_is_none():
    """Scratch-allocated output handles (via _make_compute_out) have
    pending=None. Math ops produce them; no DMA → no auto-wait needed
    when they're consumed downstream."""
    ctx = _ctx()
    out = ctx._make_compute_out(shape=(8, 64), dtype="f16")
    assert getattr(out, "pending", "MISSING") is None, (
        "compute-out handle must have pending=None"
    )


# ── 3. LoadFuture class exists ───────────────────────────────────────


def test_loadfuture_class_exists():
    """ADR-0062 §D2: LoadFuture class wraps the DMA done event and a
    resolved-flag, analogous to RecvFuture for IPCQ. Lives in
    tl_context.py to keep all greenlet bridge types in one module."""
    from kernbench.triton_emu.tl_context import LoadFuture  # added Phase 2
    assert LoadFuture is not None


# ── 4. Distinct loads produce distinct LoadFutures ───────────────────


def test_distinct_loads_have_distinct_pending():
    """ADR-0062 §D2: two tl.load calls (different addresses) produce two
    independent LoadFuture objects. Each consumer must wait on the
    right one — sharing the same future would over-serialise."""
    ctx = _ctx()
    h1 = ctx.load(0x1000, shape=(8,), dtype="f16")
    h2 = ctx.load(0x2000, shape=(8,), dtype="f16")
    assert h1.pending is not None and h2.pending is not None
    assert h1.pending is not h2.pending, (
        "distinct loads must have distinct LoadFuture objects"
    )


# ── 5. LoadFuture starts unresolved ──────────────────────────────────


def test_loadfuture_starts_unresolved():
    """ADR-0062 §D2: a fresh LoadFuture has resolved=False — the consumer
    op must await it before reading. Once awaited (in greenlet mode by
    auto-wait at first use), the SimPy event triggers and a subsequent
    consumer can skip the yield."""
    ctx = _ctx()
    h = ctx.load(0x1000, shape=(8,), dtype="f16")
    assert h.pending.resolved is False, (
        "LoadFuture must start unresolved; got resolved=True at issue time"
    )


# ── 6. op_log: dma_read_count unchanged from blocking version ────────


def test_op_log_dma_read_count_unchanged():
    """ADR-0062 §D2 (last paragraph) / Test Req 4: each tl.load still
    emits exactly one DmaReadCmd. The asynchrony is a scheduling
    property, not a new op kind — dma_read_count and existing op_log
    consumers see no change."""
    ctx = _ctx()
    ctx.load(0x1000, shape=(8, 64), dtype="f16")
    ctx.load(0x2000, shape=(8, 64), dtype="f16")
    ctx.load(0x3000, shape=(8, 64), dtype="f16")
    dma_cmds = [c for c in ctx.commands if isinstance(c, DmaReadCmd)]
    assert len(dma_cmds) == 3, (
        f"lazy tl.load must still emit one DmaReadCmd per call; "
        f"got {len(dma_cmds)}"
    )


# ── 7. Handle metadata fields preserved across the lazy change ───────


def test_tl_load_handle_metadata_unchanged():
    """ADR-0062 §D1: the tl surface is unchanged. The handle's addr,
    shape, dtype, nbytes, space, pinned fields are identical to the
    blocking version — only `pending` is new."""
    ctx = _ctx()
    h = ctx.load(0x1000, shape=(8, 64), dtype="f16")
    assert h.addr == 0x1000
    assert h.shape == (8, 64)
    assert h.dtype == "f16"
    assert h.nbytes == 8 * 64 * 2
    assert h.space == "hbm"
    assert h.pinned is True


# ── 8. LoadFuture carries the DmaReadCmd it wraps ────────────────────


def test_loadfuture_carries_cmd():
    """ADR-0062 §D2: LoadFuture references the DmaReadCmd it wraps so
    the runtime can resolve handle → command → completion event at
    auto-wait time. Mirrors RecvFuture.cmd."""
    ctx = _ctx()
    h = ctx.load(0x1000, shape=(8, 64), dtype="f16")
    assert hasattr(h.pending, "cmd"), "LoadFuture.cmd must exist"
    assert isinstance(h.pending.cmd, DmaReadCmd)
    assert h.pending.cmd.handle is h
