"""DAVE receive-side decrypt tests for the vendored voice-recv fork.

Exercises Lane A's patch on
`vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py`:

* `_maybe_dave_decrypt(rtp_packet)` — DAVE E2E layer sandwiched between the
  transport-layer decrypt and the routing into the opus decoder. No-op when
  the room is non-DAVE, when davey isn't installed, or when the SSRC map
  hasn't caught up to a new speaker yet.
* `callback()` hook — feeds the DAVE-unwrapped plaintext into
  `packet_router.feed_rtp()` on success, drops the packet on failure WITHOUT
  killing the reader thread.

The test fixtures mirror `tests/test_voice_session_cleanup.py:33-54`:
  * Construct `AudioReader` via `AudioReader.__new__(AudioReader)` + manual
    attr set so the reader thread is never started.
  * `MagicMock` everything below the unit under test.

`_HAS_DAVE` / `_MEDIA_TYPE_AUDIO` on the reader module are monkey-patched per
test so the suite passes whether or not `davey` is importable in the venv.
The assertions only become meaningful once Lane A's patch has landed and the
package has been re-installed (`pip install -e vendor/discord-ext-voice-recv
--force-reinstall`); the FILE itself is valid Python regardless.

See plan: /home/moltbot/.claude/plans/wiggly-exploring-glade.md
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from discord.ext.voice_recv.reader import AudioReader


# Sentinel for `_MEDIA_TYPE_AUDIO`. Using a plain object() keeps the tests
# independent of whether davey is importable — we just assert the value
# flows through to `dave_session.decrypt()` unchanged.
_FAKE_MEDIA_TYPE_AUDIO = object()


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def fake_dave_session():
    sess = MagicMock(name="dave_session")
    sess.ready = True
    # Default: this user is NOT in passthrough — DAVE decrypt must run.
    # Individual tests can override to True to exercise the passthrough path.
    sess.can_passthrough = MagicMock(return_value=False)
    sess.decrypt = MagicMock(return_value=b"plaintext_opus")
    return sess


@pytest.fixture
def fake_connection(fake_dave_session):
    conn = MagicMock(name="_connection")
    conn.dave_protocol_version = 1
    conn.dave_session = fake_dave_session
    return conn


@pytest.fixture
def fake_voice_client(fake_connection):
    vc = MagicMock(name="voice_client")
    vc._connection = fake_connection
    vc._ssrc_to_id = {12345: 999_000}
    return vc


@pytest.fixture
def fake_rtp_packet():
    pkt = MagicMock(name="rtp_packet")
    pkt.ssrc = 12345
    pkt.decrypted_data = b"ciphertext_after_transport_decrypt"
    pkt.is_silence = MagicMock(return_value=False)
    return pkt


@pytest.fixture
def reader(fake_voice_client):
    """`AudioReader` instance with no reader thread spawned.

    Same pattern used by `tests/test_voice_session_cleanup.py` for
    `VoiceSession` — bypass `__init__` so we can drive the public surface
    against pure mocks.
    """
    r = AudioReader.__new__(AudioReader)
    r.voice_client = fake_voice_client
    r.error = None
    return r


@pytest.fixture
def dave_enabled(monkeypatch):
    """Force the reader module's DAVE-availability flags ON.

    Pins `_MEDIA_TYPE_AUDIO` to a known sentinel so the happy-path test can
    assert exactly what gets passed to `dave_session.decrypt`. `raising=False`
    keeps the monkeypatch valid even if Lane A's patch hasn't landed yet —
    the tests will still fail (no `_maybe_dave_decrypt` attr), just for the
    right reason.
    """
    import discord.ext.voice_recv.reader as reader_mod

    monkeypatch.setattr(reader_mod, "_HAS_DAVE", True, raising=False)
    monkeypatch.setattr(
        reader_mod, "_MEDIA_TYPE_AUDIO", _FAKE_MEDIA_TYPE_AUDIO, raising=False
    )
    return reader_mod


# ---------------------------------------------------------------------------
# Unit tests: `_maybe_dave_decrypt`
# ---------------------------------------------------------------------------


class TestMaybeDaveDecrypt:
    """Seven unit tests on the DAVE-decrypt gate.

    The gate mirrors `voice_client.can_encrypt` in discord.py 2.7.1 exactly
    (`voice_state.py:272-273`). Bypass semantics on every "DAVE inactive"
    branch let non-DAVE rooms and davey-less environments keep working.
    """

    def test_protocol_version_zero_bypasses_decrypt(
        self, dave_enabled, reader, fake_connection, fake_dave_session, fake_rtp_packet,
    ):
        """`dave_protocol_version == 0` → return the transport-decrypted
        payload unchanged; never touch `dave_session.decrypt`."""
        fake_connection.dave_protocol_version = 0
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is fake_rtp_packet.decrypted_data
        fake_dave_session.decrypt.assert_not_called()

    def test_dave_session_none_bypasses_decrypt(
        self, dave_enabled, reader, fake_connection, fake_rtp_packet,
    ):
        """`dave_session is None` → bypass. Pre-MLS-handshake state."""
        fake_connection.dave_session = None
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is fake_rtp_packet.decrypted_data

    def test_dave_session_not_ready_bypasses_decrypt(
        self, dave_enabled, reader, fake_dave_session, fake_rtp_packet,
    ):
        """`dave_session.ready is False` → bypass. Pre-MLS-epoch-1 packets
        are transport-only on the wire."""
        fake_dave_session.ready = False
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is fake_rtp_packet.decrypted_data
        fake_dave_session.decrypt.assert_not_called()

    def test_unknown_ssrc_returns_none(
        self, dave_enabled, reader, fake_voice_client, fake_dave_session, fake_rtp_packet,
    ):
        """SSRC not in `_ssrc_to_id` → drop (return None).

        Accepted regression: davey requires per-user keys; when SPEAKING
        events race behind the first audio packet, 1-5 packets per new
        speaker per session are dropped. See plan §Edge cases.
        """
        fake_voice_client._ssrc_to_id.clear()
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is None
        fake_dave_session.decrypt.assert_not_called()

    def test_happy_path_invokes_decrypt_and_returns_plaintext(
        self, dave_enabled, reader, fake_dave_session, fake_rtp_packet,
    ):
        """Full DAVE-active path: `decrypt(user_id, MediaType.audio, ciphertext)`
        called exactly once with the expected args; method returns the
        davey plaintext bytes verbatim."""
        ciphertext = fake_rtp_packet.decrypted_data
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result == b"plaintext_opus"
        fake_dave_session.decrypt.assert_called_once_with(
            999_000, _FAKE_MEDIA_TYPE_AUDIO, ciphertext,
        )

    def test_decrypt_raises_returns_none_no_crash(
        self, dave_enabled, reader, fake_dave_session, fake_rtp_packet,
    ):
        """davey.decrypt raising → drop the packet, don't propagate, and
        leave `reader.error` untouched so the reader thread stays alive.

        MLS epoch transitions can produce transient decrypt failures —
        bumping `reader.error` would call `self.stop()` and kill the whole
        receive pipeline."""
        fake_dave_session.decrypt.side_effect = RuntimeError(
            "simulated MLS epoch transition fail"
        )
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is None
        assert reader.error is None

    def test_has_dave_false_bypasses_even_with_session_present(
        self, monkeypatch, reader, fake_dave_session, fake_rtp_packet,
    ):
        """`_HAS_DAVE = False` → bypass everything, even if a real session
        somehow showed up on the connection. Defensive shim that keeps the
        tests (and any davey-less deploys) green."""
        import discord.ext.voice_recv.reader as reader_mod

        monkeypatch.setattr(reader_mod, "_HAS_DAVE", False, raising=False)
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is fake_rtp_packet.decrypted_data
        fake_dave_session.decrypt.assert_not_called()

    def test_can_passthrough_true_returns_payload_without_decrypt(
        self, dave_enabled, reader, fake_dave_session, fake_rtp_packet,
    ):
        """`can_passthrough(user_id) == True` → return the transport-decrypted
        payload as-is; never call `decrypt`. Mirrors Discord's protocol where
        a passthrough-mode peer sends non-DAVE-wrapped packets that the
        receiver must accept verbatim."""
        fake_dave_session.can_passthrough = MagicMock(return_value=True)
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result is fake_rtp_packet.decrypted_data
        fake_dave_session.can_passthrough.assert_called_once_with(999_000)
        fake_dave_session.decrypt.assert_not_called()

    def test_can_passthrough_raises_falls_through_to_decrypt(
        self, dave_enabled, reader, fake_dave_session, fake_rtp_packet,
    ):
        """`can_passthrough` raising → swallow the error and try `decrypt`.
        Defensive: an older davey build or transient internal state shouldn't
        break the receive pipeline."""
        fake_dave_session.can_passthrough = MagicMock(
            side_effect=RuntimeError("simulated davey internal error")
        )
        result = reader._maybe_dave_decrypt(fake_rtp_packet)
        assert result == b"plaintext_opus"
        fake_dave_session.decrypt.assert_called_once()


# ---------------------------------------------------------------------------
# Integration tests: `callback()` exercises the DAVE hook
# ---------------------------------------------------------------------------


class TestCallbackIntegration:
    """Two integration tests for the hook Lane A inserts between transport
    decrypt (reader.py:141) and the post-decrypt routing (reader.py:159).

    Strategy: stub the transport-decrypt and RTP parsing path so `callback()`
    reaches the hook, then mock `_maybe_dave_decrypt` directly on the reader
    instance. The assertion focuses on `feed_rtp` being called (test 8) vs.
    not called (test 9). The transport path correctness is covered by
    voice-recv's own upstream tests.
    """

    @staticmethod
    def _wire_callback(reader, monkeypatch, fake_rtp_packet):
        import discord.ext.voice_recv.reader as reader_mod

        # Redirect rtp parsing — we want an RTP path (not RTCP) so the hook fires.
        monkeypatch.setattr(reader_mod.rtp, "is_rtcp", lambda data: False)
        monkeypatch.setattr(reader_mod.rtp, "decode_rtp", lambda data: fake_rtp_packet)

        # Stub the instance attrs `callback()` touches besides the hook.
        reader.decryptor = MagicMock(name="decryptor")
        reader.decryptor.decrypt_rtp = MagicMock(return_value=b"ciphertext")
        reader.packet_router = MagicMock(name="packet_router")
        reader.packet_router.feed_rtp = MagicMock()
        reader.speaking_timer = MagicMock(name="speaking_timer")
        reader.sink = MagicMock(name="sink")

    def test_callback_feeds_when_dave_returns_bytes(
        self, monkeypatch, reader, fake_rtp_packet,
    ):
        """Hook returns plaintext → `feed_rtp` called once with the
        rtp_packet whose `decrypted_data` is now the post-DAVE plaintext."""
        self._wire_callback(reader, monkeypatch, fake_rtp_packet)
        plaintext = b"dave_unwrapped_opus_payload"
        reader._maybe_dave_decrypt = MagicMock(return_value=plaintext)

        reader.callback(b"raw_packet_bytes")

        reader._maybe_dave_decrypt.assert_called_once_with(fake_rtp_packet)
        assert reader.packet_router.feed_rtp.call_count == 1
        called_with = reader.packet_router.feed_rtp.call_args[0][0]
        assert called_with is fake_rtp_packet
        assert fake_rtp_packet.decrypted_data == plaintext
        assert reader.error is None

    def test_callback_drops_when_dave_returns_none(
        self, monkeypatch, reader, fake_rtp_packet,
    ):
        """Hook returns None → `feed_rtp` NOT called, no exception propagated,
        `reader.error` stays None (reader thread survives the drop)."""
        self._wire_callback(reader, monkeypatch, fake_rtp_packet)
        reader._maybe_dave_decrypt = MagicMock(return_value=None)

        reader.callback(b"raw_packet_bytes")

        reader._maybe_dave_decrypt.assert_called_once_with(fake_rtp_packet)
        reader.packet_router.feed_rtp.assert_not_called()
        assert reader.error is None
