"""Adversarial verifier probes for the mattermost-ws-401-classify fix
(commit fdd1a11ac5).

The implementer's test_ws_auth_retry.py covers:
  - WSServerHandshakeError(status=401) stops the loop
  - a transient RuntimeError whose message contains "401" now retries
  - the pre-existing _closing early-return path is untouched

This file probes boundary/edge cases the implementer's tests did NOT
cover, per the independent-verifier mandate to go beyond what the
implementer thought to test:

  1. WSServerHandshakeError(status=403) — the other structured-check
     status value — must still stop the loop (only 401 was tested).
  2. WSServerHandshakeError with a non-auth status (e.g. 500) must NOT
     stop the loop — the structured check must not over-match.
  3. A transient exception containing "unauthorized" (not "401"/"403")
     must retry now that the substring fallback is fully removed —
     the implementer only exercised the "401" substring variant.
  4. Multiple consecutive transient errors must all retry (not just
     one) — proves the removed code path isn't silently reintroduced
     via some other mechanism after N attempts.
"""

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# Optional dep: CI installs [all] extras, but local envs may lack aiohttp —
# skip cleanly instead of crashing collection.
aiohttp = pytest.importorskip("aiohttp")

from plugins.platforms.mattermost.adapter import MattermostAdapter


def _make_adapter(closing: bool = False) -> MattermostAdapter:
    adapter = MattermostAdapter.__new__(MattermostAdapter)
    adapter._closing = closing
    # The genuine-auth path now escalates via _set_fatal_error +
    # _notify_fatal_error (OOF-156 follow-up), which touch these attributes
    # that __init__ would normally provide.
    from gateway.config import Platform

    adapter.platform = Platform.MATTERMOST
    adapter._running = True
    adapter._fatal_error_handler = None
    return adapter


class TestMattermostWSAuthRetryBoundaryProbes:
    def test_403_handshake_stops_reconnect(self):
        """status=403 (the other half of the structured check's {401, 403}
        set) must also stop the loop. The implementer only tested 401."""
        exc = aiohttp.WSServerHandshakeError(
            request_info=MagicMock(),
            history=(),
            status=403,
            message="Forbidden",
            headers=MagicMock(),
        )

        adapter = _make_adapter()
        call_count = 0

        async def fake_connect():
            nonlocal call_count
            call_count += 1
            raise exc

        adapter._ws_connect_and_listen = fake_connect

        asyncio.run(adapter._ws_loop())

        assert call_count == 1

    def test_non_auth_handshake_status_does_not_stop_reconnect(self):
        """A WSServerHandshakeError with a non-auth status (500) is a
        structured exception of the RIGHT TYPE but the WRONG status —
        it must NOT be classified as a permanent auth failure. This
        guards against an overly broad isinstance-only check that
        forgets to gate on .status."""
        exc = aiohttp.WSServerHandshakeError(
            request_info=MagicMock(),
            history=(),
            status=500,
            message="Internal Server Error",
            headers=MagicMock(),
        )

        adapter = _make_adapter()
        call_count = 0

        async def fake_connect():
            nonlocal call_count
            call_count += 1
            if call_count >= 2:
                adapter._closing = True
            raise exc

        adapter._ws_connect_and_listen = fake_connect

        with patch("asyncio.sleep", new=AsyncMock()):
            asyncio.run(adapter._ws_loop())

        assert call_count == 2

    def test_unauthorized_substring_no_longer_stops_reconnect(self):
        """Before the fix, a transient error whose message contained the
        word 'unauthorized' (not digits) would ALSO trip the removed
        substring fallback. The implementer's regression test only
        covered the '401' digit-substring case; this proves the
        'unauthorized' word variant is equally fixed."""
        adapter = _make_adapter()
        call_count = 0

        async def fake_connect():
            nonlocal call_count
            call_count += 1
            if call_count >= 2:
                adapter._closing = True
            raise RuntimeError(
                "upstream proxy replied: request unauthorized by WAF rule, retry"
            )

        adapter._ws_connect_and_listen = fake_connect

        with patch("asyncio.sleep", new=AsyncMock()):
            asyncio.run(adapter._ws_loop())

        assert call_count == 2

    def test_repeated_transient_errors_all_retry(self):
        """Guards against a fix that only relaxes classification for the
        FIRST occurrence (e.g. some hidden retry-budget/counter that
        starts rejecting after N attempts). Runs 5 consecutive
        transient errors and confirms every one retries."""
        adapter = _make_adapter()
        call_count = 0
        target_attempts = 5

        async def fake_connect():
            nonlocal call_count
            call_count += 1
            if call_count >= target_attempts:
                adapter._closing = True
            raise RuntimeError("403 seen in unrelated proxy diagnostic body")

        adapter._ws_connect_and_listen = fake_connect

        with patch("asyncio.sleep", new=AsyncMock()):
            asyncio.run(adapter._ws_loop())

        assert call_count == target_attempts
