"""Phase 5: Billing events — metering and Stripe foundation (remote/marketplace)."""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock

import httpx
import pytest

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

# ---------------------------------------------------------------------------
# BillingEvent model
# ---------------------------------------------------------------------------

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


@pytest.mark.unit
async def test_dispatch_remote_billing_callback_receives_event_dict() -> None:
    """When billing callback is attached, successful remote dispatch invokes it with event dict."""
    dispatcher = BackendDispatcher()
    await dispatcher.startup()

    tunnel_base = "https://remote-billing.example.com"
    dispatcher.register_remote_backend(
        tunnel_url=tunnel_base,
        name="remote-billing",
        models=["llama3.2:8b"],
        device_id="device-123",
    )

    remote_entry = next(b for b in dispatcher._backends if b.backend_type == "remote")
    client = dispatcher._get_or_create_pool(remote_entry)
    client.post = AsyncMock(
        return_value=httpx.Response(
            status_code=200,
            json=_OPENAI_COMPAT_RESPONSE,
            request=httpx.Request("POST", f"{tunnel_base}/v1/chat/completions"),
        )
    )

    events: list[dict[str, Any]] = []

    def _capture(event: dict[str, Any]) -> None:
        events.append(event)

    dispatcher.attach_billing_callback(_capture)

    req = UnifiedRequest(
        model="llama3.2:8b",
        messages=[UnifiedMessage(role="user", content="Hi")],
        stream=False,
    )
    await dispatcher.dispatch(
        req,
        request_id="req-456",
        api_key_id="key-789",
        user_id="user-abc",
    )

    assert len(events) == 1
    ev = events[0]
    assert ev["request_id"] == "req-456"
    assert ev["api_key_id"] == "key-789"
    assert ev["user_id"] == "user-abc"
    assert ev["device_id"] == "device-123"
    assert ev["model"] == "llama3.2:8b"
    assert ev["prompt_tokens"] == 5
    assert ev["completion_tokens"] == 3
    assert ev["total_tokens"] == 8
    assert ev["source"] == "marketplace"
    assert "timestamp" in ev

    await dispatcher.shutdown()


@pytest.mark.unit
async def test_dispatch_remote_billing_store_appends_event() -> None:
    """When BillingStore is attached, successful remote dispatch appends one event."""
    with tempfile.TemporaryDirectory() as tmp:
        db_path = Path(tmp) / "billing.db"
        store = BillingStore(db_path)
        await store.initialize()

        dispatcher = BackendDispatcher()
        await dispatcher.startup()

        tunnel_base = "https://remote-store.example.com"
        dispatcher.register_remote_backend(
            tunnel_url=tunnel_base,
            name="remote-store",
            models=["llama3.2:8b"],
            device_id="dev-999",
        )

        remote_entry = next(b for b in dispatcher._backends if b.backend_type == "remote")
        client = dispatcher._get_or_create_pool(remote_entry)
        client.post = AsyncMock(
            return_value=httpx.Response(
                status_code=200,
                json=_OPENAI_COMPAT_RESPONSE,
                request=httpx.Request("POST", f"{tunnel_base}/v1/chat/completions"),
            )
        )

        dispatcher.attach_billing_store(store)

        req = UnifiedRequest(
            model="llama3.2:8b",
            messages=[UnifiedMessage(role="user", content="Hi")],
            stream=False,
        )
        await dispatcher.dispatch(req, request_id="req-store-1", api_key_id="key-x")

        last = await store.get_last_n(1)
        await store.close()

    assert len(last) == 1
    ev = last[0]
    assert ev.request_id == "req-store-1"
    assert ev.api_key_id == "key-x"
    assert ev.device_id == "dev-999"
    assert ev.model == "llama3.2:8b"
    assert ev.prompt_tokens == 5
    assert ev.completion_tokens == 3
    assert ev.total_tokens == 8
    assert ev.source == "marketplace"

    await dispatcher.shutdown()


@pytest.mark.unit
async def test_dispatch_remote_no_billing_attached_succeeds() -> None:
    """Remote dispatch with no callback or store succeeds and does not raise."""
    dispatcher = BackendDispatcher()
    await dispatcher.startup()

    tunnel_base = "https://remote-no-billing.example.com"
    dispatcher.register_remote_backend(
        tunnel_url=tunnel_base,
        name="remote-no-billing",
        models=["llama3.2:8b"],
    )

    remote_entry = next(b for b in dispatcher._backends if b.backend_type == "remote")
    client = dispatcher._get_or_create_pool(remote_entry)
    client.post = AsyncMock(
        return_value=httpx.Response(
            status_code=200,
            json=_OPENAI_COMPAT_RESPONSE,
            request=httpx.Request("POST", f"{tunnel_base}/v1/chat/completions"),
        )
    )

    req = UnifiedRequest(
        model="llama3.2:8b",
        messages=[UnifiedMessage(role="user", content="Hi")],
        stream=False,
    )
    resp = await dispatcher.dispatch(req)
    assert resp.content == "Remote reply"
    assert resp.total_tokens == 8

    await dispatcher.shutdown()


@pytest.mark.unit
async def test_dispatch_core_does_not_emit_billing() -> None:
    """Dispatch to local Core backend does not invoke billing callback or store."""
    from unittest.mock import AsyncMock

    from llmhosts.proxy.dispatcher import BackendEntry

    dispatcher = BackendDispatcher()
    await dispatcher.startup()

    # Set up mock Core engine
    mock_engine = type("MockEngine", (), {"is_loaded": True})()
    dispatcher._core_engine = mock_engine

    core_entry = BackendEntry(
        name="llmhosts-core",
        base_url="local://",
        backend_type="llmhosts-core",
        models=["llama3.2:8b"],
        priority=-1,
    )
    dispatcher._backends.insert(0, core_entry)

    # Mock _dispatch_core to return a proper response
    _core_body = {
        "id": "chatcmpl-test",
        "object": "chat.completion",
        "created": 1000000,
        "model": "llama3.2:8b",
        "choices": [{"index": 0, "message": {"role": "assistant", "content": "Local reply"}, "finish_reason": "stop"}],
        "usage": {"prompt_tokens": 2, "completion_tokens": 4, "total_tokens": 6},
    }
    _core_headers = {"X-LLMHosts-Backend": "llmhosts-core", "X-LLMHosts-Cost": "0.0"}
    dispatcher._dispatch_core = AsyncMock(return_value=(_core_body, _core_headers))

    events: list[dict[str, Any]] = []

    def _capture(event: dict[str, Any]) -> None:
        events.append(event)

    dispatcher.attach_billing_callback(_capture)

    req = UnifiedRequest(
        model="llama3.2:8b",
        messages=[UnifiedMessage(role="user", content="Hi")],
        stream=False,
    )
    resp = await dispatcher.dispatch(req)

    assert resp.content == "Local reply"
    assert len(events) == 0

    await dispatcher.shutdown()


# ---------------------------------------------------------------------------
# BillingStore get_last_n / get_since
# ---------------------------------------------------------------------------


@pytest.mark.unit
async def test_billing_store_get_last_n_and_since() -> None:
    """BillingStore get_last_n and get_since return appended events."""
    with tempfile.TemporaryDirectory() as tmp:
        db_path = Path(tmp) / "billing.db"
        store = BillingStore(db_path)
        await store.initialize()

        from datetime import datetime, timezone

        t1 = datetime.now(timezone.utc)
        await store.append(
            BillingEvent(
                request_id="r1",
                timestamp=t1,
                api_key_id="k1",
                device_id="d1",
                model="m1",
                prompt_tokens=1,
                completion_tokens=2,
                total_tokens=3,
                source="marketplace",
            )
        )
        await store.append(
            BillingEvent(
                request_id="r2",
                timestamp=datetime.now(timezone.utc),
                api_key_id="k2",
                device_id="d2",
                model="m2",
                prompt_tokens=10,
                completion_tokens=20,
                total_tokens=30,
                source="marketplace",
            )
        )

        last = await store.get_last_n(1)
        assert len(last) == 1
        assert last[0].request_id == "r2"

        last2 = await store.get_last_n(10)
        assert len(last2) == 2
        assert last2[0].request_id == "r2"
        assert last2[1].request_id == "r1"

        since = await store.get_since(t1)
        assert len(since) == 2
        assert since[0].request_id == "r1"
        assert since[1].request_id == "r2"

        await store.close()
