"""Tests for event construction."""

import time

from llm_observatory.event import build_event, _maybe_redact, _redact_message


class FakeUsage:
    def __init__(self, prompt=100, completion=50, total=150):
        self.prompt_tokens = prompt
        self.completion_tokens = completion
        self.total_tokens = total


class FakeFunction:
    def __init__(self, name="search", arguments='{"query": "test"}'):
        self.name = name
        self.arguments = arguments


class FakeToolCall:
    def __init__(self, id="call_123", name="search", arguments='{"query": "test"}'):
        self.id = id
        self.type = "function"
        self.function = FakeFunction(name, arguments)


class FakeMessage:
    def __init__(self, role="assistant", content="Hello!", reasoning_content=None, tool_calls=None):
        self.role = role
        self.content = content
        self.reasoning_content = reasoning_content
        self.tool_calls = tool_calls


class FakeChoice:
    def __init__(self, content="Hello!", finish_reason="stop", reasoning_content=None, tool_calls=None):
        self.index = 0
        self.message = FakeMessage(content=content, reasoning_content=reasoning_content, tool_calls=tool_calls)
        self.finish_reason = finish_reason


class FakeResponse:
    def __init__(self, model="gpt-4o-2025-01-01", reasoning_content=None, tool_calls=None):
        self.id = "chatcmpl-abc123"
        self.model = model
        self.choices = [FakeChoice(reasoning_content=reasoning_content, tool_calls=tool_calls)]
        self.usage = FakeUsage()


def test_build_event_basic():
    start = time.time()
    end = start + 1.5

    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]},
        response=FakeResponse(),
        start_time=start,
        end_time=end,
    )

    assert event["event_id"]
    assert event["timestamp"]
    assert event["request"]["model"] == "gpt-4o"
    assert event["request"]["messages"] == [{"role": "user", "content": "Hi"}]
    assert event["response"]["id"] == "chatcmpl-abc123"
    assert event["response"]["model"] == "gpt-4o-2025-01-01"
    assert event["response"]["finish_reason"] == "stop"
    assert event["usage"]["prompt_tokens"] == 100
    assert event["usage"]["completion_tokens"] == 50
    assert event["usage"]["total_tokens"] == 150
    assert event["metrics"]["latency_ms"] == 1500
    assert "error" not in event


def test_build_event_with_error():
    start = time.time()
    end = start + 0.1

    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": []},
        response=None,
        start_time=start,
        end_time=end,
        error={"type": "RateLimitError", "message": "Rate limit exceeded"},
    )

    assert event["error"]["type"] == "RateLimitError"
    assert event["error"]["message"] == "Rate limit exceeded"
    assert event["usage"]["prompt_tokens"] == 0


def test_build_event_with_streaming_metrics():
    start = time.time()
    end = start + 2.0

    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": [], "stream": True},
        response=FakeResponse(),
        start_time=start,
        end_time=end,
        time_to_first_token_ms=180,
        tokens_per_second=38.123,
    )

    assert event["request"]["stream"] is True
    assert event["metrics"]["time_to_first_token_ms"] == 180
    assert event["metrics"]["tokens_per_second"] == 38.1


def test_build_event_with_metadata():
    start = time.time()
    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": []},
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
        metadata={"user_id": "u123", "session_id": "s456", "tags": ["prod"]},
    )

    assert event["metadata"]["user_id"] == "u123"
    assert event["metadata"]["session_id"] == "s456"
    assert event["metadata"]["tags"] == ["prod"]


def test_build_event_redact_messages():
    start = time.time()
    event = build_event(
        request_kwargs={
            "model": "gpt-4o",
            "messages": [
                {"role": "system", "content": "You are helpful."},
                {"role": "user", "content": "Secret data"},
            ],
        },
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
        redact_messages=True,
    )

    for msg in event["request"]["messages"]:
        assert msg["content"] == "[REDACTED]"


def test_build_event_custom_metadata_coerces_to_strings():
    """Non-string values in custom metadata must be coerced to strings.

    The Go ingestion API expects map[string]string — sending an int (e.g. "turn": 2)
    causes a 400 and drops the entire batch.
    """
    start = time.time()
    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": []},
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
        metadata={
            "user_id": "u1",
            "custom": {"environment": "prod", "turn": 2, "score": 0.95, "debug": True},
        },
    )

    custom = event["metadata"]["custom"]
    assert custom["environment"] == "prod"
    assert custom["turn"] == "2"
    assert custom["score"] == "0.95"
    assert custom["debug"] == "True"
    # All values must be strings
    for v in custom.values():
        assert isinstance(v, str)


def test_build_event_with_reasoning_content():
    """Reasoning content (e.g. from o1/o3 or Parasail models) should be captured."""
    start = time.time()
    event = build_event(
        request_kwargs={"model": "o3-mini", "messages": [{"role": "user", "content": "Think step by step."}]},
        response=FakeResponse(model="o3-mini", reasoning_content="Let me think about this..."),
        start_time=start,
        end_time=start + 2,
    )

    choice = event["response"]["choices"][0]
    assert choice["message"]["reasoning_content"] == "Let me think about this..."
    assert choice["message"]["content"] == "Hello!"


def test_build_event_no_reasoning_content_omitted():
    """When reasoning_content is None, it should not appear in the event."""
    start = time.time()
    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": []},
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
    )

    choice = event["response"]["choices"][0]
    assert "reasoning_content" not in choice["message"]


def test_build_event_top_level_reasoning_tokens():
    """Some providers send reasoning_tokens at the top level of usage."""
    start = time.time()

    class TopLevelUsage:
        prompt_tokens = 100
        completion_tokens = 50
        total_tokens = 150
        reasoning_tokens = 40
        cached_tokens = 20

    resp = FakeResponse()
    resp.usage = TopLevelUsage()

    event = build_event(
        request_kwargs={"model": "parasail-model", "messages": []},
        response=resp,
        start_time=start,
        end_time=start + 1,
    )

    assert event["usage"]["reasoning_tokens"] == 40
    assert event["usage"]["cached_tokens"] == 20


def test_build_event_optional_fields():
    start = time.time()
    event = build_event(
        request_kwargs={
            "model": "gpt-4o",
            "messages": [],
            "temperature": 0.7,
            "max_tokens": 1000,
            "tools": [{"type": "function", "function": {"name": "search"}}],
        },
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
    )

    assert event["request"]["temperature"] == 0.7
    assert event["request"]["max_tokens"] == 1000
    assert len(event["request"]["tools"]) == 1


# ── Deep redaction tests ─────────────────────────────────────────


def test_redact_multipart_message():
    """Multi-part content (list of blocks) should be deeply redacted."""
    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "What is in this image?"},
                {"type": "image_url", "image_url": {"url": "https://example.com/secret.png"}},
            ],
        }
    ]

    redacted = _maybe_redact(messages, True)
    content = redacted[0]["content"]
    assert content[0]["text"] == "[REDACTED]"
    assert content[1]["image_url"]["url"] == "[REDACTED]"


def test_redact_tool_calls_in_request():
    """tool_calls[].function.arguments should be redacted; name preserved."""
    messages = [
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": "call_abc",
                    "type": "function",
                    "function": {
                        "name": "search",
                        "arguments": '{"query": "secret search term"}',
                    },
                }
            ],
        }
    ]

    redacted = _maybe_redact(messages, True)
    tc = redacted[0]["tool_calls"][0]
    assert tc["function"]["name"] == "search"
    assert tc["function"]["arguments"] == "[REDACTED]"


def test_redact_function_call_in_request():
    """Legacy function_call.arguments should be redacted."""
    messages = [
        {
            "role": "assistant",
            "function_call": {
                "name": "get_weather",
                "arguments": '{"location": "NYC"}',
            },
        }
    ]

    redacted = _maybe_redact(messages, True)
    fc = redacted[0]["function_call"]
    assert fc["name"] == "get_weather"
    assert fc["arguments"] == "[REDACTED]"


def test_redact_preserves_role():
    """Redaction should not remove or alter the role field."""
    messages = [
        {"role": "system", "content": "You are a secret agent."},
        {"role": "user", "content": "What is the password?"},
    ]

    redacted = _maybe_redact(messages, True)
    assert redacted[0]["role"] == "system"
    assert redacted[1]["role"] == "user"


def test_no_redact_when_false():
    """When redact=False, messages should pass through unchanged."""
    messages = [{"role": "user", "content": "visible"}]
    result = _maybe_redact(messages, False)
    assert result is messages  # Same reference, not copied


def test_no_redact_when_none():
    """When messages is None, return None."""
    assert _maybe_redact(None, True) is None


def test_redact_response_content():
    """Response choice content should be redacted when redact_messages=True."""
    start = time.time()
    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]},
        response=FakeResponse(),
        start_time=start,
        end_time=start + 1,
        redact_messages=True,
    )

    choice = event["response"]["choices"][0]
    assert choice["message"]["content"] == "[REDACTED]"


def test_redact_response_reasoning_content():
    """Response reasoning content should be redacted when redact_messages=True."""
    start = time.time()
    event = build_event(
        request_kwargs={"model": "o3-mini", "messages": []},
        response=FakeResponse(model="o3-mini", reasoning_content="Secret thoughts"),
        start_time=start,
        end_time=start + 1,
        redact_messages=True,
    )

    choice = event["response"]["choices"][0]
    assert choice["message"]["reasoning_content"] == "[REDACTED]"


def test_redact_response_tool_calls():
    """Response tool_calls arguments should be redacted; name preserved."""
    start = time.time()
    resp = FakeResponse()
    resp.choices = [FakeChoice(tool_calls=[FakeToolCall(name="search", arguments='{"q": "secret"}')])]

    event = build_event(
        request_kwargs={"model": "gpt-4o", "messages": []},
        response=resp,
        start_time=start,
        end_time=start + 1,
        redact_messages=True,
    )

    tc = event["response"]["choices"][0]["message"]["tool_calls"][0]
    assert tc["function"]["name"] == "search"
    assert tc["function"]["arguments"] == "[REDACTED]"


def test_redact_message_dict():
    """_redact_message should handle all field types."""
    msg = {
        "role": "assistant",
        "content": "secret",
        "tool_calls": [
            {"id": "c1", "type": "function", "function": {"name": "fn", "arguments": "secret_args"}},
        ],
        "function_call": {"name": "fn2", "arguments": "secret_args2"},
    }

    redacted = _redact_message(msg)
    assert redacted["content"] == "[REDACTED]"
    assert redacted["tool_calls"][0]["function"]["arguments"] == "[REDACTED]"
    assert redacted["tool_calls"][0]["function"]["name"] == "fn"  # preserved
    assert redacted["function_call"]["arguments"] == "[REDACTED]"
    assert redacted["function_call"]["name"] == "fn2"  # preserved
