"""
tests/test_llm.py — Unit tests for the LLM provider abstraction layer.

All tests use mocks — no real API calls, no API keys required.
Run with: pytest tests/test_llm.py -v
"""

from __future__ import annotations

import json
import time
from dataclasses import dataclass
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio

from cortex.llm.provider import (
    CompletionResponse,
    EmbedResponse,
    ExtractionResponse,
    LLMProvider,
    UsageRecord,
)
from cortex.llm.anthropic_provider import AnthropicProvider, _anthropic_cost, _voyage_cost
from cortex.llm.openai_provider import OpenAIProvider, _openai_completion_cost, _openai_embed_cost
from cortex.llm.ollama_provider import OllamaProvider


# ===========================================================================
# Helpers / fixtures
# ===========================================================================

def _mock_anthropic_response(text: str, tokens_in: int = 10, tokens_out: int = 20) -> MagicMock:
    """Build a mock Anthropic Messages API response."""
    content_block = MagicMock()
    content_block.text = text
    content_block.type = "text"

    usage = MagicMock()
    usage.input_tokens = tokens_in
    usage.output_tokens = tokens_out

    resp = MagicMock()
    resp.content = [content_block]
    resp.usage = usage
    return resp


def _mock_anthropic_tool_response(data: dict, tokens_in: int = 10, tokens_out: int = 20) -> MagicMock:
    """Build a mock Anthropic tool-use response for extract()."""
    tool_block = MagicMock()
    tool_block.type = "tool_use"
    tool_block.input = data

    usage = MagicMock()
    usage.input_tokens = tokens_in
    usage.output_tokens = tokens_out

    resp = MagicMock()
    resp.content = [tool_block]
    resp.usage = usage
    return resp


def _mock_openai_completion(text: str, tokens_in: int = 10, tokens_out: int = 20) -> MagicMock:
    message = MagicMock()
    message.content = text

    choice = MagicMock()
    choice.message = message

    usage = MagicMock()
    usage.prompt_tokens = tokens_in
    usage.completion_tokens = tokens_out

    resp = MagicMock()
    resp.choices = [choice]
    resp.usage = usage
    return resp


def _mock_openai_embed(embeddings: list[list[float]], tokens: int = 5) -> MagicMock:
    items = [MagicMock(embedding=e) for e in embeddings]
    usage = MagicMock()
    usage.prompt_tokens = tokens

    resp = MagicMock()
    resp.data = items
    resp.usage = usage
    return resp


def _mock_voyage_embed(embeddings: list[list[float]], tokens: int = 5) -> MagicMock:
    resp = MagicMock()
    resp.embeddings = embeddings
    resp.total_tokens = tokens
    return resp


# ===========================================================================
# ABC contract tests (using a concrete dummy implementation)
# ===========================================================================

class DummyProvider(LLMProvider):
    """Minimal concrete implementation for ABC contract testing."""

    DEFAULT_COMPLETION_MODEL = "dummy-model"
    DEFAULT_EMBED_MODEL = "dummy-embed"
    PROVIDER_NAME = "dummy"

    async def complete(self, system, user, model=None, response_format="text",
                       max_tokens=None, temperature=0.0) -> CompletionResponse:
        m = self._resolve_model(model, self._completion_model if hasattr(self, "_completion_model") else self.DEFAULT_COMPLETION_MODEL)
        r = UsageRecord(provider="dummy", model=m, call_type="complete",
                        tokens_in=5, tokens_out=10, cost_estimate=0.0, latency_ms=1.0)
        self._log_usage(r)
        return CompletionResponse(text="hello", model=m, tokens_in=5, tokens_out=10,
                                  cost_estimate=0.0, latency_ms=1.0)

    async def extract(self, system, user, schema, model=None, max_tokens=None,
                      temperature=0.0) -> ExtractionResponse:
        m = self._resolve_model(model, self.DEFAULT_COMPLETION_MODEL)
        r = UsageRecord(provider="dummy", model=m, call_type="extract",
                        tokens_in=5, tokens_out=10, cost_estimate=0.0, latency_ms=1.0)
        self._log_usage(r)
        return ExtractionResponse(data={"result": "ok"}, model=m, tokens_in=5,
                                  tokens_out=10, cost_estimate=0.0, latency_ms=1.0)

    async def embed(self, texts, model=None, input_type="query") -> EmbedResponse:
        m = model or self.DEFAULT_EMBED_MODEL
        r = UsageRecord(provider="dummy", model=m, call_type="embed",
                        tokens_in=3, tokens_out=0, cost_estimate=0.0, latency_ms=0.5)
        self._log_usage(r)
        return EmbedResponse(embeddings=[[0.1, 0.2]], model=m, tokens_in=3,
                             tokens_out=0, cost_estimate=0.0, latency_ms=0.5)


class TestLLMProviderABC:
    """Tests that validate the ABC contract."""

    def setup_method(self):
        self.provider = DummyProvider()

    @pytest.mark.asyncio
    async def test_complete_returns_completion_response(self):
        result = await self.provider.complete("sys", "user")
        assert isinstance(result, CompletionResponse)
        assert result.text == "hello"

    @pytest.mark.asyncio
    async def test_extract_returns_extraction_response(self):
        schema = {"type": "object", "properties": {"result": {"type": "string"}}}
        result = await self.provider.extract("sys", "user", schema)
        assert isinstance(result, ExtractionResponse)
        assert isinstance(result.data, dict)

    @pytest.mark.asyncio
    async def test_embed_returns_embed_response(self):
        result = await self.provider.embed(["hello world"])
        assert isinstance(result, EmbedResponse)
        assert isinstance(result.embeddings, list)
        assert all(isinstance(row, list) for row in result.embeddings)

    def test_usage_log_accumulates(self):
        assert len(self.provider._usage_log) == 0

    @pytest.mark.asyncio
    async def test_usage_summary_after_calls(self):
        await self.provider.complete("sys", "user")
        await self.provider.embed(["text"])
        summary = self.provider.usage_summary()
        assert summary["calls"] == 2
        assert summary["tokens_in"] > 0

    def test_cannot_instantiate_abc_directly(self):
        with pytest.raises(TypeError):
            LLMProvider()  # type: ignore

    @pytest.mark.asyncio
    async def test_resolve_model_uses_default(self):
        result = await self.provider.complete("sys", "user", model=None)
        assert result.model == "dummy-model"

    @pytest.mark.asyncio
    async def test_resolve_model_uses_override(self):
        result = await self.provider.complete("sys", "user", model="custom-model")
        assert result.model == "custom-model"


# ===========================================================================
# Pricing utility tests
# ===========================================================================

class TestPricingUtils:
    def test_anthropic_cost_known_model(self):
        cost = _anthropic_cost("claude-haiku-4-5", 1_000_000, 1_000_000)
        assert cost == pytest.approx((0.80 + 4.0), rel=0.01)

    def test_anthropic_cost_unknown_model(self):
        cost = _anthropic_cost("nonexistent-model", 1_000_000, 0)
        assert cost == 0.0

    def test_voyage_cost(self):
        cost = _voyage_cost("voyage-code-3", 1_000_000)
        assert cost == pytest.approx(0.18, rel=0.01)

    def test_openai_completion_cost(self):
        cost = _openai_completion_cost("gpt-4.1-mini", 1_000_000, 1_000_000)
        assert cost == pytest.approx((0.40 + 1.60), rel=0.01)

    def test_openai_embed_cost(self):
        cost = _openai_embed_cost("text-embedding-3-small", 1_000_000)
        assert cost == pytest.approx(0.02, rel=0.01)


# ===========================================================================
# AnthropicProvider tests
# ===========================================================================

class TestAnthropicProvider:

    def _make_provider(self):
        return AnthropicProvider(
            completion_model="claude-haiku-4-5",
            embed_model="voyage-code-3",
        )

    @pytest.mark.asyncio
    async def test_complete_success(self):
        provider = self._make_provider()
        mock_resp = _mock_anthropic_response("The answer is 42.", tokens_in=15, tokens_out=8)

        mock_client = MagicMock()
        mock_client.messages.create = AsyncMock(return_value=mock_resp)
        provider._anthropic_client = mock_client

        result = await provider.complete(
            system="You are a helpful assistant.",
            user="What is 6 times 7?",
        )

        assert isinstance(result, CompletionResponse)
        assert result.text == "The answer is 42."
        assert result.tokens_in == 15
        assert result.tokens_out == 8
        assert result.cost_estimate > 0
        assert result.latency_ms >= 0
        assert result.model == "claude-haiku-4-5"

    @pytest.mark.asyncio
    async def test_complete_logs_usage(self):
        provider = self._make_provider()
        mock_resp = _mock_anthropic_response("ok", tokens_in=5, tokens_out=2)

        mock_client = MagicMock()
        mock_client.messages.create = AsyncMock(return_value=mock_resp)
        provider._anthropic_client = mock_client

        await provider.complete("sys", "user")
        assert len(provider._usage_log) == 1
        record = provider._usage_log[0]
        assert record.provider == "anthropic"
        assert record.call_type == "complete"
        assert record.tokens_in == 5
        assert record.tokens_out == 2

    @pytest.mark.asyncio
    async def test_complete_model_override(self):
        provider = self._make_provider()
        mock_resp = _mock_anthropic_response("hi", tokens_in=3, tokens_out=1)
        mock_client = MagicMock()
        mock_client.messages.create = AsyncMock(return_value=mock_resp)
        provider._anthropic_client = mock_client

        result = await provider.complete("sys", "user", model="claude-opus-4-5")
        assert result.model == "claude-opus-4-5"

    @pytest.mark.asyncio
    async def test_extract_tool_use(self):
        provider = self._make_provider()
        expected_data = {"name": "Eva", "role": "assistant"}
        mock_resp = _mock_anthropic_tool_response(expected_data, tokens_in=20, tokens_out=15)

        mock_client = MagicMock()
        mock_client.messages.create = AsyncMock(return_value=mock_resp)
        provider._anthropic_client = mock_client

        schema = {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "role": {"type": "string"},
            },
        }
        result = await provider.extract("Extract entity", "Eva is an assistant.", schema)

        assert isinstance(result, ExtractionResponse)
        assert result.data == expected_data
        assert result.tokens_in == 20
        assert result.tokens_out == 15
        assert result.cost_estimate > 0

    @pytest.mark.asyncio
    async def test_extract_logs_usage(self):
        provider = self._make_provider()
        mock_resp = _mock_anthropic_tool_response({"key": "val"})
        mock_client = MagicMock()
        mock_client.messages.create = AsyncMock(return_value=mock_resp)
        provider._anthropic_client = mock_client

        await provider.extract("sys", "user", {"type": "object"})
        assert len(provider._usage_log) == 1
        assert provider._usage_log[0].call_type == "extract"

    @pytest.mark.asyncio
    async def test_embed_via_voyage(self):
        provider = self._make_provider()
        fake_embeddings = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
        mock_voyage = _mock_voyage_embed(fake_embeddings, tokens=12)

        mock_voyage_client = MagicMock()
        mock_voyage_client.embed = AsyncMock(return_value=mock_voyage)
        provider._voyage_client = mock_voyage_client

        result = await provider.embed(["hello", "world"], model="voyage-code-3")

        assert isinstance(result, EmbedResponse)
        assert result.embeddings == fake_embeddings
        assert result.model == "voyage-code-3"
        assert result.tokens_in == 12
        assert result.tokens_out == 0
        assert result.cost_estimate > 0

    @pytest.mark.asyncio
    async def test_embed_logs_usage_as_voyage(self):
        provider = self._make_provider()
        mock_voyage_client = MagicMock()
        mock_voyage_client.embed = AsyncMock(return_value=_mock_voyage_embed([[0.1]], tokens=5))
        provider._voyage_client = mock_voyage_client

        await provider.embed(["text"])
        assert provider._usage_log[0].provider == "voyage"
        assert provider._usage_log[0].call_type == "embed"

    def test_missing_api_key_raises(self):
        provider = self._make_provider()
        # Mock the anthropic module so the import succeeds, then check env var.
        mock_anthropic_mod = MagicMock()
        with patch.dict("sys.modules", {"anthropic": mock_anthropic_mod}):
            with patch.dict("os.environ", {}, clear=True):
                with pytest.raises(RuntimeError, match="ANTHROPIC_API_KEY"):
                    provider._get_anthropic()

    def test_anthropic_base_url_is_passed(self):
        provider = self._make_provider()
        mock_anthropic_mod = MagicMock()
        with patch.dict("sys.modules", {"anthropic": mock_anthropic_mod}):
            with patch.dict(
                "os.environ",
                {
                    "ANTHROPIC_API_KEY": "test-key",
                    "ANTHROPIC_BASE_URL": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
                },
                clear=True,
            ):
                provider._get_anthropic()
                mock_anthropic_mod.AsyncAnthropic.assert_called_once_with(
                    api_key="test-key",
                    base_url="https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
                )

    def test_missing_voyage_key_raises(self):
        provider = self._make_provider()
        mock_voyage_mod = MagicMock()
        with patch.dict("sys.modules", {"voyageai": mock_voyage_mod}):
            with patch.dict("os.environ", {}, clear=True):
                with pytest.raises(RuntimeError, match="VOYAGE_API_KEY"):
                    provider._get_voyage()


# ===========================================================================
# OpenAIProvider tests
# ===========================================================================

class TestOpenAIProvider:

    def _make_provider(self):
        return OpenAIProvider(
            completion_model="gpt-4.1-mini",
            embed_model="text-embedding-3-small",
        )

    @pytest.mark.asyncio
    async def test_complete_success(self):
        provider = self._make_provider()
        mock_resp = _mock_openai_completion("Paris.", tokens_in=12, tokens_out=3)

        mock_client = MagicMock()
        mock_client.chat.completions.create = AsyncMock(return_value=mock_resp)
        provider._openai_client = mock_client

        result = await provider.complete(
            system="Answer concisely.",
            user="Capital of France?",
        )

        assert isinstance(result, CompletionResponse)
        assert result.text == "Paris."
        assert result.tokens_in == 12
        assert result.tokens_out == 3
        assert result.cost_estimate > 0
        assert result.model == "gpt-4.1-mini"

    @pytest.mark.asyncio
    async def test_complete_json_format(self):
        provider = self._make_provider()
        json_text = '{"answer": "Paris"}'
        mock_resp = _mock_openai_completion(json_text)

        mock_client = MagicMock()
        mock_client.chat.completions.create = AsyncMock(return_value=mock_resp)
        provider._openai_client = mock_client

        result = await provider.complete("sys", "user", response_format="json")
        # Verify response_format was passed
        call_kwargs = mock_client.chat.completions.create.call_args[1]
        assert call_kwargs["response_format"] == {"type": "json_object"}

    @pytest.mark.asyncio
    async def test_extract_parses_json(self):
        provider = self._make_provider()
        data = {"city": "Bangkok", "country": "Thailand"}
        mock_resp = _mock_openai_completion(json.dumps(data), tokens_in=10, tokens_out=15)

        mock_client = MagicMock()
        mock_client.chat.completions.create = AsyncMock(return_value=mock_resp)
        provider._openai_client = mock_client

        schema = {"type": "object", "properties": {"city": {"type": "string"}}}
        result = await provider.extract("Extract location.", "Bangkok is in Thailand.", schema)

        assert isinstance(result, ExtractionResponse)
        assert result.data == data

    @pytest.mark.asyncio
    async def test_embed_openai(self):
        provider = self._make_provider()
        fake_embeddings = [[0.1, 0.2], [0.3, 0.4]]
        mock_resp = _mock_openai_embed(fake_embeddings, tokens=8)

        mock_client = MagicMock()
        mock_client.embeddings.create = AsyncMock(return_value=mock_resp)
        provider._openai_client = mock_client

        result = await provider.embed(["foo", "bar"])

        assert isinstance(result, EmbedResponse)
        assert result.embeddings == fake_embeddings
        assert result.model == "text-embedding-3-small"
        assert result.tokens_in == 8
        assert result.cost_estimate > 0

    @pytest.mark.asyncio
    async def test_embed_voyage_routing(self):
        """When embed model is voyage-*, should route to Voyage API."""
        provider = OpenAIProvider(embed_model="voyage-code-3")
        fake_embeddings = [[0.5, 0.6, 0.7]]
        mock_voyage = _mock_voyage_embed(fake_embeddings, tokens=5)

        mock_voyage_client = MagicMock()
        mock_voyage_client.embed = AsyncMock(return_value=mock_voyage)
        provider._voyage_client = mock_voyage_client

        result = await provider.embed(["code snippet"])

        assert result.embeddings == fake_embeddings
        assert result.model == "voyage-code-3"
        assert provider._usage_log[0].provider == "voyage"

    @pytest.mark.asyncio
    async def test_complete_logs_usage(self):
        provider = self._make_provider()
        mock_resp = _mock_openai_completion("ok", tokens_in=4, tokens_out=1)
        mock_client = MagicMock()
        mock_client.chat.completions.create = AsyncMock(return_value=mock_resp)
        provider._openai_client = mock_client

        await provider.complete("sys", "user")
        assert len(provider._usage_log) == 1
        assert provider._usage_log[0].provider == "openai"
        assert provider._usage_log[0].tokens_in == 4

    @pytest.mark.asyncio
    async def test_multiple_calls_accumulate_usage(self):
        provider = self._make_provider()
        mock_client = MagicMock()
        mock_client.chat.completions.create = AsyncMock(
            return_value=_mock_openai_completion("ok", tokens_in=5, tokens_out=2)
        )
        provider._openai_client = mock_client

        await provider.complete("sys", "user1")
        await provider.complete("sys", "user2")

        summary = provider.usage_summary()
        assert summary["calls"] == 2
        assert summary["tokens_in"] == 10  # 5 + 5

    def test_missing_api_key_raises(self):
        provider = self._make_provider()
        mock_openai_mod = MagicMock()
        with patch.dict("sys.modules", {"openai": mock_openai_mod}):
            with patch.dict("os.environ", {}, clear=True):
                with pytest.raises(RuntimeError, match="OPENAI_API_KEY"):
                    provider._get_openai()

    def test_openai_base_url_is_passed(self):
        provider = self._make_provider()
        mock_openai_mod = MagicMock()
        with patch.dict("sys.modules", {"openai": mock_openai_mod}):
            with patch.dict(
                "os.environ",
                {
                    "OPENAI_API_KEY": "test-key",
                    "OPENAI_BASE_URL": "https://coding-intl.dashscope.aliyuncs.com/v1",
                },
                clear=True,
            ):
                provider._get_openai()
                mock_openai_mod.AsyncOpenAI.assert_called_once_with(
                    api_key="test-key",
                    base_url="https://coding-intl.dashscope.aliyuncs.com/v1",
                )


# ===========================================================================
# OllamaProvider tests
# ===========================================================================

class TestOllamaProvider:

    def _make_provider(self) -> OllamaProvider:
        return OllamaProvider(
            completion_model="llama3.2",
            embed_model="nomic-embed-text",
            base_url="http://localhost:11434",
        )

    def _make_mock_http(self, response_body: dict | None = None) -> MagicMock:
        """Create a mock aiohttp ClientSession that returns response_body."""
        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value=response_body or {})
        mock_response.raise_for_status = MagicMock()
        mock_response.__aenter__ = AsyncMock(return_value=mock_response)
        mock_response.__aexit__ = AsyncMock(return_value=False)

        mock_session = MagicMock()
        mock_session.post = MagicMock(return_value=mock_response)
        return mock_session, mock_response

    @pytest.mark.asyncio
    async def test_complete_success(self):
        provider = self._make_provider()
        body = {
            "message": {"content": "The answer is 42.", "role": "assistant"},
            "prompt_eval_count": 10,
            "eval_count": 8,
        }
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        result = await provider.complete("sys", "What is 6×7?")

        assert isinstance(result, CompletionResponse)
        assert result.text == "The answer is 42."
        assert result.tokens_in == 10
        assert result.tokens_out == 8
        assert result.cost_estimate == 0.0   # local, no cost
        assert result.model == "llama3.2"

    @pytest.mark.asyncio
    async def test_complete_logs_usage(self):
        provider = self._make_provider()
        body = {"message": {"content": "hi"}, "prompt_eval_count": 5, "eval_count": 2}
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        await provider.complete("sys", "user")
        assert len(provider._usage_log) == 1
        assert provider._usage_log[0].provider == "ollama"
        assert provider._usage_log[0].cost_estimate == 0.0

    @pytest.mark.asyncio
    async def test_complete_json_format_sets_format_field(self):
        provider = self._make_provider()
        body = {"message": {"content": '{"key": "val"}'}, "prompt_eval_count": 5, "eval_count": 5}
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        await provider.complete("sys", "user", response_format="json")
        call_kwargs = mock_session.post.call_args[1]
        assert call_kwargs["json"]["format"] == "json"

    @pytest.mark.asyncio
    async def test_extract_parses_json_body(self):
        provider = self._make_provider()
        data = {"subject": "Andrew", "role": "founder"}
        body = {
            "message": {"content": json.dumps(data)},
            "prompt_eval_count": 12,
            "eval_count": 10,
        }
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        schema = {"type": "object"}
        result = await provider.extract("Extract.", "Andrew is the founder.", schema)

        assert isinstance(result, ExtractionResponse)
        assert result.data == data

    @pytest.mark.asyncio
    async def test_extract_handles_invalid_json_gracefully(self):
        provider = self._make_provider()
        body = {
            "message": {"content": "This is NOT json"},
            "prompt_eval_count": 5,
            "eval_count": 4,
        }
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        result = await provider.extract("sys", "user", {"type": "object"})
        assert "_parse_error" in result.data

    @pytest.mark.asyncio
    async def test_embed_success(self):
        provider = self._make_provider()
        fake_embeddings = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
        body = {"embeddings": fake_embeddings}
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        result = await provider.embed(["hello", "world"])

        assert isinstance(result, EmbedResponse)
        assert result.embeddings == fake_embeddings
        assert result.model == "nomic-embed-text"
        assert result.cost_estimate == 0.0

    @pytest.mark.asyncio
    async def test_embed_logs_usage(self):
        provider = self._make_provider()
        body = {"embeddings": [[0.1]]}
        mock_session, _ = self._make_mock_http(body)
        provider._http_client = mock_session

        await provider.embed(["text"])
        assert len(provider._usage_log) == 1
        assert provider._usage_log[0].call_type == "embed"

    @pytest.mark.asyncio
    async def test_custom_base_url(self):
        provider = OllamaProvider(base_url="http://remote:11434")
        assert provider._base_url == "http://remote:11434"

    def test_env_var_base_url(self):
        with patch.dict("os.environ", {"OLLAMA_BASE_URL": "http://myserver:11434"}):
            provider = OllamaProvider()
            assert provider._base_url == "http://myserver:11434"

    def test_token_estimation_fallback(self):
        """_estimate_tokens should never return 0."""
        p = OllamaProvider()
        assert p._estimate_tokens("") >= 1
        assert p._estimate_tokens("hello world") > 0


# ===========================================================================
# Dataclass integrity tests
# ===========================================================================

class TestDataclasses:
    def test_completion_response_fields(self):
        r = CompletionResponse(
            text="hi", model="gpt-4.1-mini",
            tokens_in=5, tokens_out=3, cost_estimate=0.001, latency_ms=120.0,
        )
        assert r.text == "hi"
        assert r.cost_estimate == 0.001

    def test_embed_response_fields(self):
        r = EmbedResponse(
            embeddings=[[0.1, 0.2]], model="text-embedding-3-small",
            tokens_in=4, tokens_out=0, cost_estimate=0.0001, latency_ms=50.0,
        )
        assert len(r.embeddings) == 1
        assert r.tokens_out == 0

    def test_extraction_response_fields(self):
        r = ExtractionResponse(
            data={"key": "val"}, model="claude-haiku-4-5",
            tokens_in=10, tokens_out=20, cost_estimate=0.005, latency_ms=300.0,
        )
        assert r.data["key"] == "val"

    def test_usage_record_timestamp_auto(self):
        before = time.time()
        r = UsageRecord(
            provider="test", model="m", call_type="complete",
            tokens_in=1, tokens_out=1, cost_estimate=0.0, latency_ms=1.0,
        )
        after = time.time()
        assert before <= r.timestamp <= after
