"""Phase 5A load test: 100 concurrent marketplace requests (issue #307).

Satisfies Phase 5A load-test acceptance criterion: under 100 concurrent
dispatches to marketplace (remote) backends, all complete (or defined
timeout/success mix), and billing receives one event per successful request.

Run with: pytest tests/test_marketplace_load.py -v
Exclude from fast CI: pytest -m 'not slow and not performance'
"""

from __future__ import annotations

import asyncio
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock

import httpx
import pytest

from llmhosts.billing.store import BillingStore
from llmhosts.proxy.dispatcher import BackendDispatcher
from llmhosts.proxy.models import UnifiedMessage, UnifiedRequest

_OPENAI_COMPAT_RESPONSE = {
    "id": "chatcmpl-load-1",
    "object": "chat.completion",
    "created": 1234567890,
    "model": "llama3.2:8b",
    "choices": [
        {
            "index": 0,
            "message": {"role": "assistant", "content": "OK"},
            "finish_reason": "stop",
        }
    ],
    "usage": {"prompt_tokens": 2, "completion_tokens": 1, "total_tokens": 3},
}


@pytest.mark.performance
@pytest.mark.slow
@pytest.mark.unit
async def test_100_concurrent_marketplace_requests_all_complete_and_billing_events() -> None:
    """100 concurrent dispatches to mock remote backends; all complete; BillingStore gets 100 events.

    Phase 5A load-test acceptance: dispatcher handles 100 concurrent marketplace
    requests with correct routing and billing. Uses asyncio.gather; mock httpx
    so no real network I/O. When BillingStore is attached, assert it received
    exactly 100 events (one per successful dispatch).
    """
    with tempfile.TemporaryDirectory() as tmp:
        db_path = Path(tmp) / "billing.db"
        store = BillingStore(db_path)
        await store.initialize()

        dispatcher = BackendDispatcher()
        await dispatcher.startup()

        # 2-3 mock remote backends with overlapping model so router can spread load
        bases = [
            "https://remote-load-a.example.com",
            "https://remote-load-b.example.com",
            "https://remote-load-c.example.com",
        ]
        for i, base in enumerate(bases):
            dispatcher.register_remote_backend(
                tunnel_url=base,
                name=f"remote-load-{i}",
                models=["llama3.2:8b"],
                device_id=f"dev-{i}",
            )
        dispatcher.attach_billing_store(store)

        # Mock POST for each backend's pool so every dispatch succeeds
        def make_fake_post(base: str):
            async def _fake_post(url: str, **kwargs: object) -> httpx.Response:
                assert url == f"{base}/v1/chat/completions"
                return httpx.Response(
                    status_code=200,
                    json=_OPENAI_COMPAT_RESPONSE,
                    request=httpx.Request("POST", url),
                )

            return _fake_post

        remote_entries = [b for b in dispatcher._backends if b.backend_type == "remote"]
        assert len(remote_entries) == 3
        for entry in remote_entries:
            client = dispatcher._get_or_create_pool(entry)
            client.post = AsyncMock(side_effect=make_fake_post(entry.base_url))  # type: ignore[method-assign]

        unified_request = UnifiedRequest(
            model="llama3.2:8b",
            messages=[UnifiedMessage(role="user", content="Hi")],
            stream=False,
        )

        async def one_dispatch(idx: int):
            return await dispatcher.dispatch(
                unified_request,
                request_id=f"load-req-{idx:03d}",
                api_key_id="key-load",
            )

        tasks = [one_dispatch(i) for i in range(100)]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        # Expected: all 100 complete (no exception); with mocks no timeouts
        completed = [r for r in results if not isinstance(r, Exception)]
        exceptions = [r for r in results if isinstance(r, Exception)]
        assert len(exceptions) == 0, f"Expected no exceptions, got: {exceptions[:5]}"
        assert len(completed) == 100, f"Expected 100 completions, got {len(completed)}"

        for resp in completed:
            assert resp.content == "OK"
            assert resp.total_tokens == 3

        # BillingStore must have received 100 events (one per successful dispatch)
        events = await store.get_last_n(100)
        await store.close()
        assert len(events) == 100, f"Expected 100 billing events, got {len(events)}"
        assert all(e.source == "marketplace" for e in events)

        await dispatcher.shutdown()
