"""Tests for Session.write_array_elements default-fill sparse-array helper."""

from __future__ import annotations

from typing import Any

import pytest

from zb_mom_ww_mxgateway import ClientOptions, GatewayClient
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_sparse_mx_value(
    element_data_type: "pb.MxDataType.ValueType",
    total_length: int,
    elements: dict[int, Any],
) -> pb.MxValue:
    """Build an MxValue wrapping an MxSparseArray from Python primitives.

    Mirrors the logic inside Session.write_array_elements so tests can assert
    the exact wire shape the helper produces without going through the full
    gRPC stack.
    """
    from zb_mom_ww_mxgateway.values import to_mx_value

    return pb.MxValue(
        sparse_array_value=pb.MxSparseArray(
            element_data_type=element_data_type,
            total_length=total_length,
            elements=[
                pb.MxSparseElement(index=idx, value=to_mx_value(val))
                for idx, val in elements.items()
            ],
        )
    )


# ---------------------------------------------------------------------------
# Fake stub (minimal — only needs Invoke / OpenSession)
# ---------------------------------------------------------------------------


class _FakeUnary:
    def __init__(self, replies: list[Any]) -> None:
        self.replies = list(replies)
        self.requests: list[Any] = []
        self.metadata: tuple[tuple[str, str], ...] | None = None

    async def __call__(
        self,
        request: Any,
        *,
        metadata: tuple[tuple[str, str], ...],
    ) -> Any:
        self.requests.append(request)
        self.metadata = metadata
        return self.replies.pop(0)


class _FakeStub:
    """Minimal stub that satisfies GatewayClient for a single invoke round-trip."""

    def __init__(self) -> None:
        ok = pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK)
        self.open_session = _FakeUnary([pb.OpenSessionReply(session_id="s1", protocol_status=ok)])
        self.invoke = _FakeUnary(
            [
                pb.MxCommandReply(
                    session_id="s1",
                    kind=pb.MX_COMMAND_KIND_WRITE,
                    protocol_status=ok,
                ),
            ]
        )
        self.OpenSession = self.open_session
        self.Invoke = self.invoke


# ---------------------------------------------------------------------------
# Unit tests
# ---------------------------------------------------------------------------


def test_sparse_mx_value_builder_sets_correct_oneof() -> None:
    """Builder helper must produce an MxValue with kind == 'sparse_array_value'."""
    mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 5, {0: 10, 3: 30})
    assert mv.WhichOneof("kind") == "sparse_array_value"


def test_sparse_mx_value_builder_total_length() -> None:
    """total_length must equal the value passed to the builder."""
    mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 20, {1: 7})
    assert mv.sparse_array_value.total_length == 20


def test_sparse_mx_value_builder_element_count_and_values() -> None:
    """Elements list length and scalar values must match the input dict."""
    mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_INTEGER, 10, {0: 11, 4: 55, 9: 99})
    sa = mv.sparse_array_value
    assert len(sa.elements) == 3
    by_index = {e.index: e.value for e in sa.elements}
    assert by_index[0].int32_value == 11
    assert by_index[4].int32_value == 55
    assert by_index[9].int32_value == 99


def test_sparse_mx_value_builder_element_data_type() -> None:
    """element_data_type must be forwarded verbatim."""
    mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_FLOAT, 3, {})
    assert mv.sparse_array_value.element_data_type == pb.MX_DATA_TYPE_FLOAT


def test_sparse_mx_value_builder_empty_elements() -> None:
    """An empty elements dict must still produce a valid MxSparseArray."""
    mv = _make_sparse_mx_value(pb.MX_DATA_TYPE_BOOLEAN, 8, {})
    sa = mv.sparse_array_value
    assert len(sa.elements) == 0
    assert sa.total_length == 8


# ---------------------------------------------------------------------------
# Integration-level: write_array_elements routes through Session.write
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_write_array_elements_sends_sparse_array_write_command() -> None:
    """write_array_elements must send a WRITE command whose value is sparse_array_value."""
    stub = _FakeStub()
    client = await GatewayClient.connect(
        ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
        stub=stub,
    )
    session = await client.open_session()

    await session.write_array_elements(
        server_handle=1,
        item_handle=2,
        element_data_type=pb.MX_DATA_TYPE_INTEGER,
        total_length=10,
        elements={0: 100, 5: 500},
    )

    assert len(stub.invoke.requests) == 1
    cmd_req: pb.MxCommandRequest = stub.invoke.requests[0]
    cmd = cmd_req.command
    assert cmd.kind == pb.MX_COMMAND_KIND_WRITE
    mv = cmd.write.value
    assert mv.WhichOneof("kind") == "sparse_array_value"

    sa = mv.sparse_array_value
    assert sa.element_data_type == pb.MX_DATA_TYPE_INTEGER
    assert sa.total_length == 10
    assert len(sa.elements) == 2
    by_index = {e.index: e.value for e in sa.elements}
    assert by_index[0].int32_value == 100
    assert by_index[5].int32_value == 500


@pytest.mark.asyncio
async def test_write_array_elements_forwards_user_id() -> None:
    """user_id must reach the WriteCommand."""
    stub = _FakeStub()
    client = await GatewayClient.connect(
        ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
        stub=stub,
    )
    session = await client.open_session()

    await session.write_array_elements(
        server_handle=1,
        item_handle=2,
        element_data_type=pb.MX_DATA_TYPE_BOOLEAN,
        total_length=4,
        elements={},
        user_id=42,
    )

    cmd = stub.invoke.requests[0].command
    assert cmd.write.user_id == 42


@pytest.mark.asyncio
async def test_write_array_elements_string_elements() -> None:
    """String element values must be encoded as string_value scalars."""
    stub = _FakeStub()
    client = await GatewayClient.connect(
        ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
        stub=stub,
    )
    session = await client.open_session()

    await session.write_array_elements(
        server_handle=1,
        item_handle=2,
        element_data_type=pb.MX_DATA_TYPE_STRING,
        total_length=3,
        elements={1: "hello", 2: "world"},
    )

    sa = stub.invoke.requests[0].command.write.value.sparse_array_value
    by_index = {e.index: e.value for e in sa.elements}
    assert by_index[1].string_value == "hello"
    assert by_index[2].string_value == "world"
