"""Tests for PE_MMU component integration and MmuMapMsg fabric path.

Validates:
  T15-a. PE_MMU component registered in ComponentRegistry
  T15-b. PE_MMU component receives MmuMapMsg via inbox, updates page table
  T15-c. PE_DMA translates VA→PA via mmu before routing
  T16.   MmuMapMsg/MmuUnmapMsg message types defined with correct fields
  T17.   PE_CPU passes VA (not PA) to kernel when VA is available
  T18.   End-to-end: deploy (MmuMapMsg broadcast) → kernel launch → DMA with VA
"""
import pytest

try:
    from kernbench.runtime_api.kernel import MmuMapMsg, MmuUnmapMsg
except ImportError:
    pytest.skip("MmuMapMsg/MmuUnmapMsg not yet defined (Phase 2)", allow_module_level=True)


# ── T16. MmuMapMsg / MmuUnmapMsg message types ──────────────────────


def test_mmu_map_msg_fields():
    """MmuMapMsg carries VA→PA mapping entries for broadcast."""
    msg = MmuMapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=(
            {"va": 0x1_0000_0000, "pa": 0xA000_0000, "size": 4096},
            {"va": 0x1_0000_1000, "pa": 0xB000_0000, "size": 4096},
        ),
        target_cubes="all",
        target_pe="all",
    )
    assert msg.msg_type == "mmu_map"
    assert len(msg.entries) == 2
    assert msg.entries[0]["va"] == 0x1_0000_0000
    assert msg.entries[0]["pa"] == 0xA000_0000
    assert msg.entries[0]["size"] == 4096


def test_mmu_map_msg_immutable():
    """MmuMapMsg is frozen."""
    msg = MmuMapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=(),
        target_cubes="all",
        target_pe="all",
    )
    with pytest.raises(AttributeError):
        msg.entries = ()  # type: ignore[misc]


def test_mmu_unmap_msg_fields():
    """MmuUnmapMsg carries VA ranges to unmap."""
    msg = MmuUnmapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=(
            {"va": 0x1_0000_0000, "size": 4096},
        ),
        target_cubes="all",
        target_pe="all",
    )
    assert msg.msg_type == "mmu_unmap"
    assert len(msg.entries) == 1
    assert msg.entries[0]["va"] == 0x1_0000_0000


# ── T15-a. PE_MMU component registry ────────────────────────────────


def test_pe_mmu_registry():
    """pe_mmu impl resolves in ComponentRegistry."""
    from kernbench.components.base import ComponentRegistry
    from kernbench.components.builtin.pe_mmu import PeMmuComponent
    from kernbench.topology.types import Node

    node = Node(
        id="sip0.cube0.pe0.pe_mmu",
        kind="pe_mmu",
        impl="builtin.pe_mmu",
        pos_mm=None,
        attrs={"tlb_overhead_ns": 0.5},
    )
    comp = ComponentRegistry.create(node)
    assert isinstance(comp, PeMmuComponent)


# ── T15-b. PE_MMU receives MmuMapMsg and updates page table ─────────


def test_pe_mmu_processes_map_msg():
    """PE_MMU component receives MmuMapMsg → translate works."""
    import simpy
    from kernbench.components.builtin.pe_mmu import PeMmuComponent
    from kernbench.sim_engine.transaction import Transaction
    from kernbench.topology.types import Node

    env = simpy.Environment()
    node = Node(
        id="sip0.cube0.pe0.pe_mmu",
        kind="pe_mmu",
        impl="builtin.pe_mmu",
        pos_mm=None,
        attrs={"tlb_overhead_ns": 0.5, "page_size": 4096},
    )
    comp = PeMmuComponent(node)
    comp.in_ports["src"] = simpy.Store(env)
    comp.start(env)

    # Submit MmuMapMsg via inbox
    map_msg = MmuMapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=(
            {"va": 0x1_0000_0000, "pa": 0xABCD_0000, "size": 4096},
        ),
        target_cubes="all",
        target_pe="all",
    )
    done = env.event()
    txn = Transaction(
        request=map_msg,
        path=["sip0.cube0.pe0.pe_mmu"],
        step=0, nbytes=0, done=done,
    )

    def inject():
        yield comp._inbox.put(txn)

    env.process(inject())
    env.run(until=100)

    # After processing, the MMU's translate should work
    from kernbench.policy.address.pe_mmu import PeMMU
    mmu = comp.mmu  # the underlying PeMMU utility object
    assert isinstance(mmu, PeMMU)
    assert mmu.translate(0x1_0000_0000) == 0xABCD_0000


# ── T15-c. PE_DMA uses MMU translate ────────────────────────────────


def test_pe_dma_translates_va():
    """PE_DMA.handle_command calls mmu.translate(va) → PA before routing.

    This test validates the contract: after Phase 2, DmaReadCmd carries VA,
    and PE_DMA must translate it to PA via the MMU before resolving the
    HBM node path.
    """
    # This test validates the interface contract. Full integration test
    # requires the engine wiring which is validated in test_engine.
    # Here we check that PE_DMA has an mmu attribute it can call.
    from kernbench.components.builtin.pe_dma import PeDmaComponent
    from kernbench.topology.types import Node

    node = Node(
        id="sip0.cube0.pe0.pe_dma",
        kind="pe_dma",
        impl="builtin.pe_dma",
        pos_mm=None,
        attrs={"rd_engines": 1, "wr_engines": 1},
    )
    comp = PeDmaComponent(node)

    # PE_DMA must have a way to access the MMU (via ctx or direct reference)
    # The exact wiring mechanism is flexible, but the attribute must exist
    assert hasattr(comp, '_mmu') or hasattr(comp, 'mmu') or (
        hasattr(comp, 'ctx') and comp.ctx is not None
    ), "PE_DMA must have access to PE_MMU for VA translation"


# ── T17. PE_CPU passes VA to kernel ──────────────────────────────────


def test_pe_cpu_uses_va_base_from_tensor_arg():
    """PE_CPU should use TensorArg.va_base for kernel pointer args.

    After Phase 2, TensorArg carries va_base alongside shards.
    PE_CPU extracts va_base and passes it to the kernel function
    so kernels operate on VA (not PA).
    """
    from kernbench.runtime_api.kernel import TensorArg, TensorArgShard

    shard = TensorArgShard(sip=0, cube=0, pe=0, pa=0x1000,
                           nbytes=4096, offset_bytes=0)
    targ = TensorArg(shards=(shard,), va_base=0x1_0000_0000)

    # PE_CPU should use targ.va_base for kernel pointer arg
    assert targ.va_base == 0x1_0000_0000
    # PA still accessible via shard for direct-PA operations (IPCQ etc.)
    assert shard.pa == 0x1000


# ── T18. MmuMapMsg broadcast pattern ─────────────────────────────────


def test_mmu_map_msg_broadcast_target():
    """MmuMapMsg with target_pe='all' is a broadcast to all PEs."""
    msg = MmuMapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=({"va": 0x1000, "pa": 0x2000, "size": 4096},),
        target_cubes="all",
        target_pe="all",
    )
    assert msg.target_pe == "all"
    assert msg.target_cubes == "all"


def test_mmu_map_msg_same_entries_all_pes():
    """All PEs in a broadcast receive identical entries (not per-PE splits)."""
    entries = (
        {"va": 0x1_0000_0000, "pa": 0xA000_0000, "size": 8192},
        {"va": 0x1_0000_2000, "pa": 0xB000_0000, "size": 8192},
    )
    msg = MmuMapMsg(
        correlation_id="c0",
        request_id="r0",
        entries=entries,
        target_cubes="all",
        target_pe="all",
    )
    # The message carries the full mapping — every PE receives exactly this
    assert msg.entries == entries
