"""Tests for DataExecutor Phase 2 execution (ADR-0020 D6)."""
import numpy as np

from kernbench.sim_engine.data_executor import DataExecutor
from kernbench.sim_engine.memory_store import MemoryStore
from kernbench.sim_engine.op_log import OpRecord


def test_gemm_execution():
    """Phase 2 GEMM: out = a @ b with f32 accumulation."""
    store = MemoryStore()
    a = np.ones((4, 8), dtype=np.float16)
    b = np.ones((8, 4), dtype=np.float16) * 2.0
    store.write("tcm", 0x0, a)
    store.write("tcm", 0x100, b)

    op = OpRecord(
        t_start=0.0, t_end=100.0,
        component_id="pe_gemm",
        op_kind="gemm", op_name="gemm_f16",
        params={
            "src_a_addr": 0x0, "src_b_addr": 0x100, "dst_addr": 0x200,
            "shape_a": (4, 8), "shape_b": (8, 4), "shape_out": (4, 4),
            "dtype_in": "f16", "dtype_acc": "f32", "dtype_out": "f16",
            "addr_space": "tcm",
        },
    )

    executor = DataExecutor([op], store)
    executor.run()

    result = store.read("tcm", 0x200)
    expected = (a.astype(np.float32) @ b.astype(np.float32)).astype(np.float16)
    assert np.allclose(result, expected)


def test_math_exp():
    store = MemoryStore()
    x = np.array([0.0, 1.0, 2.0], dtype=np.float32)
    store.write("tcm", 0x0, x)

    op = OpRecord(
        t_start=0.0, t_end=10.0,
        component_id="pe_math",
        op_kind="math", op_name="exp",
        params={
            "op": "exp",
            "input_addrs": [0x0], "input_shapes": [(3,)],
            "dst_addr": 0x100, "shape_out": (3,),
            "dtype": "f32", "axis": None, "addr_space": "tcm",
        },
    )

    executor = DataExecutor([op], store)
    executor.run()

    result = store.read("tcm", 0x100)
    assert np.allclose(result, np.exp(x))


def test_math_extra_ops():
    """Phase 2 replay of tl.maximum/minimum/fma/clamp/softmax."""
    store = MemoryStore()
    a = np.array([1.0, 5.0, 3.0], dtype=np.float32)
    b = np.array([4.0, 2.0, 6.0], dtype=np.float32)
    c = np.array([0.5, 0.5, 0.5], dtype=np.float32)
    store.write("tcm", 0x0, a)
    store.write("tcm", 0x100, b)
    store.write("tcm", 0x200, c)

    def _math(name, op, dst, inputs, axis=None):
        return OpRecord(
            t_start=float(dst), t_end=float(dst) + 1.0,
            component_id="pe_math", op_kind="math", op_name=name,
            params={
                "op": op,
                "input_addrs": [a for a, _ in inputs],
                "input_shapes": [s for _, s in inputs],
                "input_spaces": ["tcm"] * len(inputs),
                "input_dtypes": ["f32"] * len(inputs),
                "dst_addr": dst, "dst_space": "tcm",
                "shape_out": (3,), "dtype": "f32", "axis": axis,
            },
        )

    ops = [
        _math("maximum", "maximum", 0x300, [(0x0, (3,)), (0x100, (3,))]),
        _math("minimum", "minimum", 0x400, [(0x0, (3,)), (0x100, (3,))]),
        _math("fma",     "fma",     0x500, [(0x0, (3,)), (0x100, (3,)), (0x200, (3,))]),
        _math("clamp",   "clamp",   0x600, [(0x0, (3,)), (0x200, (3,)), (0x100, (3,))]),
    ]
    DataExecutor(ops, store).run()

    assert np.array_equal(store.read("tcm", 0x300), np.maximum(a, b))
    assert np.array_equal(store.read("tcm", 0x400), np.minimum(a, b))
    assert np.array_equal(store.read("tcm", 0x500), a * b + c)
    assert np.array_equal(
        store.read("tcm", 0x600), np.minimum(np.maximum(a, c), b)
    )


def test_math_softmax():
    store = MemoryStore()
    x = np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float32)
    store.write("tcm", 0x0, x)

    op = OpRecord(
        t_start=0.0, t_end=1.0,
        component_id="pe_math", op_kind="math", op_name="softmax",
        params={
            "op": "softmax",
            "input_addrs": [0x0], "input_shapes": [(2, 3)],
            "input_spaces": ["tcm"], "input_dtypes": ["f32"],
            "dst_addr": 0x100, "dst_space": "tcm",
            "shape_out": (2, 3), "dtype": "f32", "axis": -1,
        },
    )
    DataExecutor([op], store).run()
    expected = np.exp(x - x.max(axis=-1, keepdims=True))
    expected /= expected.sum(axis=-1, keepdims=True)
    assert np.allclose(store.read("tcm", 0x100), expected)


def test_math_add():
    store = MemoryStore()
    a = np.array([1.0, 2.0], dtype=np.float32)
    b = np.array([3.0, 4.0], dtype=np.float32)
    store.write("tcm", 0x0, a)
    store.write("tcm", 0x100, b)

    op = OpRecord(
        t_start=0.0, t_end=5.0,
        component_id="pe_math",
        op_kind="math", op_name="add",
        params={
            "op": "add",
            "input_addrs": [0x0, 0x100], "input_shapes": [(2,), (2,)],
            "dst_addr": 0x200, "shape_out": (2,),
            "dtype": "f32", "axis": None, "addr_space": "tcm",
        },
    )

    executor = DataExecutor([op], store)
    executor.run()

    result = store.read("tcm", 0x200)
    assert np.array_equal(result, np.array([4.0, 6.0], dtype=np.float32))


def test_math_sum_reduction():
    store = MemoryStore()
    x = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
    store.write("tcm", 0x0, x)

    op = OpRecord(
        t_start=0.0, t_end=5.0,
        component_id="pe_math",
        op_kind="math", op_name="sum",
        params={
            "op": "sum",
            "input_addrs": [0x0], "input_shapes": [(2, 2)],
            "dst_addr": 0x100, "shape_out": (1, 2),
            "dtype": "f32", "axis": 0, "addr_space": "tcm",
        },
    )

    executor = DataExecutor([op], store)
    executor.run()

    result = store.read("tcm", 0x100)
    assert np.array_equal(result, np.array([[4.0, 6.0]], dtype=np.float32))


def test_verify_pass():
    store = MemoryStore()
    store.write("hbm", 0x0, np.array([1.0, 2.0], dtype=np.float32))

    executor = DataExecutor([], store)
    results = executor.verify({
        ("hbm", 0x0): np.array([1.0, 2.0], dtype=np.float32),
    })
    assert results["hbm:0x0"] is True


def test_verify_fail():
    store = MemoryStore()
    store.write("hbm", 0x0, np.array([1.0, 2.0], dtype=np.float32))

    executor = DataExecutor([], store)
    results = executor.verify({
        ("hbm", 0x0): np.array([9.0, 9.0], dtype=np.float32),
    })
    assert results["hbm:0x0"] is False


def test_memory_ops_skipped():
    """Memory ops in op_log should be skipped (handled in Phase 1)."""
    store = MemoryStore()
    op = OpRecord(
        t_start=0.0, t_end=5.0,
        component_id="pe_dma",
        op_kind="memory", op_name="dma_read",
        params={"src_addr": 0x0, "nbytes": 64, "handle_id": "t0"},
    )
    # Should not raise
    executor = DataExecutor([op], store)
    executor.run()


def test_sequential_gemm_then_math():
    """GEMM output feeds into math op."""
    store = MemoryStore()
    a = np.eye(2, dtype=np.float16)
    b = np.ones((2, 2), dtype=np.float16)
    store.write("tcm", 0x0, a)
    store.write("tcm", 0x100, b)

    ops = [
        OpRecord(
            t_start=0.0, t_end=50.0,
            component_id="pe_gemm",
            op_kind="gemm", op_name="gemm_f16",
            params={
                "src_a_addr": 0x0, "src_b_addr": 0x100, "dst_addr": 0x200,
                "shape_a": (2, 2), "shape_b": (2, 2), "shape_out": (2, 2),
                "dtype_in": "f16", "dtype_acc": "f32", "dtype_out": "f32",
                "addr_space": "tcm",
            },
        ),
        OpRecord(
            t_start=50.0, t_end=55.0,
            component_id="pe_math",
            op_kind="math", op_name="exp",
            params={
                "op": "exp",
                "input_addrs": [0x200], "input_shapes": [(2, 2)],
                "dst_addr": 0x300, "shape_out": (2, 2),
                "dtype": "f32", "axis": None, "addr_space": "tcm",
            },
        ),
    ]

    executor = DataExecutor(ops, store)
    executor.run()

    gemm_result = store.read("tcm", 0x200)
    expected_gemm = (a.astype(np.float32) @ b.astype(np.float32)).astype(np.float32)
    assert np.allclose(gemm_result, expected_gemm)

    exp_result = store.read("tcm", 0x300)
    assert np.allclose(exp_result, np.exp(expected_gemm))


def test_parallel_same_timestamp_ops():
    """Multiple independent ops at the same t_start produce correct results
    when executed in parallel (ThreadPoolExecutor)."""
    store = MemoryStore()
    n_ops = 8
    # Each op: independent GEMM writing to a different address
    for i in range(n_ops):
        a = np.full((4, 4), float(i + 1), dtype=np.float16)
        b = np.eye(4, dtype=np.float16)
        store.write("tcm", 0x1000 * i, a)
        store.write("tcm", 0x1000 * i + 0x800, b)

    ops = [
        OpRecord(
            t_start=0.0, t_end=100.0,
            component_id=f"pe{i}.pe_gemm",
            op_kind="gemm", op_name="gemm_f16",
            params={
                "src_a_addr": 0x1000 * i,
                "src_b_addr": 0x1000 * i + 0x800,
                "dst_addr": 0x80000 + 0x1000 * i,
                "shape_a": (4, 4), "shape_b": (4, 4), "shape_out": (4, 4),
                "dtype_in": "f16", "dtype_acc": "f32", "dtype_out": "f16",
                "addr_space": "tcm",
            },
        )
        for i in range(n_ops)
    ]

    executor = DataExecutor(ops, store)
    executor.run()

    for i in range(n_ops):
        result = store.read("tcm", 0x80000 + 0x1000 * i)
        expected = np.full((4, 4), float(i + 1), dtype=np.float16)
        assert np.allclose(result, expected), f"op {i}: expected {expected}, got {result}"
