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

// Reference implementation:
// https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/function_call/kimik2_detector.py
// https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/kimi_k2_tool_parser.py

use std::sync::OnceLock;

use regex::Regex;

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

static ID_REGEX: OnceLock<Regex> = OnceLock::new();

static TOOL_CALL_REGEX: OnceLock<Regex> = OnceLock::new();

/// Returns the cached regex that captures `function_id` (e.g. `functions.get_weather:0`) and
/// `arguments` (JSON object) between the configured `call_start`, `argument_begin`, and
/// `call_end` tokens.
///
/// The `function_id` pattern `[\w.\-]+:\d+` matches the `functions.name:index` format used by
/// Kimi K2, consistent with sglang's reference implementation. The hyphen is included to
/// support function names with dashes (common in MCP tools, e.g. `mcp__portal__search-documents`).
fn get_tool_call_regex(config: &KimiK2ParserConfig) -> &'static Regex {
    TOOL_CALL_REGEX.get_or_init(|| {
        // Arguments capture is intentionally permissive (`.*?`) rather than
        // `\{...\}` so that truncated JSON (e.g. `{"location":"NYC` from
        // max_tokens / EOS) still matches. The downstream `serde_json::from_str`
        // is the validator: well-formed payloads parse, malformed/truncated
        // ones fall back to the raw-string arguments path.
        let pattern = format!(
            r"(?s){}\s*(?P<function_id>[\w.\-]+:\d+)\s*{}\s*(?P<arguments>.*?)\s*{}",
            regex::escape(&config.call_start),
            regex::escape(&config.argument_begin),
            regex::escape(&config.call_end),
        );
        Regex::new(&pattern).expect("Failed to compile kimi k2 tool call regex")
    })
}

fn get_id_regex() -> &'static Regex {
    ID_REGEX.get_or_init(|| {
        Regex::new(r"^(?:functions\.)?(?P<name>[\w.\-]+):(?P<index>\d+)$")
            .expect("Failed to compile kimi k2 id regex")
    })
}

/// Check if a chunk contains the start of a Kimi K2 style tool call.
/// Detects `<|tool_calls_section_begin|>` (or singular variant) or partial match for streaming.
pub fn detect_tool_call_start_kimi_k2(chunk: &str, config: &KimiK2ParserConfig) -> bool {
    if chunk.contains(config.call_start.as_str()) {
        return true;
    }

    let mut start_tokens = config.section_start_variants.clone();
    start_tokens.push(config.call_start.clone());

    for start_token in &start_tokens {
        debug_assert!(
            start_token.is_ascii(),
            "Kimi K2 section tokens must be ASCII for safe byte slicing, got: {start_token:?}"
        );

        // Check for complete start token.
        if chunk.contains(start_token.as_str()) {
            return true;
        }

        // Check for partial match at the end of the chunk (for streaming).
        for i in 1..start_token.len() {
            if chunk.ends_with(&start_token[..i]) {
                return true;
            }
        }
    }

    false
}

/// Returns the position after `<|tool_calls_section_end|>` (or singular variant) or the length
/// of the chunk if not found.
/// Returns `Some(pos)` when `section_end` is found, or `None` when it is
/// missing. `None` tells the streaming jail that the section is not properly
/// closed and it should keep accumulating instead of early-exiting.
pub fn find_tool_call_end_position_kimi_k2(
    chunk: &str,
    config: &KimiK2ParserConfig,
) -> Option<usize> {
    let mut earliest: Option<usize> = None;
    for end_token in &config.section_end_variants {
        if let Some(pos) = chunk.find(end_token.as_str()) {
            let end_pos = pos + end_token.len();
            earliest = Some(earliest.map_or(end_pos, |e: usize| e.min(end_pos)));
        }
    }
    earliest
}

/// Format:
/// ```text
/// <|tool_calls_section_begin|>
/// <|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|>
/// <|tool_calls_section_end|>
/// ```
///
/// Returns (parsed_tool_calls, normal_text_content)
pub fn try_tool_call_parse_kimi_k2(
    message: &str,
    config: &KimiK2ParserConfig,
    tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
    let (normal_text, tool_calls) = extract_tool_calls(message, config, tools)?;

    let normal_content = if normal_text.is_empty() {
        Some("".to_string())
    } else {
        Some(normal_text)
    };

    Ok((tool_calls, normal_content))
}

/// Find the first occurrence of any section start variant in `text[cursor..]`.
/// Returns `(relative_position, matched_token_length)` or `None`.
fn find_section_start(
    text: &str,
    cursor: usize,
    config: &KimiK2ParserConfig,
) -> Option<(usize, usize)> {
    let mut best: Option<(usize, usize)> = None;
    for variant in &config.section_start_variants {
        if let Some(pos) = text[cursor..].find(variant.as_str())
            && best.is_none_or(|(bp, _)| pos < bp)
        {
            best = Some((pos, variant.len()));
        }
    }
    best
}

/// Find the first occurrence of any section end variant in `text[from..]`.
/// Returns `(relative_position, matched_token_length)` or `None`.
fn find_section_end(
    text: &str,
    from: usize,
    config: &KimiK2ParserConfig,
) -> Option<(usize, usize)> {
    let mut best: Option<(usize, usize)> = None;
    for variant in &config.section_end_variants {
        if let Some(pos) = text[from..].find(variant.as_str())
            && best.is_none_or(|(bp, _)| pos < bp)
        {
            best = Some((pos, variant.len()));
        }
    }
    best
}

/// True for prefix-only narration before a tool section where vLLM preserves
/// the wrapper-adjacent trailing space (TOOLCALLING.batch.8.a).
fn should_preserve_vllm_prefix_trailing_space(normal_parts: &[String]) -> bool {
    let mut non_empty_parts = normal_parts
        .iter()
        .enumerate()
        .filter(|(_, part)| !part.trim().is_empty());

    matches!(
        non_empty_parts.next(),
        Some((0, part)) if part.chars().next_back().is_some_and(|ch| ch.is_whitespace())
    ) && non_empty_parts.next().is_none()
}

/// True when the first visible normal text appears only after a parsed tool
/// section, so the intermediate Kimi tool-call turn should expose no content
/// (TOOLCALLING.batch.8.b).
fn should_drop_post_tool_text_without_prefix(normal_parts: &[String]) -> bool {
    normal_parts
        .iter()
        .enumerate()
        .find(|(_, part)| !part.trim().is_empty())
        .is_some_and(|(idx, _)| idx != 0)
}

/// True when there is prefix narration before the first tool section plus
/// post-wrapper or inter-wrapper narration that should be dropped for Kimi
/// tool-call turns (TOOLCALLING.batch.2.c, 8.c, 8.d).
fn should_drop_post_tool_text_after_prefix(normal_parts: &[String]) -> bool {
    normal_parts
        .first()
        .is_some_and(|prefix| !prefix.trim().is_empty())
        && normal_parts
            .iter()
            .skip(1)
            .any(|part| !part.trim().is_empty())
}

/// Extract tool calls and normal text from message.
///
/// ## Difference from Moonshot's reference implementation
///
/// The reference parser in
/// [tool_call_guidance.md](https://huggingface.co/moonshotai/Kimi-K2-Instruct/blob/main/docs/tool_call_guidance.md)
/// requires `section_end` to extract any tool calls:
///
/// ```python
/// pattern = r"<\|tool_calls_section_begin\|>(.*?)<\|tool_calls_section_end\|>"
/// tool_calls_sections = re.findall(pattern, tool_call_rsp, re.DOTALL)
/// ```
///
/// When `section_end` is missing (model hit max_tokens, EOS, or stop sequence),
/// `re.findall` returns `[]` and all complete individual tool calls are silently
/// dropped — even when individual calls have complete `call_begin` + args +
/// `call_end` markers.
///
/// This implementation treats a missing `section_end` as "section extends to
/// end-of-string", equivalent to:
///
/// ```python
/// pattern = r"<\|tool_calls_section_begin\|>(.*?)(?:<\|tool_calls_section_end\|>|$)"
/// ```
///
/// This allows recovery of complete individual tool calls from truncated output.
fn extract_tool_calls(
    text: &str,
    config: &KimiK2ParserConfig,
    tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<(String, Vec<ToolCallResponse>)> {
    let mut normal_parts = Vec::new();
    let mut calls = Vec::new();
    let mut cursor = 0;

    if find_section_start(text, 0, config).is_none()
        && let Some(marker_idx) = first_orphan_kimi_marker_index(text, config)
    {
        let orphan_tail = &text[marker_idx..];
        if orphan_tail.starts_with(config.call_start.as_str()) {
            let mut recovered = parse_section_block(orphan_tail, config, tools)?;
            if !recovered.is_empty() {
                tracing::warn!(
                    why = "bare_call_recovery",
                    recovered_calls = recovered.len(),
                    recovered_bytes = orphan_tail.len(),
                    kept_prefix_bytes = marker_idx,
                    "Kimi K2 parser recovered complete call(s) without section_begin"
                );
                calls.append(&mut recovered);
                return Ok((text[..marker_idx].trim().to_string(), calls));
            }
        }
        return Ok((text[..marker_idx].trim().to_string(), calls));
    }

    while cursor < text.len() {
        if let Some((start_pos, _start_len)) = find_section_start(text, cursor, config) {
            let abs_start = cursor + start_pos;
            let gap = &text[cursor..abs_start];
            if let Some((prefix, mut recovered)) =
                recover_bare_kimi_calls_in_span(gap, config, tools)?
            {
                normal_parts.push(prefix);
                calls.append(&mut recovered);
            } else {
                normal_parts.push(gap.to_string());
            }

            // Add text before tool call section to normal parts.

            let (block, next_cursor) =
                if let Some((end_pos, end_len)) = find_section_end(text, abs_start, config) {
                    let abs_end = abs_start + end_pos + end_len;
                    (&text[abs_start..abs_end], abs_end)
                } else {
                    // No section_end found — treat rest of string as section
                    // body. Complete individual calls can still be extracted;
                    // truly truncated calls (no call_end) are ignored by
                    // parse_section_block's regex.
                    (&text[abs_start..], text.len())
                };

            if let Ok(mut parsed_calls) = parse_section_block(block, config, tools) {
                calls.append(&mut parsed_calls);
            }

            cursor = next_cursor;
        } else {
            // No more tool call sections.
            let gap = &text[cursor..];
            if let Some((prefix, mut recovered)) =
                recover_bare_kimi_calls_in_span(gap, config, tools)?
            {
                normal_parts.push(prefix);
                calls.append(&mut recovered);
            } else {
                normal_parts.push(gap.to_string());
            }
            break;
        }
    }

    let joined_normal_text = normal_parts.join("");
    let normal_text =
        if !calls.is_empty() && should_drop_post_tool_text_without_prefix(&normal_parts) {
            // Kimi tool-call responses are intermediate assistant turns: the
            // official API flow treats content as empty while tool_calls are
            // emitted, then asks the client to execute tools before returning
            // final user-facing content. Match vLLM/SGLang by not surfacing
            // post-wrapper text when there is no prefix narration.
            String::new()
        } else if !calls.is_empty() && should_drop_post_tool_text_after_prefix(&normal_parts) {
            // Kimi tool-call responses are intermediate assistant turns even
            // when a single section contains multiple calls or a turn contains
            // multiple sections. Preserve only the prefix emitted before the
            // first tool section and drop post-wrapper/inter-wrapper text;
            // final user-facing content should be produced after tool results.
            normal_parts[0].trim_start().to_string()
        } else if !calls.is_empty() && should_preserve_vllm_prefix_trailing_space(&normal_parts) {
            // vLLM preserves wrapper-adjacent trailing whitespace when the
            // response has only prefix narration before a Kimi tool section.
            // Keep this compatibility path narrow to avoid changing Dynamo's
            // existing handling of post-wrapper/inter-wrapper narration.
            normal_parts[0].trim_start().to_string()
        } else {
            joined_normal_text.trim().to_string()
        };
    Ok((normal_text, calls))
}

fn recover_bare_kimi_calls_in_span(
    span: &str,
    config: &KimiK2ParserConfig,
    tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<Option<(String, Vec<ToolCallResponse>)>> {
    let Some(marker_idx) = first_orphan_kimi_marker_index(span, config) else {
        return Ok(None);
    };
    let orphan_tail = &span[marker_idx..];
    if !orphan_tail.starts_with(config.call_start.as_str()) {
        return Ok(None);
    }

    let recovered = parse_section_block(orphan_tail, config, tools)?;
    if recovered.is_empty() {
        return Ok(None);
    }

    tracing::warn!(
        why = "bare_call_gap_recovery",
        recovered_calls = recovered.len(),
        recovered_bytes = orphan_tail.len(),
        kept_prefix_bytes = marker_idx,
        "Kimi K2 parser recovered complete bare call(s) before a later section_begin"
    );
    Ok(Some((span[..marker_idx].trim().to_string(), recovered)))
}

fn first_orphan_kimi_marker_index(text: &str, config: &KimiK2ParserConfig) -> Option<usize> {
    let mut best = [
        config.call_start.as_str(),
        config.call_end.as_str(),
        config.argument_begin.as_str(),
    ]
    .into_iter()
    .filter_map(|marker| text.find(marker))
    .min();

    for marker in &config.section_end_variants {
        if let Some(idx) = text.find(marker.as_str()) {
            best = Some(best.map_or(idx, |current| current.min(idx)));
        }
    }

    best
}

/// Parse a tool calls section block, extracting individual tool calls.
///
/// The block is between `<|tool_calls_section_begin|>` and `<|tool_calls_section_end|>`.
/// Each individual call is between `<|tool_call_begin|>` and `<|tool_call_end|>`.
fn parse_section_block(
    block: &str,
    config: &KimiK2ParserConfig,
    tools: Option<&[ToolDefinition]>,
) -> anyhow::Result<Vec<ToolCallResponse>> {
    let tool_call_regex = get_tool_call_regex(config);
    let id_regex = get_id_regex();

    let mut results = Vec::new();

    for cap in tool_call_regex.captures_iter(block) {
        let function_id = cap
            .name("function_id")
            .map(|m| m.as_str().trim())
            .unwrap_or("");
        let arguments_raw = cap
            .name("arguments")
            .map(|m| m.as_str().trim())
            .unwrap_or("{}");

        // Parse function ID
        let function_name = if let Some(id_cap) = id_regex.captures(function_id) {
            id_cap
                .name("name")
                .map(|m| m.as_str().to_string())
                .unwrap_or_default()
        } else {
            // Fallback: use the whole ID as the function name
            tracing::warn!(
                "Unexpected tool_call_id format: '{}', using as-is",
                function_id
            );
            function_id.to_string()
        };

        if function_name.is_empty() {
            continue;
        }

        // Validate function name against tools if provided
        if let Some(tools) = tools
            && !tools.iter().any(|t| t.name == function_name)
        {
            tracing::warn!("Tool '{}' is not defined in the tools list.", function_name);
        }

        // Validate JSON arguments
        let arguments_json = match serde_json::from_str::<serde_json::Value>(arguments_raw) {
            Ok(val) => serde_json::to_string(&val)?,
            Err(e) => {
                tracing::warn!(
                    "Failed to parse JSON arguments for tool '{}': {}. Using raw string.",
                    function_name,
                    e,
                );
                arguments_raw.to_string()
            }
        };

        // NOTE: Unlike other parsers (XML, DSML) which generate `call-{UUID}` IDs,
        // we preserve the model's native function_id (e.g., "functions.bash:0") here.
        // This matches the behavior of vllm/sglang and is required for Kimi K2 compatibility.
        let tool_call = ToolCallResponse {
            id: function_id.to_string(),
            tp: ToolCallType::Function,
            function: CalledFunction {
                name: function_name,
                arguments: arguments_json,
            },
        };

        results.push(tool_call);
    }

    Ok(results)
}

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

    fn default_config() -> KimiK2ParserConfig {
        KimiK2ParserConfig::default()
    }

    #[test] // detection helper
    fn test_detect_tool_call_start() {
        let config = default_config();
        assert!(detect_tool_call_start_kimi_k2(
            "<|tool_calls_section_begin|>",
            &config
        ));
        assert!(detect_tool_call_start_kimi_k2(
            "text <|tool_calls_section_begin|>",
            &config
        ));
        // Partial match at end
        assert!(detect_tool_call_start_kimi_k2("<|tool_calls_sec", &config));
        assert!(detect_tool_call_start_kimi_k2("<|", &config));
        // No match
        assert!(!detect_tool_call_start_kimi_k2(
            "no tool call here",
            &config
        ));
        assert!(!detect_tool_call_start_kimi_k2("toolcall", &config));
    }

    #[test] // detection helper
    fn test_find_tool_call_end_position() {
        let config = default_config();
        let text = "<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>more text";
        let pos = find_tool_call_end_position_kimi_k2(text, &config);
        assert_eq!(pos, Some(text.len() - "more text".len()));
        assert_eq!(&text[pos.unwrap()..], "more text");

        let text_no_end = "<|tool_calls_section_begin|><|tool_call_begin|>functions.test:0";
        let pos = find_tool_call_end_position_kimi_k2(text_no_end, &config);
        assert_eq!(pos, None, "should return None when section_end is missing");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.1 in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.yaml.
    #[test] // TOOLCALLING.batch.1
    fn test_parse_simple_tool_call() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("".to_string()));

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args["location"], "NYC");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.1, TOOLCALLING.batch.7.d in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.7.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.yaml.
    #[test] // TOOLCALLING.batch.1, TOOLCALLING.batch.7
    fn test_parse_multiple_args() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"San Francisco, CA","unit":"fahrenheit"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args["location"], "San Francisco, CA");
        assert_eq!(args["unit"], "fahrenheit");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.a in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml.
    #[test] // TOOLCALLING.batch.2
    fn test_parse_multiple_tool_calls() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_time:1<|tool_call_argument_begin|>{"timezone":"EST"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(calls[1].function.name, "get_time");
        assert_eq!(normal, Some("".to_string()));

        let args0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        let args1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
        assert_eq!(args0["location"], "NYC");
        assert_eq!(args1["timezone"], "EST");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.8.c in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.8.yaml.
    #[test] // TOOLCALLING.batch.8.c
    fn test_parse_keeps_prefix_drops_post_tool_text() {
        let config = default_config();
        let input = r#"I'll help you with that. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"Dallas"}<|tool_call_end|><|tool_calls_section_end|> Let me check."#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("I'll help you with that. ".to_string()));
    }

    #[test] // TOOLCALLING.batch.8.a
    fn test_parse_preserves_vllm_prefix_trailing_space() {
        let config = default_config();
        let input = r#"I'll check the weather. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"Dallas"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("I'll check the weather. ".to_string()));
    }

    #[test] // TOOLCALLING.batch.8.a
    fn test_parse_prefix_only_ignores_post_wrapper_whitespace() {
        let config = default_config();
        let input = r#"I'll check the weather. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"Dallas"}<|tool_call_end|><|tool_calls_section_end|>   "#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("I'll check the weather. ".to_string()));
    }

    #[test] // TOOLCALLING.batch.2.c
    fn test_parse_parallel_calls_with_surrounding_text() {
        let config = default_config();
        let input = r#"I will check both. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_time:1<|tool_call_argument_begin|>{"timezone":"EST"}<|tool_call_end|><|tool_calls_section_end|> Done."#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(calls[1].function.name, "get_time");
        assert_eq!(normal, Some("I will check both. ".to_string()));
    }

    #[test] // TOOLCALLING.batch.8.d
    fn test_parse_multiple_sections_drops_inter_wrapper_text() {
        let config = default_config();
        let input = r#"First check Dallas. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"Dallas"}<|tool_call_end|><|tool_calls_section_end|> Then check NYC. <|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:1<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 2);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(calls[1].function.name, "get_weather");
        assert_eq!(normal, Some("First check Dallas. ".to_string()));
    }

    #[test] // TOOLCALLING.batch.8.b
    fn test_parse_drops_post_tool_text_without_prefix() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"Dallas"}<|tool_call_end|><|tool_calls_section_end|> Let me check."#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("".to_string()));
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.6.a in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.6.yaml.
    #[test] // TOOLCALLING.batch.6
    fn test_parse_no_arg_call() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_current_time:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_current_time");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert!(args.as_object().unwrap().is_empty());
    }

    #[test] // TOOLCALLING.batch.3
    fn test_parse_no_tool_calls() {
        let config = default_config();
        let input = "This is just normal text without any tool calls.";

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 0);
        assert_eq!(normal, Some(input.to_string()));
    }

    #[test] // TOOLCALLING.fmt.1 — function-name conventions (`functions.X` vs bare `X`)
    fn test_parse_without_functions_prefix() {
        let config = default_config();
        // Some models may emit without the "functions." prefix
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.1 in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.yaml.
    #[test] // TOOLCALLING.batch.1 (with declared `ToolDefinition` tools provided)
    fn test_parse_with_tool_validation() {
        let config = default_config();
        let tools = vec![ToolDefinition {
            name: "get_weather".to_string(),
            parameters: Some(serde_json::json!({
                "type": "object",
                "properties": {
                    "location": {"type": "string"}
                }
            })),
            strict: None,
        }];

        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, Some(&tools)).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
    }

    // Recovery for truncated JSON args inside complete fences (e.g.
    // max_tokens fires inside `"location":"NYC` with no closing quote).
    // The arg-capture regex now accepts any payload between
    // `<|tool_call_argument_begin|>` and `<|tool_call_end|>`; downstream
    // `serde_json::from_str` falls back to raw-string arguments when the
    // payload doesn't parse, matching the behavior of the existing
    // `test_parse_invalid_json_falls_back_to_raw_string` case.
    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.b in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.4
    fn test_parse_truncated_json_inside_complete_fences_recovers() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        // Truncated JSON falls back to raw-string arguments.
        assert_eq!(calls[0].function.arguments, r#"{"location":"NYC"#);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.5.a in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.5.yaml.
    #[test] // TOOLCALLING.batch.5 (PR #8208)
    fn test_parse_malformed_no_section_end() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|>"#;

        // Missing section_end but individual tool call is complete (call_begin + args + call_end).
        // This happens when the model hits max_tokens before emitting section_end.
        // The parser should still extract the complete individual tool calls.
        let (calls, _normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(
            calls.len(),
            1,
            "Should parse complete tool calls even without section_end (max_tokens truncation)"
        );
        assert_eq!(calls[0].function.name, "get_weather");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.b, TOOLCALLING.batch.5.c in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.5.yaml.
    #[test] // TOOLCALLING.batch.4, TOOLCALLING.batch.5
    fn test_parse_truncated_mid_argument_no_section_end() {
        let config = default_config();
        // Model hit max_tokens mid-argument — no call_end, no section_end.
        // Truly incomplete tool call, nothing salvageable.
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NY"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(
            calls.len(),
            0,
            "Truly truncated call (no call_end) should return 0 tool calls"
        );
        // The section body is consumed by parse_section_block (which finds no
        // complete calls), so normal content is empty — the raw markers are not
        // re-emitted as user-visible text.
        assert_eq!(normal, Some("".to_string()));
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.a, TOOLCALLING.batch.5.a in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.5.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.5
    fn test_parse_multiple_calls_no_section_end() {
        let config = default_config();
        // Two complete individual tool calls, but model stopped before section_end.
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_time:1<|tool_call_argument_begin|>{"timezone":"EST"}<|tool_call_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(
            calls.len(),
            2,
            "Should parse both complete tool calls even without section_end"
        );
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(calls[1].function.name, "get_time");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.b, TOOLCALLING.batch.4.b, TOOLCALLING.batch.5.c in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.5.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.4, TOOLCALLING.batch.5
    fn test_parse_complete_plus_truncated_no_section_end() {
        let config = default_config();
        // First call is complete, second is truncated mid-argument.
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_time:1<|tool_call_argument_begin|>{"tz"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(
            calls.len(),
            1,
            "Should parse the one complete tool call, ignoring the truncated second"
        );
        assert_eq!(calls[0].function.name, "get_weather");
    }

    #[test] // TOOLCALLING.fmt.2 — whitespace tolerance
    fn test_parse_with_whitespace() {
        let config = default_config();
        let input = "<|tool_calls_section_begin|>\n<|tool_call_begin|> functions.search:0 <|tool_call_argument_begin|> {\"query\":\"rust programming\"} <|tool_call_end|>\n<|tool_calls_section_end|>";

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "search");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args["query"], "rust programming");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.7.d in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.7
    fn test_parse_complex_json_arguments() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.process_data:0<|tool_call_argument_begin|>{"items":[1,2,3],"config":{"nested":true}}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "process_data");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args["items"], serde_json::json!([1, 2, 3]));
        assert_eq!(args["config"]["nested"], true);
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.a, TOOLCALLING.batch.7.d in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.7
    fn test_parse_deeply_nested_json_multiple_calls() {
        let config = default_config();
        // Multiple tool calls with deeply nested JSON - stress test for regex backtracking
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.create_config:0<|tool_call_argument_begin|>{"database":{"primary":{"host":"db1.example.com","port":5432,"options":{"ssl":true,"pool":{"min":5,"max":20}}},"replica":{"host":"db2.example.com","port":5432}},"features":["auth","logging"]}<|tool_call_end|><|tool_call_begin|>functions.deploy:1<|tool_call_argument_begin|>{"env":"production","services":[{"name":"api","replicas":3,"config":{"memory":"2Gi","cpu":"1000m"}},{"name":"worker","replicas":2,"config":{"memory":"4Gi","cpu":"2000m"}}]}<|tool_call_end|><|tool_call_begin|>functions.notify:2<|tool_call_argument_begin|>{"channels":["slack","email"],"message":"Deployment started"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 3);

        assert_eq!(calls[0].function.name, "create_config");
        assert_eq!(calls[0].id, "functions.create_config:0");
        let args0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args0["database"]["primary"]["options"]["pool"]["max"], 20);

        assert_eq!(calls[1].function.name, "deploy");
        assert_eq!(calls[1].id, "functions.deploy:1");
        let args1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
        assert_eq!(args1["services"][0]["config"]["memory"], "2Gi");

        assert_eq!(calls[2].function.name, "notify");
        assert_eq!(calls[2].id, "functions.notify:2");
        let args2: serde_json::Value = serde_json::from_str(&calls[2].function.arguments).unwrap();
        assert_eq!(args2["channels"], serde_json::json!(["slack", "email"]));

        assert_eq!(normal, Some("".to_string()));
    }

    #[test] // helper, TOOLCALLING.fmt.3 — detection helper, singular section-token variant
    fn test_detect_singular_section_start() {
        let config = default_config();
        // Singular variant: <|tool_call_section_begin|> (without 's')
        assert!(detect_tool_call_start_kimi_k2(
            "<|tool_call_section_begin|>",
            &config
        ));
        // Partial match of singular variant
        assert!(detect_tool_call_start_kimi_k2(
            "text <|tool_call_section_b",
            &config
        ));
    }

    #[test] // TOOLCALLING.fmt.3 — singular section-token variant
    fn test_parse_with_singular_section_tokens() {
        let config = default_config();
        // Use singular form: tool_call_section_begin/end (without 's')
        let input = r#"<|tool_call_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_section_end|>"#;

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(normal, Some("".to_string()));
    }

    #[test] // helper, TOOLCALLING.fmt.3 — detection helper, singular section-token variant
    fn test_find_end_position_singular_variant() {
        let config = default_config();
        // Singular variant end token
        let text = "<|tool_call_section_begin|><|tool_call_begin|>functions.test:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_call_section_end|>more text";
        let pos = find_tool_call_end_position_kimi_k2(text, &config);
        assert_eq!(&text[pos.unwrap()..], "more text");
    }

    // --- Tests inspired by vllm/sglang coverage gaps ---

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.b in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.4
    fn test_parse_invalid_json_falls_back_to_raw_string() {
        // vllm: test_extract_tool_calls_invalid_json
        // Invalid JSON in arguments should fall back to raw string, not panic
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{invalid json here}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "get_weather");
        // Arguments should be preserved as raw string since JSON parsing failed
        assert_eq!(calls[0].function.arguments, "{invalid json here}");
    }

    #[test] // TOOLCALLING.fmt.1 — function-name conventions (ID regex validation)
    fn test_parse_invalid_function_id_rejected_by_regex() {
        // vllm: test_extract_tool_calls_invalid_funcall
        // sglang: test_invalid_tool_call
        // function_id regex requires [\w.\-]+:\d+ — IDs without :digit are rejected
        let config = default_config();

        // No colon+digit suffix at all
        let input1 = r#"<|tool_calls_section_begin|><|tool_call_begin|>just_a_name<|tool_call_argument_begin|>{"key":"val"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input1, &config, None).unwrap();
        assert_eq!(calls.len(), 0, "ID without :digit should be rejected");

        // Colon but non-digit suffix
        let input2 = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:abc<|tool_call_argument_begin|>{"key":"val"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input2, &config, None).unwrap();
        assert_eq!(calls.len(), 0, "ID with :non-digit should be rejected");

        // Multiple colons (garbage)
        let input3 = r#"<|tool_calls_section_begin|><|tool_call_begin|>:::0<|tool_call_argument_begin|>{"key":"val"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input3, &config, None).unwrap();
        assert_eq!(calls.len(), 0, "Garbage ID should be rejected");

        // Valid call mixed with invalid — only valid should be extracted
        let input4 = r#"<|tool_calls_section_begin|><|tool_call_begin|>no_colon<|tool_call_argument_begin|>{"a":"b"}<|tool_call_end|><|tool_call_begin|>functions.valid:0<|tool_call_argument_begin|>{"x":"y"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input4, &config, None).unwrap();
        assert_eq!(calls.len(), 1, "Only valid call should be extracted");
        assert_eq!(calls[0].function.name, "valid");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.7.b in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.7 — special characters in arg values
    fn test_parse_angle_brackets_in_json_arguments() {
        // vllm: angle_brackets_in_json
        // JSON values containing <tag> constructs should not confuse the parser,
        // since Kimi markers use <| prefix which is distinct from bare <
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.render_html:0<|tool_call_argument_begin|>{"template":"<div class=\"main\"><h1>Title</h1><p>Content</p></div>","format":"html"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "render_html");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert!(args["template"].as_str().unwrap().contains("<div"));
        assert!(args["template"].as_str().unwrap().contains("</div>"));
        assert_eq!(args["format"], "html");
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.b in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml.
    #[test] // TOOLCALLING.batch.2 — parallel calls, zero-spacing edge case
    fn test_parse_three_concatenated_calls_no_spacing() {
        // vllm: concatenated_tool_calls_bug_fix, three_concatenated_tool_calls
        // Three tool calls concatenated with zero whitespace between them
        let config = default_config();
        let input = "<|tool_calls_section_begin|>\
            <|tool_call_begin|>functions.search:0<|tool_call_argument_begin|>{\"q\":\"rust\"}<|tool_call_end|>\
            <|tool_call_begin|>functions.search:1<|tool_call_argument_begin|>{\"q\":\"python\"}<|tool_call_end|>\
            <|tool_call_begin|>functions.search:2<|tool_call_argument_begin|>{\"q\":\"go\"}<|tool_call_end|>\
            <|tool_calls_section_end|>";

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 3);
        assert_eq!(calls[0].function.name, "search");
        assert_eq!(calls[0].id, "functions.search:0");
        assert_eq!(calls[1].function.name, "search");
        assert_eq!(calls[1].id, "functions.search:1");
        assert_eq!(calls[2].function.name, "search");
        assert_eq!(calls[2].id, "functions.search:2");

        let a0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        let a1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
        let a2: serde_json::Value = serde_json::from_str(&calls[2].function.arguments).unwrap();
        assert_eq!(a0["q"], "rust");
        assert_eq!(a1["q"], "python");
        assert_eq!(a2["q"], "go");
        assert_eq!(normal, Some("".to_string()));
    }

    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.7.b in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.7.yaml.
    #[test] // TOOLCALLING.batch.7 — newlines in arg values
    fn test_parse_newlines_in_json_arguments() {
        // vllm: newlines_in_json
        // Multi-line formatted JSON arguments (model may emit pretty-printed JSON)
        let config = default_config();
        let input = "<|tool_calls_section_begin|><|tool_call_begin|>functions.create_user:0<|tool_call_argument_begin|>{\n  \"name\": \"John Doe\",\n  \"address\": {\n    \"street\": \"123 Main St\",\n    \"city\": \"Springfield\"\n  },\n  \"tags\": [\n    \"admin\",\n    \"active\"\n  ]\n}<|tool_call_end|><|tool_calls_section_end|>";

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, "create_user");

        let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        assert_eq!(args["name"], "John Doe");
        assert_eq!(args["address"]["city"], "Springfield");
        assert_eq!(args["tags"], serde_json::json!(["admin", "active"]));
    }

    #[test] // TOOLCALLING.fmt.4 — empty wrapper (start+end with no calls between)
    fn test_parse_empty_tool_section() {
        // vllm: test_empty_tool_section
        // Section begin immediately followed by section end, no tool calls inside
        let config = default_config();
        let input = "Here is my answer. <|tool_calls_section_begin|><|tool_calls_section_end|> And more text.";

        let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 0, "Empty section should produce no tool calls");
        assert_eq!(
            normal,
            Some("Here is my answer.  And more text.".to_string()),
            "Text around empty section should be preserved"
        );
    }

    #[rstest] // TOOLCALLING.fmt.1 — function-name conventions (hyphens, double-underscores)
    #[case(
        r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.list-tasklists:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"#,
        "list-tasklists",
        "functions.list-tasklists:0"
    )]
    #[case(
        r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.mcp__portal__search-documents:3<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"#,
        "mcp__portal__search-documents",
        "functions.mcp__portal__search-documents:3"
    )]
    #[case(
        r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.gtasks_list-tasklists:0<|tool_call_argument_begin|>{}<|tool_call_end|><|tool_calls_section_end|>"#,
        "gtasks_list-tasklists",
        "functions.gtasks_list-tasklists:0"
    )]
    fn test_parse_names_with_hyphens(#[case] input: &str, #[case] name: &str, #[case] id: &str) {
        let config = default_config();
        let (calls, _normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 1);
        assert_eq!(calls[0].function.name, name);
        assert_eq!(calls[0].id, id);
    }

    /// `TOOLCALLING.batch.4` — call missing its `<|tool_call_end|>` while the outer
    /// `<|tool_calls_section_end|>` IS present. Per-call delimiter is the
    /// regex anchor; without it the call cannot be matched even though
    /// the block fences are intact. Pin the silent-drop.
    ///
    /// TODO(TOOLCALLING.batch.4) — BUG, NEEDS FIX: a real call is silently dropped when
    /// `<|tool_call_end|>` is missing and section fences are complete. Fix:
    /// anchor on the next `<|tool_call_begin|>` or `<|tool_calls_section_end|>`
    /// to terminate the args region. Flip this test once fixed.
    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.4.d in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.4
    fn test_parse_missing_call_end_inside_complete_section_silent_drop() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(
            calls.len(),
            0,
            "Missing per-call <|tool_call_end|> drops the call even when \
             section fences are complete"
        );
    }

    /// `TOOLCALLING.batch.4` — middle-call truncation. Three calls A, B, C inside a
    /// complete section; B is missing its `<|tool_call_end|>`. Does B's
    /// body bleed into C, or are both lost? Pin whichever today does.
    ///
    /// TODO(TOOLCALLING.batch.4) — BUG, NEEDS FIX: B's args swallow all of C's raw
    /// markup, and C is dropped — caller gets garbage args for B and
    /// never sees C. Fix: same anchor on `<|tool_call_begin|>` as the
    /// silent-drop case above. Flip this test once fixed.
    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.2.b, TOOLCALLING.batch.4.d in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.2.yaml, tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.4.yaml.
    #[test] // TOOLCALLING.batch.2, TOOLCALLING.batch.4
    fn test_parse_middle_call_missing_end_corrupts_next() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.a:0<|tool_call_argument_begin|>{"x":"1"}<|tool_call_end|><|tool_call_begin|>functions.b:1<|tool_call_argument_begin|>{"y":"2"}<|tool_call_begin|>functions.c:2<|tool_call_argument_begin|>{"z":"3"}<|tool_call_end|><|tool_calls_section_end|>"#;

        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        // Today: A parses cleanly. B's args regex is non-greedy `\{.*?\}` and
        // tries to match `{...}` followed by `<|tool_call_end|>`; since B has
        // no end token, the regex extends until it finds C's closing `}` AND
        // C's `<|tool_call_end|>`. Result: B keeps its name but its args
        // string contains all of C's raw markup; C is consumed and dropped.
        assert_eq!(calls.len(), 2, "A and corrupted-B; C is consumed by B");
        assert_eq!(calls[0].function.name, "a");
        assert_eq!(calls[0].function.arguments, r#"{"x":"1"}"#);
        assert_eq!(calls[1].function.name, "b");
        assert!(
            calls[1].function.arguments.contains("functions.c:2"),
            "BUG: B's args swallowed C's markup verbatim; got {}",
            calls[1].function.arguments
        );
    }

    /// Parser-level invariant: the kimi_k2 parser does NOT filter by
    /// `tool_choice` — it returns every well-formed call it sees, and the
    /// jail / response builder above this layer is responsible for filtering
    /// per `tool_choice=named`/`required`/`none`. This test exists to
    /// catch accidental in-parser filtering. Real FRONTEND.tool_choice coverage lives at
    /// the integration layer (`lib/llm/tests/tool_choice.rs`).
    #[test]
    fn test_parser_does_not_filter_by_tool_choice() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_time:1<|tool_call_argument_begin|>{"timezone":"EST"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        // Parser returns BOTH calls; tool_choice="get_weather" filtering would
        // happen above this layer. Pin the parser-level invariant.
        assert_eq!(calls.len(), 2);
    }

    /// Parser-level invariant: the kimi_k2 parser is byte-stable — it doesn't
    /// see `finish_reason` and produces the same output for any upstream
    /// stream-end reason. Real PIPELINE.finish_reason coverage (stop / tool_calls / length
    /// mapping) lives in `lib/llm/tests/test_streaming_tool_parsers.rs` and
    /// belongs in cross-parser finish_reason mapping work-item (tracked separately).
    #[test]
    fn test_parser_output_independent_of_upstream_finish() {
        let config = default_config();
        // Same payload, two "logical" finish_reasons (stop vs length truncation):
        // the parser sees only the bytes, so behavior must be identical.
        let stop_input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls_stop, _) = try_tool_call_parse_kimi_k2(stop_input, &config, None).unwrap();
        assert_eq!(calls_stop.len(), 1);
    }

    /// `TOOLCALLING.batch.9` — empty / null content variants. Empty section already
    /// covered by `test_parse_empty_tool_section`; this pins the
    /// truly-empty (zero bytes) and null-ish ("\n", whitespace-only) inputs.
    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.9 in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.yaml.
    #[test] // TOOLCALLING.batch.9
    fn test_parse_empty_and_whitespace_inputs() {
        let config = default_config();
        for input in &["", " ", "\n", "\t\n  \t"] {
            let (calls, normal) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
            assert!(
                calls.is_empty(),
                "Empty/whitespace input must yield no calls (input={:?})",
                input
            );
            // Whitespace is trimmed; normal_text is the empty string.
            assert_eq!(
                normal.as_deref(),
                Some(""),
                "Empty/whitespace input collapses to empty normal_text"
            );
        }
    }

    /// `TOOLCALLING.batch.10` — duplicate calls (same function name twice in one section).
    /// Universal gap noted in the test taxonomy; first parser to land coverage.
    // DEPRECATED(parser-fixture-duplicate): Duplicate of YAML fixture coverage: TOOLCALLING.batch.10 in tests/parity/toolcalling/fixtures/kimi_k2/TOOLCALLING.batch.yaml.
    #[test] // TOOLCALLING.batch.10
    fn test_parse_duplicate_calls_same_name() {
        let config = default_config();
        let input = r#"<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{"location":"NYC"}<|tool_call_end|><|tool_call_begin|>functions.get_weather:1<|tool_call_argument_begin|>{"location":"LA"}<|tool_call_end|><|tool_calls_section_end|>"#;
        let (calls, _) = try_tool_call_parse_kimi_k2(input, &config, None).unwrap();
        assert_eq!(calls.len(), 2, "Both duplicate-name calls must be returned");
        assert_eq!(calls[0].function.name, "get_weather");
        assert_eq!(calls[1].function.name, "get_weather");
        assert_ne!(
            calls[0].id, calls[1].id,
            "Duplicate calls must have distinct ids"
        );
        let args0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
        let args1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
        assert_eq!(args0["location"], "NYC");
        assert_eq!(args1["location"], "LA");
    }
}
