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

use regex::RegexBuilder;
use serde_json::Value;
use uuid::Uuid;

use super::super::ToolDefinition;
use super::config::JsonParserConfig;
use super::response::{CalledFunction, ToolCallResponse, ToolCallType};

/// Extract individual tool call blocks from the input string for DeepSeek V3 format.
/// Returns a list of strings, each representing one tool call block.
///
/// DeepSeek V3 format: <｜tool▁call▁begin｜>{type}<｜tool▁sep｜>{name}\n```json\n{args}\n```<｜tool▁call▁end｜>
///
fn extract_tool_call_blocks_v3(
    input: &str,
    start_tokens: &[String],
    end_tokens: &[String],
) -> Vec<String> {
    let mut blocks = Vec::new();

    // Filter tokens to find individual call markers (not the wrapper "calls" versions)
    let individual_start_tokens: Vec<&String> = start_tokens
        .iter()
        .filter(|t| t.contains("tool_call_begin") || t.contains("tool▁call▁begin"))
        .collect();

    let individual_end_tokens: Vec<&String> = end_tokens
        .iter()
        .filter(|t| t.contains("tool_call_end") || t.contains("tool▁call▁end"))
        .collect();

    // Try all combinations of individual start and end tokens
    for start_token in individual_start_tokens.iter() {
        for end_token in individual_end_tokens.iter() {
            if start_token.is_empty() || end_token.is_empty() {
                continue;
            }

            // Build regex pattern with escaped tokens
            let escaped_start = regex::escape(start_token);
            let escaped_end = regex::escape(end_token);
            // DeepSeek V3 format: <｜tool▁call▁begin｜>{type}<｜tool▁sep｜>{function_name}\n```json\n{arguments}\n```<｜tool▁call▁end｜>
            let pattern = format!(r"{}(.*?){}", escaped_start, escaped_end);

            if let Ok(regex) = RegexBuilder::new(&pattern)
                .dot_matches_new_line(true)
                .build()
            {
                for capture in regex.captures_iter(input) {
                    if let Some(matched) = capture.get(1) {
                        // Don't trim the content - preserve whitespace for multiline JSON
                        let content = matched.as_str();
                        if !content.trim().is_empty() {
                            blocks.push(content.to_string());
                        }
                    }
                }

                // If we found matches with this token pair, don't try other combinations
                if !blocks.is_empty() {
                    return blocks;
                }
            }
        }
    }

    blocks
}

/// Parse a single tool call block for DeepSeek V3 format.
///
/// Format: {type}<｜tool▁sep｜>{function_name}\n```json\n{json_arguments}\n```
fn parse_single_tool_call_v3(block: &str, separator_tokens: &[String]) -> Option<(String, Value)> {
    // Try each separator token
    for sep_token in separator_tokens.iter() {
        if sep_token.is_empty() {
            continue;
        }

        if let Some((_type_part, function_and_args_part)) = block.split_once(sep_token) {
            // Parse the function name (after the type and separator)
            let (function_name_part, args_block) = function_and_args_part.split_once('\n')?;
            let function_name = function_name_part.trim();
            if function_name.is_empty() || function_name.contains(['{', '}', '[', ']']) {
                continue;
            }

            // DeepSeek V3's documented shape requires a fenced JSON argument
            // block. Treat an unfenced body as malformed rather than parsing it
            // as a call and diverging from upstream recovery behavior.
            let Some(json_start) = args_block.find("```json") else {
                continue;
            };
            let after_fence = &args_block[json_start + "```json".len()..];
            let after_newline = after_fence
                .strip_prefix("\r\n")
                .or_else(|| after_fence.strip_prefix('\n'))
                .unwrap_or(after_fence);
            let args_str = if let Some(json_end) = after_newline.find("```") {
                after_newline[..json_end].trim()
            } else {
                after_newline.trim()
            };

            // Try to parse arguments as JSON
            // First try parsing as-is
            if let Ok(arguments) = serde_json::from_str::<Value>(args_str) {
                return Some((function_name.to_string(), arguments));
            }

            // If that fails, try normalizing the JSON (handle multiline strings with unescaped newlines)
            // This is a lenient approach for malformed JSON that may come from LLMs
            let normalized = args_str
                .lines()
                .map(|line| line.trim_start())
                .collect::<Vec<_>>()
                .join(" ");

            if let Ok(arguments) = serde_json::from_str::<Value>(&normalized) {
                return Some((function_name.to_string(), arguments));
            }
        }
    }

    None
}

fn normal_text_before_wrapper_start(message: &str, config: &JsonParserConfig) -> String {
    wrapper_start_index(message, config)
        .map(|idx| message[..idx].to_string())
        .unwrap_or_default()
}

fn wrapper_start_index(message: &str, config: &JsonParserConfig) -> Option<usize> {
    config
        .tool_call_start_tokens
        .iter()
        .filter(|token| !token.is_empty())
        .filter_map(|token| message.find(token))
        .min()
}

fn has_complete_wrapper_end(message: &str, config: &JsonParserConfig) -> bool {
    config
        .tool_call_end_tokens
        .iter()
        .any(|token| !token.is_empty() && message.contains(token.as_str()))
}

pub fn parse_tool_calls_deepseek_v3(
    message: &str,
    config: &JsonParserConfig,
    _tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
    // Format Structure:
    // <｜tool▁calls▁begin｜><｜tool▁call▁begin｜>{type}<｜tool▁sep｜>{function_name}\n```json\n{json_arguments}\n```<｜tool▁call▁end｜><｜tool▁calls▁end｜>
    let trimmed = message.trim();

    // Early exit if no content
    if trimmed.is_empty() {
        return Ok((vec![], Some(String::new())));
    }

    let mut tool_call_start_tokens = config.tool_call_start_tokens.clone();
    tool_call_start_tokens.extend(vec!["<｜tool▁call▁begin｜>".to_string()]);
    let mut tool_call_end_tokens = config.tool_call_end_tokens.clone();
    tool_call_end_tokens.extend(vec!["<｜tool▁call▁end｜>".to_string()]);
    let separator_tokens = &config.tool_call_separator_tokens;

    // Early exit if no tokens configured
    if tool_call_start_tokens.is_empty() || separator_tokens.is_empty() {
        return Ok((vec![], Some(trimmed.to_string())));
    }

    // Batch parsing requires a complete outer wrapper start; the public
    // detector also accepts partial prefixes for streaming chunk detection.
    if wrapper_start_index(trimmed, config).is_none() {
        if trimmed.contains("<｜tool▁call▁begin｜>") {
            let normal_text = trimmed
                .find("<｜tool▁call▁begin｜>")
                .map(|idx| trimmed[..idx].to_string())
                .unwrap_or_default();
            let blocks = extract_tool_call_blocks_v3(
                trimmed,
                &tool_call_start_tokens,
                &tool_call_end_tokens,
            );
            let mut tool_calls: Vec<ToolCallResponse> = Vec::new();
            for block in blocks {
                if let Some((function_name, arguments)) =
                    parse_single_tool_call_v3(&block, separator_tokens)
                {
                    tool_calls.push(ToolCallResponse {
                        id: format!("call-{}", Uuid::new_v4()),
                        tp: ToolCallType::Function,
                        function: CalledFunction {
                            name: function_name,
                            arguments: serde_json::to_string(&arguments)?,
                        },
                    });
                }
            }
            if !tool_calls.is_empty() {
                tracing::warn!(
                    why = "bare_deepseek_v3_call_without_outer_wrapper",
                    recovered_calls = tool_calls.len(),
                    stripped_bytes = trimmed.len(),
                    "DeepSeek V3 parser recovered bare inner tool-call block and suppressed parser markup from normal_text"
                );
                return Ok((tool_calls, Some(normal_text)));
            }
            tracing::warn!(
                why = "bare_deepseek_v3_call_without_outer_wrapper",
                stripped_bytes = trimmed.len(),
                "DeepSeek V3 parser suppressed malformed bare inner tool-call block so parser markup does not leak into normal_text"
            );
            return Ok((vec![], Some(String::new())));
        }
        return Ok((vec![], Some(trimmed.to_string())));
    }

    let normal_text = normal_text_before_wrapper_start(trimmed, config);

    // Missing outer end-token recovery is finalize-only. Streaming jail paths
    // leave allow_eof_recovery=false so they do not release before later calls
    // or the wrapper end token arrive.
    if !has_complete_wrapper_end(trimmed, config) && !config.allow_eof_recovery {
        return Ok((vec![], Some(normal_text)));
    }

    // Extract individual tool call blocks
    let blocks =
        extract_tool_call_blocks_v3(trimmed, &tool_call_start_tokens, &tool_call_end_tokens);

    if blocks.is_empty() {
        // Found a wrapper start token but no complete individual call. Strip
        // the malformed/truncated tool-call block instead of leaking protocol
        // markers into message content.
        return Ok((vec![], Some(normal_text)));
    }

    // Parse each block to extract function name and arguments
    let mut tool_calls: Vec<ToolCallResponse> = Vec::new();
    for block in blocks {
        if let Some((function_name, arguments)) =
            parse_single_tool_call_v3(&block, separator_tokens)
        {
            tool_calls.push(ToolCallResponse {
                id: format!("call-{}", Uuid::new_v4()),
                tp: ToolCallType::Function,
                function: CalledFunction {
                    name: function_name,
                    arguments: serde_json::to_string(&arguments)?,
                },
            });
        }
    }

    // If no valid tool calls were parsed, strip the wrapper and keep only the
    // prefix before it.
    if tool_calls.is_empty() {
        return Ok((vec![], Some(normal_text)));
    }

    Ok((tool_calls, Some(normal_text)))
}

pub fn detect_tool_call_start_deepseek_v3(chunk: &str, config: &JsonParserConfig) -> bool {
    let trimmed = chunk.trim();
    if trimmed.is_empty() {
        return false;
    }

    // Check for complete start tokens first
    let has_complete_token = config
        .tool_call_start_tokens
        .iter()
        .any(|token| !token.is_empty() && trimmed.contains(token));

    if has_complete_token {
        return true;
    }

    if trimmed.contains("<｜tool▁call▁begin｜>") {
        return true;
    }

    // Check for partial start tokens (streaming scenario)
    // This handles cases where start tokens are split across multiple chunks
    config.tool_call_start_tokens.iter().any(|token| {
        if token.is_empty() {
            return false;
        }
        // Check if the chunk could be a prefix of this start token
        // Handle Unicode character boundaries properly
        for i in 1..=token.chars().count() {
            if let Some(prefix) = token.chars().take(i).collect::<String>().get(..) {
                let prefix_str = &prefix[..prefix.len()];
                if trimmed == prefix_str || trimmed.ends_with(prefix_str) {
                    return true;
                }
            }
        }
        false
    })
}

#[cfg(test)]
mod tests {
    use super::super::config::ToolCallConfig;
    use super::*;

    fn extract_name_and_args(call: ToolCallResponse) -> (String, serde_json::Value) {
        let args: serde_json::Value = serde_json::from_str(&call.function.arguments).unwrap();
        (call.function.name, args)
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.b in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.2.yaml.
    #[test] // TOOLCALLING.batch.2
    fn test_parse_tool_calls_deepseek_v3_basic() {
        let text = r#"<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather
```json
{"location": "HongKong"}
```<｜tool▁call▁end｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather
```json
{"location": "Paris"}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜><｜end▁of▁sentence｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("".to_string()));
        assert_eq!(result.len(), 2);
        let (name, args) = extract_name_and_args(result[0].clone());
        assert_eq!(name, "get_current_weather");
        assert_eq!(args["location"], "HongKong");
        let (name, args) = extract_name_and_args(result[1].clone());
        assert_eq!(name, "get_current_weather");
        assert_eq!(args["location"], "Paris");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.8.a in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.8.yaml.
    #[test] // TOOLCALLING.batch.8
    fn test_parse_tool_calls_deepseek_v3_with_normal_text() {
        let text = r#"The following tool call retrieves weather information: <｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather
```json
{"location": "New York"}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜><｜end▁of▁sentence｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(
            content,
            Some("The following tool call retrieves weather information: ".to_string())
        );
        assert_eq!(result.len(), 1);
        let (name, args) = extract_name_and_args(result[0].clone());
        assert_eq!(name, "get_current_weather");
        assert_eq!(args["location"], "New York");
    }

    #[test]
    fn test_parse_tool_calls_deepseek_v3_partial_wrapper_prefix_is_normal_text() {
        let text = "This is normal text <";
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };

        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();

        assert_eq!(content, Some(text.to_string()));
        assert_eq!(result.len(), 0);
    }

    #[test]
    fn test_parse_tool_calls_deepseek_v3_missing_wrapper_end_requires_eof_recovery() {
        let text = r#"prefix <｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather
```json
{"location": "HongKong"}
```<｜tool▁call▁end｜>"#;
        let mut config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };

        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("prefix ".to_string()));
        assert_eq!(result.len(), 0);

        config.allow_eof_recovery = true;
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("prefix ".to_string()));
        assert_eq!(result.len(), 1);
        let (name, args) = extract_name_and_args(result[0].clone());
        assert_eq!(name, "get_current_weather");
        assert_eq!(args["location"], "HongKong");
    }

    #[test]
    fn test_normal_text_before_wrapper_start_uses_earliest_token() {
        let mut config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        config.tool_call_start_tokens = vec!["<later>".to_string(), "<early>".to_string()];

        assert_eq!(
            normal_text_before_wrapper_start("prefix <early> body <later>", &config),
            "prefix "
        );
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.d in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.4 — recovery from missing start
    fn test_parse_tool_calls_deepseek_v3_without_tool_call_start_token() {
        let text = r#"<｜tool▁call▁begin｜>function宽带}{location": "HongKong"}
```json
}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("".to_string()));
        assert_eq!(result.len(), 0);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.a, TOOLCALLING.batch.7.d in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.7
    fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_multiple_args() {
        let text = r#"<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather
```json
{"location": "Shanghai", "units": "metric"}
```<｜tool▁call▁end｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_weather_forecast
```json
{"location": "Shanghai", "days": 7, "units": "imperial"}
```<｜tool▁call▁end｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_air_quality
```json
{"location": "Shanghai", "radius": 50}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜><｜end▁of▁sentence｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("".to_string()));
        assert_eq!(result.len(), 3);
        let (name, args) = extract_name_and_args(result[0].clone());
        assert_eq!(name, "get_current_weather");
        assert_eq!(args["location"], "Shanghai");
        assert_eq!(args["units"], "metric");
        let (name, args) = extract_name_and_args(result[1].clone());
        assert_eq!(name, "get_weather_forecast");
        assert_eq!(args["location"], "Shanghai");
        assert_eq!(args["days"], 7);
        assert_eq!(args["units"], "imperial");
        let (name, args) = extract_name_and_args(result[2].clone());
        assert_eq!(name, "get_air_quality");
        assert_eq!(args["location"], "Shanghai");
        assert_eq!(args["radius"], 50);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.b in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.4
    fn test_parse_tool_calls_deepseek_v3_with_invalid_json() {
        // Malformed wrapper content is stripped instead of leaked as normal text.
        let text = r#"<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>get_current_weather}{location": "HongKong"}
```json
}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("".to_string()));
        assert_eq!(result.len(), 0);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.c, TOOLCALLING.batch.8.a in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.8.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.8
    fn test_parse_tool_calls_deepseek_v3_with_multi_tool_calls_with_normal_text() {
        // Malformed wrapper content is stripped while preserving prefix text.
        let text = r#"The following tool calls retrieve weather information: <｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function宽带}{location": "HongKong"}
```json
}
```<｜tool▁call▁end｜><｜tool▁call▁begin｜>function宽带}{location": "Shanghai", "days": 7, "units": "imperial"}
```json
}
```<｜tool▁call▁end｜><｜tool▁call▁begin｜>function宽带}{location": "Shanghai", "radius": 50}
```json
}
```<｜tool▁call▁end｜><｜tool▁calls▁end｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(
            content,
            Some("The following tool calls retrieve weather information: ".to_string())
        );
        assert_eq!(result.len(), 0);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.7.b in tests/parity/toolcalling/fixtures/deepseek_v3/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.7, TOOLCALLING.fmt.2
    fn test_parse_tool_calls_deepseek_v3_with_multiline_json() {
        let text = r#"I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure
  and key files to provide you with a comprehensive
  explanation.<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>TodoWrite
```json
{"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｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };

        let (tool_call_results, normal_content) =
            parse_tool_calls_deepseek_v3(text, &config, None).unwrap();

        assert_eq!(tool_call_results.len(), 1);

        let (name, args) = extract_name_and_args(tool_call_results[0].clone());
        assert_eq!(name, "TodoWrite");
        assert_eq!(tool_call_results[0].tp, ToolCallType::Function);

        let todos_array = args["todos"].as_array().unwrap();
        assert_eq!(todos_array.len(), 5);

        assert_eq!(
            todos_array[0]["content"],
            "Explore the root directory structure"
        );
        assert_eq!(todos_array[0]["status"], "in_progress");
        assert_eq!(
            todos_array[0]["activeForm"],
            "Exploring the root directory structure"
        );

        assert_eq!(
            todos_array[1]["content"],
            "Examine package.json and configuration files"
        );
        assert_eq!(todos_array[1]["status"], "pending");

        assert_eq!(
            todos_array[4]["content"],
            "Summarize the codebase purpose and functionality"
        );
        assert_eq!(todos_array[4]["status"], "pending");

        assert_eq!(
            normal_content,
            Some("I'll help you understand this Xiaohongshu codebase. Let me start by exploring the structure\n  and key files to provide you with a comprehensive\n  explanation.".to_string())
        );
    }

    #[test] // TOOLCALLING.stream.4.c
    fn test_parse_deepseek_v3_bare_inner_call_recovers_without_marker_leak() {
        let text = r#"Before <｜tool▁call▁begin｜>function<｜tool▁sep｜>get_weather
```json
{"location": "NYC"}
```
<｜tool▁call▁end｜>
<｜tool▁call▁end｜>
<｜tool▁call▁end｜>"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let (result, content) = parse_tool_calls_deepseek_v3(text, &config, None).unwrap();
        assert_eq!(content, Some("Before ".to_string()));
        assert_eq!(result.len(), 1);
        let (name, args) = extract_name_and_args(result[0].clone());
        assert_eq!(name, "get_weather");
        assert_eq!(args["location"], "NYC");
    }
}

#[cfg(test)]
mod detect_parser_tests {
    use super::super::config::ToolCallConfig;
    use super::*;
    #[test] // helper
    fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token() {
        let text = r#"<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function宽带}"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let result = detect_tool_call_start_deepseek_v3(text, &config);
        assert!(result);
    }

    #[test] // helper
    fn test_detect_tool_call_start_deepseek_v3_chunk_without_tool_call_start_token() {
        let text = r#"<｜tool▁call▁begin｜>function宽带}"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let result = detect_tool_call_start_deepseek_v3(text, &config);
        assert!(result);
    }

    #[test] // helper
    fn test_detect_tool_call_start_deepseek_v3_chunk_with_tool_call_start_token_in_middle() {
        let text = r#"The following tool calls retrieve weather information: <｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function宽带}"#;
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };
        let result = detect_tool_call_start_deepseek_v3(text, &config);
        assert!(result);
    }

    #[test] // helper, TOOLCALLING.stream.3
    fn test_detect_tool_call_start_deepseek_v3_partial_tokens() {
        // Test partial token detection for streaming scenarios with unicode characters
        let config = match ToolCallConfig::deepseek_v3().parser_config {
            super::super::config::ParserConfig::Json(cfg) => cfg,
            _ => panic!("Expected JSON parser config"),
        };

        // Test various partial prefixes
        assert!(
            detect_tool_call_start_deepseek_v3("<", &config),
            "'<' should be detected as potential start"
        );
        assert!(
            detect_tool_call_start_deepseek_v3("<｜", &config),
            "'<｜' should be detected as potential start"
        );
        assert!(
            detect_tool_call_start_deepseek_v3("<｜tool", &config),
            "'<｜tool' should be detected as potential start"
        );
        assert!(
            detect_tool_call_start_deepseek_v3("<｜tool▁calls", &config),
            "'<｜tool▁calls' should be detected as potential start"
        );

        // Test that unrelated text is not detected
        assert!(
            !detect_tool_call_start_deepseek_v3("hello world", &config),
            "'hello world' should not be detected"
        );
        assert!(
            !detect_tool_call_start_deepseek_v3("xyz", &config),
            "'xyz' should not be detected"
        );
    }
}
