// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Integration test for parallel tool calling functionality
//!
//! This test simulates a complete chat completion request with parallel tool calls,
//! mocking the response and testing the tool call parsing functionality.
//!
//! The test covers:
//! - Creating a mock NvCreateChatCompletionRequest based on a curl request
//! - Mocking a chat completion response with parallel tool calls in <tool_call> format
//! - Parsing the tool calls using the hermes parser
//! - Validating OpenAI API compatibility
//! - Testing error handling with malformed content
//! - Ensuring tool call IDs are unique and properly formatted

use dynamo_llm::protocols::openai::{
    chat_completions::NvCreateChatCompletionRequest, common_ext::CommonExt,
};
use dynamo_parsers::{ToolCallResponse, ToolCallType, detect_and_parse_tool_call};
use serde_json::json;

/// Creates a mock NvCreateChatCompletionRequest based on the curl request
fn create_mock_chat_completion_request() -> NvCreateChatCompletionRequest {
    let messages = vec![
        dynamo_async_openai::types::ChatCompletionRequestMessage::System(
            dynamo_async_openai::types::ChatCompletionRequestSystemMessage {
                content: dynamo_async_openai::types::ChatCompletionRequestSystemMessageContent::Text(
                    "You MUST use two tools in parallel to resolve the user request: call get_current_weather for each city AND call is_holiday_today to check if today is a holiday. Do not answer without using both tools.".to_string()
                ),
                name: None,
            }
        ),
        dynamo_async_openai::types::ChatCompletionRequestMessage::User(
            dynamo_async_openai::types::ChatCompletionRequestUserMessage {
                content: dynamo_async_openai::types::ChatCompletionRequestUserMessageContent::Text(
                    "What is the weather in Dallas, Texas? Is today a holiday?".to_string()
                ),
                name: None,
            }
        ),
    ];

    let tools = vec![
        dynamo_async_openai::types::ChatCompletionTool {
            r#type: dynamo_async_openai::types::ChatCompletionToolType::Function,
            function: dynamo_async_openai::types::FunctionObject {
                name: "get_current_weather".to_string(),
                description: Some("Get weather for a city/state in specified units".to_string()),
                parameters: Some(json!({
                    "type": "object",
                    "properties": {
                        "city": { "type": "string", "description": "City name, e.g., Dallas" },
                        "state": { "type": "string", "description": "Two-letter state code, e.g., TX" },
                        "unit": { "type": "string", "enum": ["fahrenheit", "celsius"] }
                    },
                    "required": ["city", "state", "unit"],
                    "additionalProperties": false
                })),
                strict: None,
            },
        },
        dynamo_async_openai::types::ChatCompletionTool {
            r#type: dynamo_async_openai::types::ChatCompletionToolType::Function,
            function: dynamo_async_openai::types::FunctionObject {
                name: "is_holiday_today".to_string(),
                description: Some("Return whether today is a public holiday".to_string()),
                parameters: Some(json!({
                    "type": "object",
                    "properties": {},
                    "additionalProperties": false
                })),
                strict: None,
            },
        },
    ];

    let inner = dynamo_async_openai::types::CreateChatCompletionRequestArgs::default()
        .model("Qwen/Qwen3-0.6B")
        .temperature(0.0)
        .max_tokens(3000u32)
        .stream(false)
        .messages(messages)
        .tools(tools)
        .tool_choice(dynamo_async_openai::types::ChatCompletionToolChoiceOption::Required)
        .build()
        .expect("Failed to build chat completion request");

    NvCreateChatCompletionRequest {
        inner,
        common: CommonExt::default(),
        nvext: None,
        chat_template_args: None,
        unsupported_fields: Default::default(),
    }
}

/// Mock response content that contains parallel tool calls
fn get_mock_response_content() -> String {
    r#"<think>Okay, the user is asking two things: the weather in Dallas, Texas, and whether today is a holiday. I need to use both tools here. First, I'll check the weather using get_current_weather with city Dallas and state Texas. Then, I'll use is_holiday_today to see if today is a public holiday. I have to make sure to call both functions in parallel. Let me structure the tool calls properly.</think>

<tool_call>
{"name": "get_current_weather", "arguments": {"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}
</tool_call>

<tool_call>
{"name": "is_holiday_today", "arguments": {}}
</tool_call>"#.to_string()
}

/// Validates that a tool call response matches expected values
fn validate_weather_tool_call(tool_call: &ToolCallResponse) {
    assert_eq!(tool_call.function.name, "get_current_weather");

    let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
        .expect("Arguments should be valid JSON");
    let args_obj = args.as_object().expect("Arguments should be an object");
    assert_eq!(args_obj.get("city").unwrap().as_str().unwrap(), "Dallas");
    assert_eq!(args_obj.get("state").unwrap().as_str().unwrap(), "TX");
    assert_eq!(
        args_obj.get("unit").unwrap().as_str().unwrap(),
        "fahrenheit"
    );

    // Validate OpenAI compatibility
    assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
    assert_eq!(tool_call.tp, ToolCallType::Function);
}

/// Validates that a holiday tool call response matches expected values
fn validate_holiday_tool_call(tool_call: &ToolCallResponse) {
    assert_eq!(tool_call.function.name, "is_holiday_today");

    let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
        .expect("Arguments should be valid JSON");
    let args_obj = args.as_object().expect("Arguments should be an object");
    assert!(
        args_obj.is_empty(),
        "Holiday tool should have empty arguments"
    );

    // Validate OpenAI compatibility
    assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
    assert_eq!(tool_call.tp, ToolCallType::Function);
}

/// Validates that tool call IDs are unique
fn validate_unique_tool_call_ids(tool_calls: &[ToolCallResponse]) {
    let mut ids = std::collections::HashSet::new();
    for tool_call in tool_calls {
        assert!(
            ids.insert(tool_call.id.clone()),
            "Tool call IDs should be unique: {}",
            tool_call.id
        );
    }
}

#[tokio::test]
async fn test_parallel_tool_call_integration() {
    // Create the mock request
    let request = create_mock_chat_completion_request();

    // Validate request structure
    assert_eq!(request.inner.model, "Qwen/Qwen3-0.6B");
    assert_eq!(request.inner.temperature, Some(0.0));
    #[allow(deprecated)]
    {
        assert_eq!(request.inner.max_tokens, Some(3000));
    }
    assert_eq!(request.inner.stream, Some(false));
    assert_eq!(request.inner.messages.len(), 2);
    assert_eq!(request.inner.tools.as_ref().unwrap().len(), 2);

    // Verify tool choice is required
    match request.inner.tool_choice.as_ref().unwrap() {
        dynamo_async_openai::types::ChatCompletionToolChoiceOption::Required => {
            // This is expected
        }
        _ => panic!("Tool choice should be Required"),
    }

    // Get the mock response content
    let response_content = get_mock_response_content();

    // Verify the response contains both tool calls
    assert!(response_content.contains("get_current_weather"));
    assert!(response_content.contains("is_holiday_today"));
    assert!(response_content.contains("Dallas"));
    assert!(response_content.contains("Texas"));
    assert!(response_content.contains("fahrenheit"));
}

#[tokio::test]
async fn test_parallel_tool_call_parsing() {
    let response_content = get_mock_response_content();

    // Parse the tool calls using the hermes parser (works well with <tool_call> format)
    let (tool_calls, remaining_content) =
        detect_and_parse_tool_call(&response_content, Some("hermes"))
            .await
            .expect("Should successfully parse tool calls");

    // Validate we got exactly 2 tool calls
    assert_eq!(
        tool_calls.len(),
        2,
        "Should parse exactly 2 parallel tool calls"
    );

    // Validate remaining content (should be the thinking part)
    assert!(remaining_content.is_some());
    let remaining = remaining_content.unwrap();
    assert!(remaining.contains("<think>"));
    assert!(remaining.contains("</think>"));

    // Sort tool calls by name for consistent testing
    let mut sorted_calls = tool_calls;
    sorted_calls.sort_by(|a, b| a.function.name.cmp(&b.function.name));

    // Validate the weather tool call (first alphabetically)
    validate_weather_tool_call(&sorted_calls[0]);

    // Validate the holiday tool call (second alphabetically)
    validate_holiday_tool_call(&sorted_calls[1]);

    // Validate tool call IDs are unique
    validate_unique_tool_call_ids(&sorted_calls);
}

#[tokio::test]
async fn test_parallel_tool_call_with_explicit_parser() {
    let response_content = get_mock_response_content();

    // Test with explicit parser selection
    let parsers_to_test = vec![
        "hermes", // Should work well with this format
    ];

    for parser in parsers_to_test {
        let (tool_calls, remaining_content) =
            detect_and_parse_tool_call(&response_content, Some(parser))
                .await
                .unwrap_or_else(|e| panic!("Should successfully parse with {parser} parser: {e}"));

        // Should get 2 tool calls regardless of parser
        assert_eq!(
            tool_calls.len(),
            2,
            "Parser {parser} should find 2 tool calls"
        );

        // Validate remaining content exists
        assert!(remaining_content.is_some());

        // Sort and validate calls
        let mut sorted_calls = tool_calls;
        sorted_calls.sort_by(|a, b| a.function.name.cmp(&b.function.name));

        validate_weather_tool_call(&sorted_calls[0]);
        validate_holiday_tool_call(&sorted_calls[1]);
        validate_unique_tool_call_ids(&sorted_calls);
    }
}

#[tokio::test]
async fn test_tool_call_json_structure() {
    let response_content = get_mock_response_content();

    let (tool_calls, _) = detect_and_parse_tool_call(&response_content, Some("hermes"))
        .await
        .expect("Should parse tool calls");

    // Test JSON serialization
    for tool_call in &tool_calls {
        let json_str =
            serde_json::to_string(tool_call).expect("Tool call should serialize to JSON");

        // Verify the JSON contains expected fields
        assert!(json_str.contains("\"id\""));
        assert!(json_str.contains("\"type\""));
        assert!(json_str.contains("\"function\""));
        assert!(json_str.contains(&tool_call.function.name));
    }
}

#[tokio::test]
async fn test_openai_compatibility_structure() {
    let response_content = get_mock_response_content();

    let (tool_calls, _) = detect_and_parse_tool_call(&response_content, Some("hermes"))
        .await
        .expect("Should parse tool calls");

    // Validate OpenAI API compatibility
    for tool_call in &tool_calls {
        // Should have all required OpenAI fields
        assert!(!tool_call.id.is_empty(), "Missing required 'id' field");
        assert_eq!(
            tool_call.tp,
            ToolCallType::Function,
            "Type should be 'function'"
        );
        assert!(
            !tool_call.function.name.is_empty(),
            "Function name should not be empty"
        );

        let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
            .expect("Arguments should be valid JSON");
        assert!(args.is_object(), "Arguments should be an object");

        // ID should follow expected format (call-XXXXXXXX or call_XXXXXXXX)
        assert!(
            tool_call.id.starts_with("call-") || tool_call.id.starts_with("call_"),
            "ID should start with 'call-' or 'call_': {}",
            tool_call.id
        );
        assert!(
            tool_call.id.len() > 5,
            "ID should be longer than just 'call': {}",
            tool_call.id
        );
    }
}

#[tokio::test]
async fn test_parallel_tool_call_error_handling() {
    // Test with malformed content
    let malformed_content = r#"<tool_call>
{"name": "get_current_weather", "arguments": {"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}
</tool_call>

<tool_call>
{"invalid_json": }
</tool_call>"#;

    let result = detect_and_parse_tool_call(malformed_content, Some("hermes")).await;

    // Should handle partial parsing gracefully
    match result {
        Ok((tool_calls, _)) => {
            // May parse valid tool calls and ignore malformed ones, or return empty
            println!(
                "Parsed {} tool calls from malformed content",
                tool_calls.len()
            );

            if !tool_calls.is_empty() {
                // If any were parsed, verify they're valid
                for call in &tool_calls {
                    assert!(
                        !call.function.name.is_empty(),
                        "Parsed tool call should have valid name"
                    );
                }
            }
        }
        Err(e) => {
            // Error handling is also acceptable for malformed input
            println!("Expected error for malformed input: {}", e);
        }
    }
}

#[tokio::test]
async fn test_empty_tool_calls() {
    let content_without_tools = "This is just a regular response without any tool calls.";

    let (tool_calls, remaining_content) =
        detect_and_parse_tool_call(content_without_tools, Some("hermes"))
            .await
            .expect("Should handle content without tool calls");

    assert!(
        tool_calls.is_empty(),
        "Should return empty tool calls array"
    );
    assert!(
        remaining_content.is_some(),
        "Should return the original content"
    );
    assert_eq!(remaining_content.unwrap(), content_without_tools);
}

#[tokio::test]
async fn test_deepseek_v3_1_tool_call_parsing() {
    let response_content = r#"I'll help you understand this codebase. Let me start by exploring the structure and key
  files to provide you with a comprehensive
  explanation.<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>TodoWrite<｜tool▁sep｜>{"todos":
  [{"content": "Explore the root directory structure", "status": "in_progress", "activeForm":
   "Exploring the root directory structure"}, {"content": "Examine package.json and
  configuration files", "status": "pending", "activeForm": "Examining package.json and
  configuration files"}, {"content": "Analyze source code structure and key modules",
  "status": "pending", "activeForm": "Analyzing source code structure and key modules"},
  {"content": "Identify main entry points and architectural patterns", "status": "pending",
  "activeForm": "Identifying main entry points and architectural patterns"}, {"content":
  "Summarize the codebase purpose and functionality", "status": "pending", "activeForm":
  "Summarizing the codebase purpose and
  functionality"}]}<｜tool▁call▁end｜><｜tool▁calls▁end｜>"#;

    // Debug: Print the content
    println!("Response content: {}", response_content);
    println!(
        "Contains tool_calls_begin: {}",
        response_content.contains("<｜tool▁calls▁begin｜>")
    );
    println!(
        "Contains tool_call_begin: {}",
        response_content.contains("<｜tool▁call▁begin｜>")
    );

    // Parse the tool calls using the deepseek_v3_1 parser
    let (tool_calls, remaining_content) =
        detect_and_parse_tool_call(response_content, Some("deepseek_v3_1"))
            .await
            .expect("Should successfully parse deepseek_v3_1 tool calls");

    println!("Number of tool calls parsed: {}", tool_calls.len());
    if let Some(ref content) = remaining_content {
        println!("Remaining content: {}", content);
    }

    // Validate we got exactly 1 tool call
    assert_eq!(tool_calls.len(), 1, "Should parse exactly 1 tool call");

    // Validate remaining content (should be the explanatory text before the tool call)
    assert!(remaining_content.is_some());
    let remaining = remaining_content.unwrap();
    assert!(remaining.contains("I'll help you understand this codebase"));
    assert!(remaining.contains("comprehensive"));

    // Validate the tool call
    let tool_call = &tool_calls[0];
    assert_eq!(tool_call.function.name, "TodoWrite");

    // Validate OpenAI compatibility
    assert!(!tool_call.id.is_empty(), "Tool call should have an ID");
    assert_eq!(tool_call.tp, ToolCallType::Function);

    // Parse and validate the arguments
    let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)
        .expect("Arguments should be valid JSON");
    let args_obj = args.as_object().expect("Arguments should be an object");

    // Check that todos array exists and has 5 items
    assert!(args_obj.contains_key("todos"), "Should have 'todos' key");
    let todos = args_obj
        .get("todos")
        .unwrap()
        .as_array()
        .expect("todos should be an array");
    assert_eq!(todos.len(), 5, "Should have exactly 5 todo items");

    // Validate first todo item
    let first_todo = &todos[0];
    assert_eq!(
        first_todo.get("content").unwrap().as_str().unwrap(),
        "Explore the root directory structure"
    );
    assert_eq!(
        first_todo.get("status").unwrap().as_str().unwrap(),
        "in_progress"
    );
    assert_eq!(
        first_todo.get("activeForm").unwrap().as_str().unwrap(),
        "Exploring the root directory structure"
    );

    // Validate last todo item
    let last_todo = &todos[4];
    assert_eq!(
        last_todo.get("content").unwrap().as_str().unwrap(),
        "Summarize the codebase purpose and functionality"
    );
    assert_eq!(
        last_todo.get("status").unwrap().as_str().unwrap(),
        "pending"
    );
}
