"""Tests for the typed single-item command helpers (CLI-04).

Covers the parity-critical MXAccess commands promoted from raw ``Invoke`` to
typed async session helpers: ``advise_supervisory``, ``write_secured`` /
``write_secured2``, ``authenticate_user``, ``archestra_user_to_id``, and the
buffered/suspend/activate family. The credential-redaction contract for the
secured/auth helpers is asserted explicitly.
"""

from __future__ import annotations

from typing import Any

import pytest

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


class FakeUnary:
    """Records requests and pops scripted replies, matching the client's call shape."""

    def __init__(self, replies: list[Any]) -> None:
        self.replies = 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 FakeGatewayStub:
    """Minimal stub: a fixed open-session reply plus a scriptable invoke queue."""

    def __init__(self, invoke_replies: list[Any]) -> None:
        self.open_session = FakeUnary(
            [
                pb.OpenSessionReply(
                    session_id="session-1",
                    protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
                ),
            ],
        )
        self.invoke = FakeUnary(invoke_replies)
        self.OpenSession = self.open_session
        self.Invoke = self.invoke


async def _session_with(invoke_replies: list[Any]):
    stub = FakeGatewayStub(invoke_replies)
    client = await GatewayClient.connect(
        ClientOptions(endpoint="fake", api_key="mxgw_test_secret", plaintext=True),
        stub=stub,
    )
    session = await client.open_session()
    return session, stub


def _ok(kind: "pb.MxCommandKind.ValueType", **payload: Any) -> pb.MxCommandReply:
    return pb.MxCommandReply(
        session_id="session-1",
        kind=kind,
        protocol_status=pb.ProtocolStatus(code=pb.PROTOCOL_STATUS_CODE_OK),
        **payload,
    )


@pytest.mark.asyncio
async def test_advise_supervisory_sends_typed_command() -> None:
    session, stub = await _session_with([_ok(pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY)])

    await session.advise_supervisory(12, 34)

    command = stub.invoke.requests[0].command
    assert command.kind == pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY
    assert command.advise_supervisory.server_handle == 12
    assert command.advise_supervisory.item_handle == 34


@pytest.mark.asyncio
async def test_authenticate_user_returns_user_id_and_sends_credentials() -> None:
    session, stub = await _session_with(
        [_ok(pb.MX_COMMAND_KIND_AUTHENTICATE_USER, authenticate_user=pb.AuthenticateUserReply(user_id=77))],
    )

    user_id = await session.authenticate_user(12, "operator", "s3cr3t-pw")

    assert user_id == 77
    command = stub.invoke.requests[0].command
    assert command.kind == pb.MX_COMMAND_KIND_AUTHENTICATE_USER
    assert command.authenticate_user.verify_user == "operator"
    # The credential is carried on the wire (redaction is about logs/errors, not the RPC).
    assert command.authenticate_user.verify_user_password == "s3cr3t-pw"


@pytest.mark.asyncio
async def test_authenticate_user_scrubs_credential_from_surfaced_error() -> None:
    password = "super-secret-pw"
    failure = pb.MxCommandReply(
        session_id="session-1",
        kind=pb.MX_COMMAND_KIND_AUTHENTICATE_USER,
        protocol_status=pb.ProtocolStatus(
            code=pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE,
            # Simulate a gateway that unwisely echoed the credential back in the message.
            message=f"authentication failed for password {password}",
        ),
        hresult=-1,
    )
    session, _ = await _session_with([failure])

    with pytest.raises(MxAccessError) as captured:
        await session.authenticate_user(12, "operator", password)

    assert password not in str(captured.value)
    assert "[redacted]" in str(captured.value)


@pytest.mark.asyncio
async def test_write_secured_surfaces_native_failure_without_prior_authenticate() -> None:
    """Parity: WriteSecured failing before authenticate/advise-supervisory is surfaced as-is."""
    secret_value = "priv-payload"
    failure = pb.MxCommandReply(
        session_id="session-1",
        kind=pb.MX_COMMAND_KIND_WRITE_SECURED,
        protocol_status=pb.ProtocolStatus(
            code=pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE,
            message=f"WriteSecured rejected value {secret_value}",
        ),
        hresult=-2147217407,
    )
    session, stub = await _session_with([failure])

    with pytest.raises(MxAccessError) as captured:
        await session.write_secured(12, 34, secret_value, current_user_id=5, verifier_user_id=6)

    # Native failure is surfaced (not "fixed"): the raw reply's structure is
    # preserved so callers still see the native verdict...
    raw = captured.value.raw_reply
    assert raw is not None
    assert raw.kind == pb.MX_COMMAND_KIND_WRITE_SECURED
    assert raw.hresult == -2147217407
    assert raw.protocol_status.code == pb.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
    # ...but the credential-sensitive value is scrubbed from the surfaced message
    # AND from the reply's echoed diagnostics, so a logger dumping raw_reply's
    # structured fields cannot reintroduce the leak.
    assert secret_value not in str(captured.value)
    assert secret_value not in raw.protocol_status.message
    assert "[redacted]" in raw.protocol_status.message
    command = stub.invoke.requests[0].command
    assert command.kind == pb.MX_COMMAND_KIND_WRITE_SECURED
    assert command.write_secured.current_user_id == 5
    assert command.write_secured.verifier_user_id == 6


@pytest.mark.asyncio
async def test_write_secured2_sends_value_and_timestamp() -> None:
    from datetime import datetime, timezone

    session, stub = await _session_with([_ok(pb.MX_COMMAND_KIND_WRITE_SECURED2)])

    stamp = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
    await session.write_secured2(12, 34, 42, stamp, current_user_id=5, verifier_user_id=6)

    command = stub.invoke.requests[0].command
    assert command.kind == pb.MX_COMMAND_KIND_WRITE_SECURED2
    assert command.write_secured2.value.int32_value == 42
    assert command.write_secured2.HasField("timestamp_value")


@pytest.mark.asyncio
async def test_archestra_user_to_id_returns_user_id() -> None:
    session, stub = await _session_with(
        [_ok(pb.MX_COMMAND_KIND_ARCHESTRA_USER_TO_ID, archestra_user_to_id=pb.ArchestrAUserToIdReply(user_id=9))],
    )

    user_id = await session.archestra_user_to_id(12, "guid-123")

    assert user_id == 9
    assert stub.invoke.requests[0].command.archestra_user_to_id.user_id_guid == "guid-123"


@pytest.mark.asyncio
async def test_add_buffered_item_returns_item_handle() -> None:
    session, stub = await _session_with(
        [_ok(pb.MX_COMMAND_KIND_ADD_BUFFERED_ITEM, add_buffered_item=pb.AddBufferedItemReply(item_handle=55))],
    )

    item_handle = await session.add_buffered_item(12, "Object.Attribute", "ctx")

    assert item_handle == 55
    command = stub.invoke.requests[0].command
    assert command.add_buffered_item.item_definition == "Object.Attribute"
    assert command.add_buffered_item.item_context == "ctx"


@pytest.mark.asyncio
async def test_set_buffered_update_interval_sends_command() -> None:
    session, stub = await _session_with([_ok(pb.MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL)])

    await session.set_buffered_update_interval(12, 250)

    command = stub.invoke.requests[0].command
    assert command.kind == pb.MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL
    assert command.set_buffered_update_interval.update_interval_milliseconds == 250


@pytest.mark.asyncio
async def test_suspend_and_activate_return_status() -> None:
    session, _ = await _session_with(
        [
            _ok(
                pb.MX_COMMAND_KIND_SUSPEND,
                suspend=pb.SuspendReply(status=pb.MxStatusProxy(category=pb.MX_STATUS_CATEGORY_OK)),
            ),
        ],
    )
    status = await session.suspend(12, 34)
    assert status.category == pb.MX_STATUS_CATEGORY_OK

    session, _ = await _session_with(
        [
            _ok(
                pb.MX_COMMAND_KIND_ACTIVATE,
                activate=pb.ActivateReply(status=pb.MxStatusProxy(category=pb.MX_STATUS_CATEGORY_OK)),
            ),
        ],
    )
    status = await session.activate(12, 34)
    assert status.category == pb.MX_STATUS_CATEGORY_OK
