import asyncio
import os
import sys
from datetime import datetime, timedelta, timezone
from typing import Optional
from unittest.mock import MagicMock, Mock, patch

import pytest

# Adds the grandparent directory to sys.path to allow importing project modules
sys.path.insert(0, os.path.abspath("../.."))
import litellm
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.datadog.datadog_llm_obs import DataDogLLMObsLogger
from litellm.types.integrations.datadog_llm_obs import (
    DatadogLLMObsInitParams,
)
from litellm.types.utils import (
    StandardLoggingGuardrailInformation,
    StandardLoggingHiddenParams,
    StandardLoggingMetadata,
    StandardLoggingModelInformation,
    StandardLoggingPayload,
    StandardLoggingPayloadErrorInformation,
)


def create_standard_logging_payload_with_cache() -> StandardLoggingPayload:
    """Create a real StandardLoggingPayload object for testing"""
    return StandardLoggingPayload(
        id="test-request-id-456",
        call_type="completion",
        response_cost=0.05,
        response_cost_failure_debug_info=None,
        status="success",
        total_tokens=30,
        prompt_tokens=10,
        completion_tokens=20,
        startTime=1234567890.0,
        endTime=1234567891.0,
        completionStartTime=1234567890.5,
        model_map_information=StandardLoggingModelInformation(
            model_map_key="gpt-4", model_map_value=None
        ),
        model="gpt-4",
        model_id="model-123",
        model_group="openai-gpt",
        api_base="https://api.openai.com",
        metadata=StandardLoggingMetadata(
            user_api_key_hash="test_hash",
            user_api_key_org_id=None,
            user_api_key_alias="test_alias",
            user_api_key_team_id="test_team",
            user_api_key_user_id="test_user",
            user_api_key_team_alias="test_team_alias",
            spend_logs_metadata=None,
            requester_ip_address="127.0.0.1",
            requester_metadata=None,
        ),
        cache_hit=True,
        cache_key="test-cache-key-789",
        saved_cache_cost=0.02,
        request_tags=[],
        end_user=None,
        requester_ip_address="127.0.0.1",
        messages=[{"role": "user", "content": "Hello, world!"}],
        response={"choices": [{"message": {"content": "Hi there!"}}]},
        error_str=None,
        model_parameters={"stream": True},
        hidden_params=StandardLoggingHiddenParams(
            model_id="model-123",
            cache_key="test-cache-key-789",
            api_base="https://api.openai.com",
            response_cost="0.05",
            additional_headers=None,
        ),
        trace_id="test-trace-id-123",
        custom_llm_provider="openai",
    )


def create_standard_logging_payload_with_failure() -> StandardLoggingPayload:
    """Create a StandardLoggingPayload object for failure testing"""
    return StandardLoggingPayload(
        id="test-request-id-failure-789",
        call_type="completion",
        response_cost=0.0,
        response_cost_failure_debug_info=None,
        status="failure",
        total_tokens=0,
        prompt_tokens=10,
        completion_tokens=0,
        startTime=1234567890.0,
        endTime=1234567891.0,
        completionStartTime=1234567890.5,
        model_map_information=StandardLoggingModelInformation(
            model_map_key="gpt-4", model_map_value=None
        ),
        model="gpt-4",
        model_id="model-123",
        model_group="openai-gpt",
        api_base="https://api.openai.com",
        metadata=StandardLoggingMetadata(
            user_api_key_hash="test_hash",
            user_api_key_org_id=None,
            user_api_key_alias="test_alias",
            user_api_key_team_id="test_team",
            user_api_key_user_id="test_user",
            user_api_key_team_alias="test_team_alias",
            spend_logs_metadata=None,
            requester_ip_address="127.0.0.1",
            requester_metadata=None,
        ),
        cache_hit=False,
        cache_key=None,
        saved_cache_cost=0.0,
        request_tags=[],
        end_user=None,
        requester_ip_address="127.0.0.1",
        messages=[{"role": "user", "content": "Hello, world!"}],
        response=None,
        error_str="RateLimitError: You exceeded your current quota",
        error_information=StandardLoggingPayloadErrorInformation(
            error_code="rate_limit_exceeded",
            error_class="RateLimitError",
            llm_provider="openai",
            traceback="Traceback (most recent call last):\n  File test.py, line 1\n    RateLimitError: You exceeded your current quota",
            error_message="RateLimitError: You exceeded your current quota",
        ),
        model_parameters={"stream": False},
        hidden_params=StandardLoggingHiddenParams(
            model_id="model-123",
            cache_key=None,
            api_base="https://api.openai.com",
            response_cost="0.0",
            additional_headers=None,
        ),
        trace_id="test-trace-id-failure-456",
        custom_llm_provider="openai",
    )


class TestDataDogLLMObsLogger:
    """Test suite for DataDog LLM Observability Logger"""

    @pytest.fixture
    def mock_env_vars(self):
        """Mock environment variables for DataDog"""
        with patch.dict(
            os.environ, {"DD_API_KEY": "test_api_key", "DD_SITE": "us5.datadoghq.com"}
        ):
            yield

    @pytest.fixture
    def mock_response_obj(self):
        """Create a mock response object"""
        mock_response = Mock()
        mock_response.__getitem__ = Mock(
            return_value={
                "choices": [
                    {
                        "message": Mock(
                            json=Mock(
                                return_value={"role": "assistant", "content": "Hello!"}
                            )
                        )
                    }
                ]
            }
        )
        return mock_response

    def test_cost_and_trace_id_integration(self, mock_env_vars, mock_response_obj):
        """Test that total_cost is passed and trace_id from standard payload is used"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            standard_payload = create_standard_logging_payload_with_cache()

            kwargs = {
                "standard_logging_object": standard_payload,
                "litellm_params": {
                    "metadata": {"trace_id": "old-trace-id-should-be-ignored"}
                },
            }

            start_time = datetime.now()
            end_time = datetime.now()

            payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)

            # Test 1: Verify total_cost is correctly extracted from response_cost
            assert payload["metrics"].get("total_cost") == 0.05

            # Test 2: Verify trace_id comes from standard_logging_payload, not metadata
            assert payload["trace_id"] == "test-trace-id-123"

            # Test 3: Verify saved_cache_cost is in metadata
            metadata = payload["meta"]["metadata"]
            assert metadata["saved_cache_cost"] == 0.02
            assert metadata["cache_hit"] is True
            assert metadata["cache_key"] == "test-cache-key-789"

            # Test 4: Verify is_streamed_request is in metadata
            assert metadata["is_streamed_request"] is True

    def test_cache_metadata_fields(self, mock_env_vars, mock_response_obj):
        """Test that cache-related metadata fields are correctly tracked"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            standard_payload = create_standard_logging_payload_with_cache()

            # Test the _get_dd_llm_obs_payload_metadata method directly
            metadata = logger._get_dd_llm_obs_payload_metadata(standard_payload)

            # Verify all cache-related fields are present
            assert metadata["cache_hit"] is True
            assert metadata["cache_key"] == "test-cache-key-789"
            assert metadata["saved_cache_cost"] == 0.02
            assert metadata["id"] == "test-request-id-456"
            assert metadata["trace_id"] == "test-trace-id-123"
            assert metadata["model_name"] == "gpt-4"
            assert metadata["model_provider"] == "openai"

    def test_get_time_to_first_token_seconds(self, mock_env_vars):
        """Test the _get_time_to_first_token_seconds method for streaming calls"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            # Test streaming case (completion_start_time available)
            streaming_payload = create_standard_logging_payload_with_cache()
            # Modify times for testing: start=1000, completion_start=1002, end=1005
            streaming_payload["startTime"] = 1000.0
            streaming_payload["completionStartTime"] = 1002.0
            streaming_payload["endTime"] = 1005.0

            # Test streaming case: should use completion_start_time - start_time
            time_to_first_token = logger._get_time_to_first_token_seconds(
                streaming_payload
            )
            assert time_to_first_token == 2.0  # 1002.0 - 1000.0 = 2.0 seconds

    def test_datadog_span_kind_mapping(self, mock_env_vars):
        """Test that call_type values are correctly mapped to DataDog span kinds"""
        from litellm.types.utils import CallTypes

        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

        # Test embedding operations
        assert (
            logger._get_datadog_span_kind(CallTypes.embedding.value, "123")
            == "embedding"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.aembedding.value, "123")
            == "embedding"
        )

        # Test LLM completion operations
        assert logger._get_datadog_span_kind(CallTypes.completion.value, None) == "llm"
        assert logger._get_datadog_span_kind(CallTypes.acompletion.value, None) == "llm"
        assert (
            logger._get_datadog_span_kind(CallTypes.text_completion.value, None)
            == "llm"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.generate_content.value, None)
            == "llm"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.anthropic_messages.value, None)
            == "llm"
        )
        assert logger._get_datadog_span_kind(CallTypes.responses.value, None) == "llm"
        assert logger._get_datadog_span_kind(CallTypes.aresponses.value, None) == "llm"

        # Test tool operations
        assert (
            logger._get_datadog_span_kind(CallTypes.call_mcp_tool.value, "123")
            == "tool"
        )

        # Test retrieval operations
        assert (
            logger._get_datadog_span_kind(CallTypes.get_assistants.value, "123")
            == "retrieval"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.file_retrieve.value, "123")
            == "retrieval"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.retrieve_batch.value, "123")
            == "retrieval"
        )

        # Test task operations
        assert (
            logger._get_datadog_span_kind(CallTypes.create_batch.value, "123") == "task"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.image_generation.value, "123")
            == "task"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.moderation.value, "123") == "task"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.transcription.value, "123")
            == "task"
        )

        # Test default fallback
        assert logger._get_datadog_span_kind("unknown_call_type", None) == "llm"
        assert logger._get_datadog_span_kind(None, None) == "llm"

    def test_datadog_span_kind_defaults_without_parent(self, mock_env_vars):
        """Test that non-llm kinds fallback to llm when no parent span is provided"""
        from litellm.types.utils import CallTypes

        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

        # Tool/task/retrieval span kinds should fallback to llm when parent_id missing
        assert (
            logger._get_datadog_span_kind(CallTypes.call_mcp_tool.value, None) == "llm"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.create_batch.value, None) == "llm"
        )
        assert (
            logger._get_datadog_span_kind(CallTypes.get_assistants.value, None) == "llm"
        )

    @pytest.mark.asyncio
    async def test_async_log_failure_event(self, mock_env_vars):
        """Test that async_log_failure_event correctly processes failure payloads according to DD LLM Obs API spec"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            # Ensure log_queue starts empty
            logger.log_queue = []

            standard_failure_payload = create_standard_logging_payload_with_failure()

            kwargs = {
                "standard_logging_object": standard_failure_payload,
                "model": "gpt-4",
                "litellm_params": {"metadata": {}},
            }

            start_time = datetime.now()
            end_time = datetime.now() + timedelta(seconds=2)

            # Mock async_send_batch to prevent actual network calls
            with patch.object(logger, "async_send_batch") as mock_send_batch:
                # Call the method under test
                await logger.async_log_failure_event(kwargs, None, start_time, end_time)

                # Verify payload was added to queue
                assert len(logger.log_queue) == 1

                # Verify the payload has correct failure characteristics according to DD LLM Obs API spec
                payload = logger.log_queue[0]
                assert payload["trace_id"] == "test-trace-id-failure-456"
                assert (
                    payload["meta"]["metadata"]["id"] == "test-request-id-failure-789"
                )
                assert payload["status"] == "error"

                # Verify error information follows DD LLM Obs API spec
                assert (
                    payload["meta"]["error"]["message"]
                    == "RateLimitError: You exceeded your current quota"
                )
                assert payload["meta"]["error"]["type"] == "RateLimitError"
                assert (
                    payload["meta"]["error"]["stack"]
                    == "Traceback (most recent call last):\n  File test.py, line 1\n    RateLimitError: You exceeded your current quota"
                )

                assert payload["metrics"]["total_cost"] == 0.0
                assert payload["metrics"]["total_tokens"] == 0
                assert payload["metrics"]["output_tokens"] == 0

                # Verify batch sending not triggered (queue size < batch_size)
                mock_send_batch.assert_not_called()


class TestDataDogLLMObsLoggerForRedaction(DataDogLLMObsLogger):
    """Test suite for DataDog LLM Observability Logger"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.logged_standard_logging_payload: Optional[StandardLoggingPayload] = None

    async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
        self.logged_standard_logging_payload = kwargs.get("standard_logging_object")


class TestS3Logger(CustomLogger):
    """Test suite for S3 Logger"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.logged_standard_logging_payload: Optional[StandardLoggingPayload] = None

    async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
        self.logged_standard_logging_payload = kwargs.get("standard_logging_object")


@pytest.mark.asyncio
async def test_dd_llms_obs_redaction(mock_env_vars):
    # init DD with turn_off_message_logging=True
    litellm._turn_on_debug()
    from litellm.types.utils import LiteLLMCommonStrings

    litellm.datadog_llm_observability_params = DatadogLLMObsInitParams(
        turn_off_message_logging=True
    )
    dd_llms_obs_logger = TestDataDogLLMObsLoggerForRedaction()
    test_s3_logger = TestS3Logger()
    litellm.callbacks = [dd_llms_obs_logger, test_s3_logger]

    # call litellm
    await litellm.acompletion(
        model="gpt-4o",
        mock_response="Hi there!",
        messages=[{"role": "user", "content": "Hello, world!"}],
    )

    # sleep 1 second for logging to complete
    await asyncio.sleep(1)

    #################
    # test validation
    # 1. both loggers logged a standard_logging_payload
    # 2. DD LLM Obs standard_logging_payload has messages and response redacted
    # 3. S3 standard_logging_payload does not have messages and response redacted

    assert dd_llms_obs_logger.logged_standard_logging_payload is not None
    assert test_s3_logger.logged_standard_logging_payload is not None

    assert (
        dd_llms_obs_logger.logged_standard_logging_payload["messages"][0]["content"]
        == "redacted-by-litellm"
    )
    assert (
        dd_llms_obs_logger.logged_standard_logging_payload["response"]["choices"][0][
            "message"
        ]["content"]
        == "redacted-by-litellm"
    )

    assert test_s3_logger.logged_standard_logging_payload["messages"] == [
        {"role": "user", "content": "Hello, world!"}
    ]
    assert (
        test_s3_logger.logged_standard_logging_payload["response"]["choices"][0][
            "message"
        ]["content"]
        == "Hi there!"
    )


@pytest.fixture
def mock_env_vars():
    """Mock environment variables for DataDog"""
    with patch.dict(
        os.environ, {"DD_API_KEY": "test_api_key", "DD_SITE": "us5.datadoghq.com"}
    ):
        yield


@pytest.mark.asyncio
async def test_create_llm_obs_payload(mock_env_vars):
    datadog_llm_obs_logger = DataDogLLMObsLogger()
    standard_logging_payload = create_standard_logging_payload_with_cache()
    payload = datadog_llm_obs_logger.create_llm_obs_payload(
        kwargs={
            "model": "gpt-4",
            "messages": [{"role": "user", "content": "Hello"}],
            "standard_logging_object": standard_logging_payload,
        },
        start_time=datetime.now(),
        end_time=datetime.now() + timedelta(seconds=1),
    )

    assert payload["name"] == "litellm_llm_call"
    assert payload["meta"]["kind"] == "llm"
    assert payload["meta"]["input"]["messages"] == [
        {"role": "user", "content": "Hello, world!"}
    ]
    assert payload["meta"]["output"]["messages"][0]["content"] == "Hi there!"
    assert payload["metrics"]["input_tokens"] == 10
    assert payload["metrics"]["output_tokens"] == 20
    assert payload["metrics"]["total_tokens"] == 30


def create_standard_logging_payload_with_latency_metrics() -> StandardLoggingPayload:
    """Create a StandardLoggingPayload object with latency metrics for testing"""
    guardrail_info = StandardLoggingGuardrailInformation(
        guardrail_name="test_guardrail",
        guardrail_status="success",
        start_time=1234567890.0,
        end_time=1234567890.5,
        duration=0.5,  # 500ms
        guardrail_request={"input": "test input message", "user_id": "test_user"},
        guardrail_response={
            "output": "filtered output",
            "flagged": False,
            "score": 0.1,
        },
    )

    hidden_params = StandardLoggingHiddenParams(
        model_id="model-123",
        cache_key="test-cache-key",
        api_base="https://api.openai.com",
        response_cost="0.05",
        litellm_overhead_time_ms=150.0,  # 150ms
        additional_headers=None,
    )

    return StandardLoggingPayload(
        id="test-request-id-latency",
        call_type="completion",
        response_cost=0.05,
        response_cost_failure_debug_info=None,
        status="success",
        total_tokens=30,
        prompt_tokens=10,
        completion_tokens=20,
        startTime=1234567890.0,
        endTime=1234567892.0,
        completionStartTime=1234567890.8,  # 800ms after start
        response_time=2.0,
        model_map_information=StandardLoggingModelInformation(
            model_map_key="gpt-4", model_map_value=None
        ),
        model="gpt-4",
        model_id="model-123",
        model_group="openai-gpt",
        api_base="https://api.openai.com",
        metadata=StandardLoggingMetadata(
            user_api_key_hash="test_hash",
            user_api_key_org_id=None,
            user_api_key_alias="test_alias",
            user_api_key_team_id="test_team",
            user_api_key_user_id="test_user",
            user_api_key_team_alias="test_team_alias",
            spend_logs_metadata=None,
            requester_ip_address="127.0.0.1",
            requester_metadata=None,
        ),
        cache_hit=False,
        cache_key=None,
        saved_cache_cost=0.0,
        request_tags=[],
        end_user=None,
        requester_ip_address="127.0.0.1",
        messages=[{"role": "user", "content": "Hello, world!"}],
        response={"choices": [{"message": {"content": "Hi there!"}}]},
        error_str=None,
        error_information=None,
        model_parameters={"stream": True},
        hidden_params=hidden_params,
        guardrail_information=[guardrail_info],
        trace_id="test-trace-id-latency",
        custom_llm_provider="openai",
    )


def test_latency_metrics_in_metadata(mock_env_vars):
    """Test that time to first token, litellm overhead, and guardrail overhead are included in metadata"""
    with (
        patch("litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"),
        patch("asyncio.create_task"),
    ):
        logger = DataDogLLMObsLogger()

        standard_payload = create_standard_logging_payload_with_latency_metrics()

        kwargs = {
            "standard_logging_object": standard_payload,
            "litellm_params": {"metadata": {}},
        }

        start_time = datetime.now()
        end_time = datetime.now()

        # Test the metadata generation directly
        metadata = logger._get_dd_llm_obs_payload_metadata(standard_payload)
        latency_metadata = metadata.get("latency_metrics", {})

        # Verify time to first token is included (800ms)
        assert "time_to_first_token_ms" in latency_metadata
        assert (
            abs(latency_metadata["time_to_first_token_ms"] - 800.0) < 0.001
        )  # 0.8 seconds * 1000 with tolerance for floating-point precision

        # Verify litellm overhead is included (150ms)
        assert "litellm_overhead_time_ms" in latency_metadata
        assert latency_metadata["litellm_overhead_time_ms"] == 150.0

        # Verify guardrail overhead is included (500ms)
        assert "guardrail_overhead_time_ms" in latency_metadata
        assert (
            latency_metadata["guardrail_overhead_time_ms"] == 500.0
        )  # 0.5 seconds * 1000

        # Verify these metrics are also included in the full payload
        payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)
        payload_metadata_latency = payload["meta"]["metadata"]["latency_metrics"]

        assert abs(payload_metadata_latency["time_to_first_token_ms"] - 800.0) < 0.001
        assert payload_metadata_latency["litellm_overhead_time_ms"] == 150.0
        assert payload_metadata_latency["guardrail_overhead_time_ms"] == 500.0


def test_latency_metrics_edge_cases(mock_env_vars):
    """Test latency metrics with edge cases (missing fields, zero values, etc.)"""
    with (
        patch("litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"),
        patch("asyncio.create_task"),
    ):
        logger = DataDogLLMObsLogger()

        # Test case 1: No latency metrics present
        standard_payload = create_standard_logging_payload_with_cache()
        metadata = logger._get_dd_llm_obs_payload_metadata(standard_payload)

        # Should not have latency fields if data is missing/zero
        assert "time_to_first_token_ms" not in metadata  # Will be 0, so not included
        assert (
            "litellm_overhead_time_ms" not in metadata
        )  # Not present in hidden_params
        assert "guardrail_overhead_time_ms" not in metadata  # No guardrail_information

        # Test case 2: Zero time to first token should not be included
        standard_payload = create_standard_logging_payload_with_cache()
        standard_payload["startTime"] = 1000.0
        standard_payload["completionStartTime"] = 1000.0  # Same time = 0 difference
        metadata = logger._get_dd_llm_obs_payload_metadata(standard_payload)
        assert "time_to_first_token_ms" not in metadata

        # Test case 3: Missing guardrail duration should not crash
        standard_payload = create_standard_logging_payload_with_cache()
        standard_payload["guardrail_information"] = [
            StandardLoggingGuardrailInformation(
                guardrail_name="test",
                guardrail_status="success",
                # duration is missing
            )
        ]
        metadata = logger._get_dd_llm_obs_payload_metadata(standard_payload)
        assert "guardrail_overhead_time_ms" not in metadata


def test_guardrail_information_in_metadata(mock_env_vars):
    """Test that guardrail_information is included in metadata with input/output fields"""
    with (
        patch("litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"),
        patch("asyncio.create_task"),
    ):
        logger = DataDogLLMObsLogger()

        # Create a standard payload with guardrail information
        standard_payload = create_standard_logging_payload_with_latency_metrics()

        kwargs = {
            "standard_logging_object": standard_payload,
            "litellm_params": {"metadata": {}},
        }

        start_time = datetime.now()
        end_time = datetime.now()

        # Create the payload and verify guardrail_information is in metadata
        payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)
        metadata = payload["meta"]["metadata"]

        # Verify guardrail_information is present in metadata
        assert "guardrail_information" in metadata
        assert metadata["guardrail_information"] is not None

        # Verify the guardrail information structure
        guardrail_info = metadata["guardrail_information"]
        assert guardrail_info[0]["guardrail_name"] == "test_guardrail"
        assert guardrail_info[0]["guardrail_status"] == "success"
        assert guardrail_info[0]["duration"] == 0.5

        # Verify input/output fields are present
        assert "guardrail_request" in guardrail_info[0]
        assert "guardrail_response" in guardrail_info[0]

        # Validate the input/output content
        assert guardrail_info[0]["guardrail_request"]["input"] == "test input message"
        assert guardrail_info[0]["guardrail_request"]["user_id"] == "test_user"
        assert guardrail_info[0]["guardrail_response"]["output"] == "filtered output"
        assert guardrail_info[0]["guardrail_response"]["flagged"] is False
        assert guardrail_info[0]["guardrail_response"]["score"] == 0.1


def create_standard_logging_payload_with_tool_calls() -> StandardLoggingPayload:
    """Create a StandardLoggingPayload object with tool calls for testing"""
    return {
        "id": "test-request-id-tool-calls",
        "trace_id": "test-trace-id-tool-calls",
        "call_type": "completion",
        "stream": None,
        "response_cost": 0.05,
        "response_cost_failure_debug_info": None,
        "status": "success",
        "custom_llm_provider": "openai",
        "total_tokens": 50,
        "prompt_tokens": 20,
        "completion_tokens": 30,
        "startTime": 1234567890.0,
        "endTime": 1234567891.0,
        "completionStartTime": 1234567890.5,
        "response_time": 1.0,
        "model_map_information": {"model_map_key": "gpt-4", "model_map_value": None},
        "model": "gpt-4",
        "model_id": "model-123",
        "model_group": "openai-gpt",
        "api_base": "https://api.openai.com",
        "metadata": {
            "user_api_key_hash": "test_hash",
            "user_api_key_org_id": None,
            "user_api_key_alias": "test_alias",
            "user_api_key_team_id": "test_team",
            "user_api_key_user_id": "test_user",
            "user_api_key_team_alias": "test_team_alias",
            "user_api_key_user_email": None,
            "user_api_key_end_user_id": None,
            "user_api_key_request_route": None,
            "spend_logs_metadata": None,
            "requester_ip_address": "127.0.0.1",
            "requester_metadata": None,
            "requester_custom_headers": None,
            "prompt_management_metadata": None,
            "mcp_tool_call_metadata": None,
            "vector_store_request_metadata": None,
            "applied_guardrails": None,
            "usage_object": None,
            "cold_storage_object_key": None,
        },
        "cache_hit": False,
        "cache_key": None,
        "saved_cache_cost": 0.0,
        "request_tags": [],
        "end_user": None,
        "requester_ip_address": "127.0.0.1",
        "messages": [
            {"role": "user", "content": "What's the weather?"},
            {
                "role": "assistant",
                "content": "I'll check the weather for you.",
                "tool_calls": [
                    {
                        "id": "call_123",
                        "type": "function",
                        "function": {
                            "name": "get_weather",
                            "arguments": '{"location": "NYC"}',
                        },
                    }
                ],
            },
            {
                "role": "tool",
                "tool_call_id": "call_123",
                "content": '{"temperature": 72, "condition": "sunny"}',
            },
        ],
        "response": {
            "choices": [
                {
                    "message": {
                        "role": "assistant",
                        "content": "It's 72°F and sunny in NYC!",
                        "tool_calls": [
                            {
                                "id": "call_456",
                                "type": "function",
                                "function": {
                                    "name": "format_response",
                                    "arguments": '{"temp": 72, "condition": "sunny"}',
                                },
                            }
                        ],
                    }
                }
            ]
        },
        "error_str": None,
        "error_information": None,
        "model_parameters": {"temperature": 0.7},
        "hidden_params": {
            "model_id": "model-123",
            "cache_key": None,
            "api_base": "https://api.openai.com",
            "response_cost": "0.05",
            "litellm_overhead_time_ms": None,
            "additional_headers": None,
            "batch_models": None,
            "litellm_model_name": None,
            "usage_object": None,
        },
        "guardrail_information": None,
        "standard_built_in_tools_params": None,
    }  # type: ignore


class TestDataDogLLMObsLoggerToolCalls:
    """Simple test suite for DataDog LLM Observability Logger tool call handling"""

    @pytest.fixture
    def mock_env_vars(self):
        """Mock environment variables for DataDog"""
        with patch.dict(
            os.environ, {"DD_API_KEY": "test_api_key", "DD_SITE": "us5.datadoghq.com"}
        ):
            yield

    def test_tool_call_span_kind_mapping(self, mock_env_vars):
        """Test that tool call operations are correctly mapped to 'tool' span kind"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            # Test MCP tool call mapping
            from litellm.types.utils import CallTypes

            assert (
                logger._get_datadog_span_kind(CallTypes.call_mcp_tool.value, "123")
                == "tool"
            )

    def test_tool_call_payload_creation(self, mock_env_vars):
        """Test that tool call payloads are created correctly"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            standard_payload = create_standard_logging_payload_with_tool_calls()

            kwargs = {
                "standard_logging_object": standard_payload,
                "litellm_params": {"metadata": {}},
            }

            start_time = datetime.now()
            end_time = datetime.now()

            payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)

            # Verify basic payload structure
            assert payload.get("name") == "litellm_llm_call"
            assert payload.get("status") == "ok"
            assert (
                payload.get("meta", {}).get("kind") == "llm"
            )  # Regular completion, not tool call

            # Verify metrics
            metrics = payload.get("metrics", {})
            assert metrics.get("input_tokens") == 20
            assert metrics.get("output_tokens") == 30
            assert metrics.get("total_tokens") == 50

    def test_tool_call_messages_preserved(self, mock_env_vars):
        """Test that tool call messages are preserved in the payload"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            standard_payload = create_standard_logging_payload_with_tool_calls()

            kwargs = {
                "standard_logging_object": standard_payload,
                "litellm_params": {"metadata": {}},
            }

            start_time = datetime.now()
            end_time = datetime.now()

            payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)

            # Verify input messages include tool calls
            meta = payload.get("meta", {})
            input_meta = meta.get("input", {})
            input_messages = input_meta.get("messages", [])
            assert len(input_messages) == 3

            # Check assistant message has tool calls
            assistant_msg = input_messages[1]
            assert assistant_msg.get("role") == "assistant"
            assert "tool_calls" in assistant_msg
            tool_calls = assistant_msg.get("tool_calls", [])
            assert len(tool_calls) == 1
            tool_call = tool_calls[0]
            function_info = tool_call.get("function", {})
            assert function_info.get("name") == "get_weather"

            # Check tool message
            tool_msg = input_messages[2]
            assert tool_msg.get("role") == "tool"
            assert tool_msg.get("tool_call_id") == "call_123"

    def test_tool_call_response_handling(self, mock_env_vars):
        """Test that tool calls in response are handled correctly"""
        with (
            patch(
                "litellm.integrations.datadog.datadog_llm_obs.get_async_httpx_client"
            ),
            patch("asyncio.create_task"),
        ):
            logger = DataDogLLMObsLogger()

            standard_payload = create_standard_logging_payload_with_tool_calls()

            kwargs = {
                "standard_logging_object": standard_payload,
                "litellm_params": {"metadata": {}},
            }

            start_time = datetime.now()
            end_time = datetime.now()

            payload = logger.create_llm_obs_payload(kwargs, start_time, end_time)

            # Verify output messages include tool calls
            meta = payload.get("meta", {})
            output_meta = meta.get("output", {})
            output_messages = output_meta.get("messages", [])
            assert len(output_messages) == 1

            output_msg = output_messages[0]
            assert output_msg.get("role") == "assistant"
            assert "tool_calls" in output_msg
            output_tool_calls = output_msg.get("tool_calls", [])
            assert len(output_tool_calls) == 1
            output_function_info = output_tool_calls[0].get("function", {})
            assert output_function_info.get("name") == "format_response"


def create_standard_logging_payload_with_spend_metrics() -> StandardLoggingPayload:
    """Create a StandardLoggingPayload object with spend metrics for testing"""
    from datetime import datetime, timezone

    # Create a budget reset time 10 days from now (using "10d" format)
    budget_reset_at = datetime.now(timezone.utc) + timedelta(days=10)

    return {
        "id": "test-request-id-spend",
        "trace_id": "test-trace-id-spend",
        "call_type": "completion",
        "stream": None,
        "response_cost": 0.15,
        "response_cost_failure_debug_info": None,
        "status": "success",
        "custom_llm_provider": "openai",
        "total_tokens": 30,
        "prompt_tokens": 10,
        "completion_tokens": 20,
        "startTime": 1234567890.0,
        "endTime": 1234567891.0,
        "completionStartTime": 1234567890.5,
        "response_time": 1.0,
        "model_map_information": {"model_map_key": "gpt-4", "model_map_value": None},
        "model": "gpt-4",
        "model_id": "model-123",
        "model_group": "openai-gpt",
        "api_base": "https://api.openai.com",
        "metadata": {
            "user_api_key_hash": "test_hash",
            "user_api_key_org_id": None,
            "user_api_key_alias": "test_alias",
            "user_api_key_team_id": "test_team",
            "user_api_key_user_id": "test_user",
            "user_api_key_team_alias": "test_team_alias",
            "user_api_key_user_email": None,
            "user_api_key_end_user_id": None,
            "user_api_key_request_route": None,
            "user_api_key_spend": 0.67,
            "user_api_key_max_budget": 10.0,  # $10 max budget
            "user_api_key_budget_reset_at": budget_reset_at.isoformat(),  # ISO format: 2025-09-26T...
            "spend_logs_metadata": None,
            "requester_ip_address": "127.0.0.1",
            "requester_metadata": None,
            "requester_custom_headers": None,
            "prompt_management_metadata": None,
            "mcp_tool_call_metadata": None,
            "vector_store_request_metadata": None,
            "applied_guardrails": None,
            "usage_object": None,
            "cold_storage_object_key": None,
        },
        "cache_hit": False,
        "cache_key": None,
        "saved_cache_cost": 0.0,
        "request_tags": [],
        "end_user": None,
        "requester_ip_address": "127.0.0.1",
        "messages": [{"role": "user", "content": "Hello, world!"}],
        "response": {"choices": [{"message": {"content": "Hi there!"}}]},
        "error_str": None,
        "error_information": None,
        "model_parameters": {"stream": False},
        "hidden_params": {
            "model_id": "model-123",
            "cache_key": None,
            "api_base": "https://api.openai.com",
            "response_cost": "0.15",
            "litellm_overhead_time_ms": None,
            "additional_headers": None,
            "batch_models": None,
            "litellm_model_name": None,
            "usage_object": None,
        },
        "guardrail_information": None,
        "standard_built_in_tools_params": None,
    }  # type: ignore


@pytest.mark.asyncio
async def test_datadog_llm_obs_spend_metrics(mock_env_vars):
    """Test that budget metrics are properly extracted and logged"""
    datadog_llm_obs_logger = DataDogLLMObsLogger()

    # Create a standard logging payload with spend metrics
    payload = create_standard_logging_payload_with_spend_metrics()

    # Show the budget reset time in ISO format
    budget_reset_iso = payload["metadata"]["user_api_key_budget_reset_at"]
    print(f"Budget reset time (ISO format): {budget_reset_iso}")
    from datetime import datetime, timezone

    print(f"Current time: {datetime.now(timezone.utc).isoformat()}")

    # Test the _get_spend_metrics method
    spend_metrics = datadog_llm_obs_logger._get_spend_metrics(payload)

    # Verify budget metrics are present
    assert "user_api_key_max_budget" in spend_metrics
    assert spend_metrics["user_api_key_max_budget"] == 10.0

    assert "user_api_key_budget_reset_at" in spend_metrics
    # The budget reset should be a datetime string in ISO format
    budget_reset = spend_metrics["user_api_key_budget_reset_at"]
    assert isinstance(budget_reset, str)
    print(f"Budget reset datetime: {budget_reset}")
    # Should be close to 10 days from now
    budget_reset_dt = datetime.fromisoformat(budget_reset.replace("Z", "+00:00"))
    now = datetime.now(timezone.utc)
    time_diff = (budget_reset_dt - now).total_seconds() / 86400  # days
    assert 9.5 <= time_diff <= 10.5  # Should be close to 10 days

    print(f"Spend metrics: {spend_metrics}")


@pytest.mark.asyncio
async def test_datadog_llm_obs_spend_metrics_no_budget(mock_env_vars):
    """Test that spend metrics work when no budget is set"""
    datadog_llm_obs_logger = DataDogLLMObsLogger()

    # Create a standard logging payload without budget metadata
    payload = create_standard_logging_payload_with_spend_metrics()

    # Remove budget-related metadata to test no-budget scenario
    payload["metadata"].pop("user_api_key_max_budget", None)
    payload["metadata"].pop("user_api_key_budget_reset_at", None)

    # Test the _get_spend_metrics method
    spend_metrics = datadog_llm_obs_logger._get_spend_metrics(payload)

    # Verify only response cost is present
    assert "response_cost" in spend_metrics
    assert spend_metrics["response_cost"] == 0.15

    # Budget metrics should not be present
    assert "user_api_key_max_budget" not in spend_metrics
    assert "user_api_key_budget_reset_at" not in spend_metrics

    print(f"Spend metrics (no budget): {spend_metrics}")


@pytest.mark.asyncio
async def test_spend_metrics_in_datadog_payload(mock_env_vars):
    """Test that spend metrics are correctly included in DataDog LLM Observability payloads"""
    from datetime import datetime

    datadog_llm_obs_logger = DataDogLLMObsLogger()

    standard_payload = create_standard_logging_payload_with_spend_metrics()

    kwargs = {
        "standard_logging_object": standard_payload,
        "litellm_params": {"metadata": {}},
    }

    start_time = datetime.now()
    end_time = datetime.now()

    payload = datadog_llm_obs_logger.create_llm_obs_payload(
        kwargs, start_time, end_time
    )

    # Verify basic payload structure
    assert payload.get("name") == "litellm_llm_call"
    assert payload.get("status") == "ok"

    # Verify spend metrics are included in metadata
    meta = payload.get("meta", {})
    assert meta is not None, "Meta section should exist in payload"

    metadata = meta.get("metadata", {})
    assert metadata is not None, "Metadata section should exist in meta"

    spend_metrics = metadata.get("spend_metrics", {})
    assert spend_metrics, "Spend metrics should exist in metadata"

    # Check that all metrics are present
    assert "response_cost" in spend_metrics
    assert "user_api_key_spend" in spend_metrics
    assert "user_api_key_max_budget" in spend_metrics
    assert "user_api_key_budget_reset_at" in spend_metrics

    # Verify the values are correct
    assert spend_metrics["response_cost"] == 0.15  # response_cost
    assert spend_metrics["user_api_key_spend"] == 0.67  # lol
    assert spend_metrics["user_api_key_max_budget"] == 10.0  # max budget

    # Verify budget reset is a datetime string in ISO format
    budget_reset = spend_metrics["user_api_key_budget_reset_at"]
    assert isinstance(budget_reset, str)
    print(
        f"Budget reset in payload: {budget_reset}"
    )  # In StandardLoggingUserAPIKeyMetadata
    user_api_key_budget_reset_at: Optional[str] = None

    # In DDLLMObsSpendMetrics
    user_api_key_budget_reset_at: str
    # Should be close to 10 days from now
    from datetime import datetime, timezone

    budget_reset_dt = datetime.fromisoformat(budget_reset.replace("Z", "+00:00"))
    now = datetime.now(timezone.utc)
    time_diff = (budget_reset_dt - now).total_seconds() / 86400  # days
    assert 9.5 <= time_diff <= 10.5  # Should be close to 10 days
