"""
Tests for services/matrix-bridge-dagi/app/matrix_client.py

Uses monkeypatching of httpx.AsyncClient — no real Synapse required.
"""

import asyncio
import hashlib
import sys
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# Make the bridge app importable without installing
_BRIDGE = Path(__file__).parent.parent / "services" / "matrix-bridge-dagi"
if str(_BRIDGE) not in sys.path:
    sys.path.insert(0, str(_BRIDGE))

from app.matrix_client import MatrixClient, _LRUSet  # noqa: E402


def run(coro):
    """Helper: run async coroutine in sync test."""
    return asyncio.run(coro)


# ── Helpers ────────────────────────────────────────────────────────────────────

HS = "https://matrix.example.com"
TOKEN = "syt_test_token_abc123"
BOT = "@dagi_bridge:example.com"
ROOM = "!testroom:example.com"


def _make_client() -> MatrixClient:
    return MatrixClient(hs=HS, access_token=TOKEN, bot_user_id=BOT)  # type: ignore[call-arg]


def _make_client_direct() -> MatrixClient:
    return MatrixClient(HS, TOKEN, BOT)


def _fake_resp(status_code: int, body: dict) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = body
    resp.raise_for_status = MagicMock()
    if status_code >= 400:
        from httpx import HTTPStatusError, Request, Response
        resp.raise_for_status.side_effect = HTTPStatusError(
            "error", request=MagicMock(), response=resp
        )
    return resp


# ── LRUSet tests ───────────────────────────────────────────────────────────────

def test_lru_set_basic():
    s = _LRUSet(maxsize=3)
    assert not s.contains("a")
    s.add("a")
    assert s.contains("a")


def test_lru_set_eviction():
    s = _LRUSet(maxsize=2)
    s.add("a")
    s.add("b")
    s.add("c")  # evicts "a"
    assert not s.contains("a")
    assert s.contains("b")
    assert s.contains("c")


def test_lru_set_moves_to_end_on_access():
    s = _LRUSet(maxsize=2)
    s.add("a")
    s.add("b")
    s.contains("a")  # refreshes "a"
    s.add("c")       # evicts "b", not "a"
    assert s.contains("a")
    assert not s.contains("b")
    assert s.contains("c")


# ── make_txn_id ────────────────────────────────────────────────────────────────

def test_make_txn_id_deterministic():
    tid1 = MatrixClient.make_txn_id(ROOM, "$event1")
    tid2 = MatrixClient.make_txn_id(ROOM, "$event1")
    assert tid1 == tid2
    assert len(tid1) == 32  # first 32 hex chars of sha256


def test_make_txn_id_different_events():
    tid1 = MatrixClient.make_txn_id(ROOM, "$event1")
    tid2 = MatrixClient.make_txn_id(ROOM, "$event2")
    assert tid1 != tid2


def test_make_txn_id_different_rooms():
    tid1 = MatrixClient.make_txn_id("!room1:x", "$event1")
    tid2 = MatrixClient.make_txn_id("!room2:x", "$event1")
    assert tid1 != tid2


# ── Context manager guard ──────────────────────────────────────────────────────

def test_client_not_initialised_raises():
    async def _inner():
        client = _make_client_direct()
        with pytest.raises(RuntimeError, match="not initialised"):
            await client.whoami()
    run(_inner())


# ── whoami ─────────────────────────────────────────────────────────────────────

def test_whoami_success():
    async def _inner():
        client = _make_client_direct()
        async with client:
            client._client.request = AsyncMock(
                return_value=_fake_resp(200, {"user_id": BOT, "device_id": "DAGI1"})
            )
            result = await client.whoami()
        assert result["user_id"] == BOT
    run(_inner())


# ── join_room ──────────────────────────────────────────────────────────────────

def test_join_room_success():
    async def _inner():
        client = _make_client_direct()
        async with client:
            client._client.request = AsyncMock(
                return_value=_fake_resp(200, {"room_id": ROOM})
            )
            result = await client.join_room(ROOM)
        assert result["room_id"] == ROOM
    run(_inner())


# ── send_text ──────────────────────────────────────────────────────────────────

def test_send_text_success():
    async def _inner():
        client = _make_client_direct()
        event_id = "$out_event1"
        txn_id = MatrixClient.make_txn_id(ROOM, "$source_event1")
        async with client:
            client._client.request = AsyncMock(
                return_value=_fake_resp(200, {"event_id": event_id})
            )
            result = await client.send_text(ROOM, "Hello DAGI!", txn_id)
        assert result["event_id"] == event_id
    run(_inner())


def test_send_text_uses_correct_method_and_path():
    async def _inner():
        client = _make_client_direct()
        txn_id = MatrixClient.make_txn_id(ROOM, "$src")
        captured = {}

        async def fake_request(method, url, **kwargs):
            captured["method"] = method
            captured["url"] = url
            captured["json"] = kwargs.get("json")
            return _fake_resp(200, {"event_id": "$out"})

        async with client:
            client._client.request = fake_request
            await client.send_text(ROOM, "test", txn_id)

        assert captured["method"] == "PUT"
        assert "send/m.room.message" in captured["url"]
        assert txn_id in captured["url"]
        assert captured["json"]["msgtype"] == "m.text"
        assert captured["json"]["body"] == "test"
    run(_inner())


# ── sync_poll ──────────────────────────────────────────────────────────────────

def test_sync_poll_initial():
    async def _inner():
        client = _make_client_direct()
        sync_data = {
            "next_batch": "s1_token",
            "rooms": {"join": {ROOM: {"timeline": {"events": []}}}}
        }
        async with client:
            client._client.request = AsyncMock(
                return_value=_fake_resp(200, sync_data)
            )
            result = await client.sync_poll()
        assert result["next_batch"] == "s1_token"
    run(_inner())


def test_sync_poll_passes_since():
    async def _inner():
        client = _make_client_direct()
        captured = {}

        async def fake_request(method, url, **kwargs):
            captured["params"] = kwargs.get("params", {})
            return _fake_resp(200, {"next_batch": "s2"})

        async with client:
            client._client.request = fake_request
            await client.sync_poll(since="s1_token")

        assert captured["params"].get("since") == "s1_token"
    run(_inner())


# ── extract_room_messages ──────────────────────────────────────────────────────

def _make_sync_with_events(events: list) -> dict:
    return {
        "next_batch": "s_test",
        "rooms": {
            "join": {
                ROOM: {
                    "timeline": {
                        "events": events
                    }
                }
            }
        }
    }


def test_extract_filters_own_messages():
    client = _make_client_direct()
    sync = _make_sync_with_events([
        {
            "type": "m.room.message",
            "event_id": "$e1",
            "sender": BOT,  # own message
            "content": {"msgtype": "m.text", "body": "I said this"},
            "origin_server_ts": 1000,
        }
    ])
    msgs = client.extract_room_messages(sync, ROOM)
    assert msgs == []


def test_extract_filters_non_text():
    client = _make_client_direct()
    sync = _make_sync_with_events([
        {
            "type": "m.room.message",
            "event_id": "$e2",
            "sender": "@user:example.com",
            "content": {"msgtype": "m.image", "url": "mxc://..."},
            "origin_server_ts": 1000,
        }
    ])
    msgs = client.extract_room_messages(sync, ROOM)
    assert msgs == []


def test_extract_filters_duplicate_events():
    client = _make_client_direct()
    event = {
        "type": "m.room.message",
        "event_id": "$e3",
        "sender": "@user:example.com",
        "content": {"msgtype": "m.text", "body": "hello"},
        "origin_server_ts": 1000,
    }
    sync = _make_sync_with_events([event])

    client.mark_seen("$e3")
    msgs = client.extract_room_messages(sync, ROOM)
    assert msgs == []


def test_extract_returns_new_text_messages():
    client = _make_client_direct()
    events = [
        {
            "type": "m.room.message",
            "event_id": "$e4",
            "sender": "@user:example.com",
            "content": {"msgtype": "m.text", "body": "What is AgroMatrix?"},
            "origin_server_ts": 2000,
        },
        {
            "type": "m.room.message",
            "event_id": "$e5",
            "sender": "@user2:example.com",
            "content": {"msgtype": "m.text", "body": "Hello!"},
            "origin_server_ts": 3000,
        },
    ]
    sync = _make_sync_with_events(events)
    msgs = client.extract_room_messages(sync, ROOM)
    assert len(msgs) == 2
    assert msgs[0]["event_id"] == "$e4"
    assert msgs[1]["event_id"] == "$e5"


def test_extract_ignores_other_room():
    client = _make_client_direct()
    other_room = "!otherroom:example.com"
    sync = {
        "next_batch": "s_x",
        "rooms": {
            "join": {
                other_room: {
                    "timeline": {
                        "events": [{
                            "type": "m.room.message",
                            "event_id": "$e6",
                            "sender": "@user:example.com",
                            "content": {"msgtype": "m.text", "body": "hi"},
                            "origin_server_ts": 1000,
                        }]
                    }
                }
            }
        }
    }
    msgs = client.extract_room_messages(sync, ROOM)
    assert msgs == []


# ── Retry on 429 ───────────────────────────────────────────────────────────────

def test_retry_on_rate_limit():
    async def _inner():
        client = _make_client_direct()
        call_count = 0

        async def fake_request(method, url, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                return _fake_resp(429, {"retry_after_ms": 10})
            return _fake_resp(200, {"user_id": BOT})

        async with client:
            client._client.request = fake_request
            with patch("asyncio.sleep", new_callable=AsyncMock):
                result = await client.whoami()

        assert result["user_id"] == BOT
        assert call_count == 3
    run(_inner())
