//! String and text manipulation utilities for fixture analysis.
//!
//! The [`replace_identifier`] function is shared between the type-alias expander
//! in `analyzer.rs` and the `adapt_type_for_consumer` helper in `code_action.rs`.

/// Format a docstring by removing leading/trailing empty lines and dedenting.
pub(crate) fn format_docstring(docstring: String) -> String {
    let lines: Vec<&str> = docstring.lines().collect();

    if lines.is_empty() {
        return String::new();
    }

    // Find first and last non-empty lines
    let mut start = 0;
    let mut end = lines.len();

    while start < lines.len() && lines[start].trim().is_empty() {
        start += 1;
    }

    while end > start && lines[end - 1].trim().is_empty() {
        end -= 1;
    }

    if start >= end {
        return String::new();
    }

    let lines = &lines[start..end];

    // Find minimum indentation (excluding first line if it's not empty)
    let mut min_indent = usize::MAX;
    for (i, line) in lines.iter().enumerate() {
        if i == 0 && !line.trim().is_empty() {
            continue; // First line indentation doesn't count
        }

        if !line.trim().is_empty() {
            let indent = line.len() - line.trim_start().len();
            min_indent = min_indent.min(indent);
        }
    }

    if min_indent == usize::MAX {
        min_indent = 0;
    }

    // Dedent all lines
    let mut result = Vec::new();
    for (i, line) in lines.iter().enumerate() {
        if i == 0 {
            result.push(line.trim().to_string());
        } else if line.trim().is_empty() {
            result.push(String::new());
        } else {
            let dedented = if line.len() > min_indent {
                &line[min_indent..]
            } else {
                line.trim_start()
            };
            result.push(dedented.to_string());
        }
    }

    result.join("\n")
}

/// Extract the word at a given character position in a line.
/// Returns None if the position is not within a word.
pub(crate) fn extract_word_at_position(line: &str, character: usize) -> Option<String> {
    let char_indices: Vec<(usize, char)> = line.char_indices().collect();

    if character >= char_indices.len() {
        return None;
    }

    let (_byte_pos, c) = char_indices[character];

    if !c.is_alphanumeric() && c != '_' {
        return None;
    }

    // Find start of word
    let mut start_idx = character;
    while start_idx > 0 {
        let (_, prev_c) = char_indices[start_idx - 1];
        if !prev_c.is_alphanumeric() && prev_c != '_' {
            break;
        }
        start_idx -= 1;
    }

    // Find end of word
    let mut end_idx = character + 1;
    while end_idx < char_indices.len() {
        let (_, curr_c) = char_indices[end_idx];
        if !curr_c.is_alphanumeric() && curr_c != '_' {
            break;
        }
        end_idx += 1;
    }

    let start_byte = char_indices[start_idx].0;
    let end_byte = if end_idx < char_indices.len() {
        char_indices[end_idx].0
    } else {
        line.len()
    };

    Some(line[start_byte..end_byte].to_string())
}

/// Find the character position of a function name in a line of code.
/// Returns (start_char, end_char) positions.
pub(crate) fn find_function_name_position(
    content: &str,
    line: usize,
    func_name: &str,
) -> (usize, usize) {
    if let Some(line_content) = content.lines().nth(line.saturating_sub(1)) {
        // Look for "def function_name" pattern
        if let Some(def_pos) = line_content.find("def ") {
            let after_def = &line_content[def_pos + 4..];
            if let Some(name_pos) = after_def.find(func_name) {
                let start_char = def_pos + 4 + name_pos;
                let end_char = start_char + func_name.len();
                return (start_char, end_char);
            }
        }
        // Fallback: search for function name anywhere in line
        if let Some(pos) = line_content.find(func_name) {
            return (pos, pos + func_name.len());
        }
    }
    // Default fallback
    (0, func_name.len())
}

/// Check if a parameter at the given position already has a type annotation.
///
/// Looks at the text after the parameter name (at `end_char`) to see if there's
/// a `:` before the next `,`, `)`, or `=` (default value). This handles:
/// - `def test(param)` -> no annotation
/// - `def test(param: Type)` -> has annotation
/// - `def test(param: Type = default)` -> has annotation
/// - `def test(param = default)` -> no annotation
///
/// # Arguments
/// * `lines` - The lines of the file content
/// * `line` - The 1-based line number
/// * `end_char` - The 0-based character position where the parameter name ends
///
/// Note: This function is used by the inlay_hint provider in main.rs (binary crate).
/// The #[allow(dead_code)] is needed because the lib crate doesn't use it directly.
#[allow(dead_code)]
pub fn parameter_has_annotation(lines: &[&str], line: usize, end_char: usize) -> bool {
    // Convert 1-based line to 0-based index
    let line_idx = line.saturating_sub(1);

    let Some(line_text) = lines.get(line_idx) else {
        return false;
    };

    // Get the text after the parameter name
    let after_param = if end_char < line_text.len() {
        &line_text[end_char..]
    } else {
        return false;
    };

    // Look for `:` before `,`, `)`, or `=`
    // Skip any whitespace first
    let trimmed = after_param.trim_start();

    // If the next non-whitespace character is `:`, there's an annotation
    trimmed.starts_with(':')
}

/// Replace all standalone occurrences of `old` with `new` in `text`,
/// respecting Python type-annotation identifier boundaries (alphanumeric,
/// underscore, dot).
///
/// A match is "standalone" when it is **not** preceded by `[a-zA-Z0-9_.]` and
/// **not** followed by `[a-zA-Z0-9_]`, so:
///
/// - `"Path"` in `"Optional[Path]"` → replaced ✓
/// - `"Path"` in `"PathLike"` → **not** replaced ✓  (right-boundary guard)
/// - `"Path"` in `"pathlib.Path"` → **not** replaced ✓  (left-boundary dot guard)
///
/// # Examples
///
/// ```text
/// replace_identifier("Optional[Path]", "Path", "pathlib.Path")
///   => "Optional[pathlib.Path]"
///
/// replace_identifier("PathLike", "Path", "pathlib.Path")
///   => "PathLike"   // right-boundary guard prevents partial match
/// ```
pub(crate) fn replace_identifier(text: &str, old: &str, new: &str) -> String {
    let bytes = text.as_bytes();
    let old_bytes = old.as_bytes();
    let len = bytes.len();
    let old_len = old_bytes.len();
    let mut result = String::with_capacity(len + new.len());
    let mut i = 0;

    while i < len {
        if i + old_len <= len && &bytes[i..i + old_len] == old_bytes {
            let left_ok = i == 0
                || !(bytes[i - 1].is_ascii_alphanumeric()
                    || bytes[i - 1] == b'_'
                    || bytes[i - 1] == b'.');
            let right_ok = i + old_len >= len
                || !(bytes[i + old_len].is_ascii_alphanumeric() || bytes[i + old_len] == b'_');
            if left_ok && right_ok {
                result.push_str(new);
                i += old_len;
                continue;
            }
        }
        // Advance by the full UTF-8 character to avoid splitting multi-byte
        // sequences (e.g. non-ASCII identifiers or string-literal content).
        let ch_len = text[i..].chars().next().map_or(1, |c| c.len_utf8());
        result.push_str(&text[i..i + ch_len]);
        i += ch_len;
    }

    result
}

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

    #[test]
    fn test_format_docstring_simple() {
        let input = "Simple docstring".to_string();
        assert_eq!(format_docstring(input), "Simple docstring");
    }

    #[test]
    fn test_format_docstring_with_whitespace() {
        let input = "\n\n  Line 1\n  Line 2\n\n".to_string();
        assert_eq!(format_docstring(input), "Line 1\nLine 2");
    }

    #[test]
    fn test_format_docstring_indented() {
        let input = "First line\n    Indented line\n    Another indented".to_string();
        let result = format_docstring(input);
        assert_eq!(result, "First line\nIndented line\nAnother indented");
    }

    #[test]
    fn test_extract_word_at_position() {
        let line = "def my_function(arg1, arg2):";
        assert_eq!(extract_word_at_position(line, 0), Some("def".to_string()));
        assert_eq!(
            extract_word_at_position(line, 4),
            Some("my_function".to_string())
        );
        assert_eq!(extract_word_at_position(line, 16), Some("arg1".to_string()));
        assert_eq!(extract_word_at_position(line, 20), None); // comma
    }

    #[test]
    fn test_find_function_name_position() {
        let content = "def my_function():\n    pass";
        let (start, end) = find_function_name_position(content, 1, "my_function");
        assert_eq!(start, 4);
        assert_eq!(end, 15);
    }

    #[test]
    fn test_parameter_has_annotation_no_annotation() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture):"];
        // "my_fixture" ends at position 27 (after the 'e')
        assert!(!parameter_has_annotation(&lines, 1, 27));
    }

    #[test]
    fn test_parameter_has_annotation_with_annotation() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture: Database):"];
        // "my_fixture" ends at position 27
        assert!(parameter_has_annotation(&lines, 1, 27));
    }

    #[test]
    fn test_parameter_has_annotation_with_space_before_colon() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture : Database):"];
        // "my_fixture" ends at position 27, but there's a space before the colon
        assert!(parameter_has_annotation(&lines, 1, 27));
    }

    #[test]
    fn test_parameter_has_annotation_with_default_no_annotation() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture = None):"];
        // "my_fixture" ends at position 27
        assert!(!parameter_has_annotation(&lines, 1, 27));
    }

    #[test]
    fn test_parameter_has_annotation_with_default_and_annotation() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture: Database = None):"];
        // "my_fixture" ends at position 27
        assert!(parameter_has_annotation(&lines, 1, 27));
    }

    #[test]
    fn test_parameter_has_annotation_multiple_params_first() {
        let lines: Vec<&str> = vec!["def test_example(fixture_a: TypeA, fixture_b):"];
        // "fixture_a" ends at position 26
        assert!(parameter_has_annotation(&lines, 1, 26));
    }

    #[test]
    fn test_parameter_has_annotation_multiple_params_second() {
        let lines: Vec<&str> = vec!["def test_example(fixture_a: TypeA, fixture_b):"];
        // "fixture_b" starts at position 35, ends at 35 + 9 = 44
        assert!(!parameter_has_annotation(&lines, 1, 44));
    }

    #[test]
    fn test_parameter_has_annotation_multiline() {
        let lines: Vec<&str> = vec![
            "def test_example(",
            "    fixture_a: TypeA,",
            "    fixture_b,",
            "):",
        ];
        // Line 2 (1-indexed), "fixture_a" ends at position 13
        assert!(parameter_has_annotation(&lines, 2, 13));
        // Line 3 (1-indexed), "fixture_b" ends at position 13
        assert!(!parameter_has_annotation(&lines, 3, 13));
    }

    #[test]
    fn test_parameter_has_annotation_out_of_bounds() {
        let lines: Vec<&str> = vec!["def test_example(my_fixture):"];
        // Line out of bounds
        assert!(!parameter_has_annotation(&lines, 10, 27));
        // Character out of bounds
        assert!(!parameter_has_annotation(&lines, 1, 100));
    }

    #[test]
    fn test_parameter_has_annotation_empty_lines() {
        let lines: Vec<&str> = vec![];
        assert!(!parameter_has_annotation(&lines, 1, 0));
    }

    // ── replace_identifier tests ─────────────────────────────────────────────

    #[test]
    fn test_replace_identifier_simple() {
        assert_eq!(
            replace_identifier("Path", "Path", "pathlib.Path"),
            "pathlib.Path"
        );
    }

    #[test]
    fn test_replace_identifier_in_generic() {
        assert_eq!(
            replace_identifier("Optional[Path]", "Path", "pathlib.Path"),
            "Optional[pathlib.Path]"
        );
    }

    #[test]
    fn test_replace_identifier_partial_no_match() {
        // "PathLike" must not be touched when replacing "Path".
        assert_eq!(
            replace_identifier("PathLike", "Path", "pathlib.Path"),
            "PathLike"
        );
    }

    #[test]
    fn test_replace_identifier_as_word_suffix() {
        // "MyPath" must not be touched when replacing "Path".
        assert_eq!(
            replace_identifier("MyPath", "Path", "pathlib.Path"),
            "MyPath"
        );
    }

    #[test]
    fn test_replace_identifier_repeated() {
        assert_eq!(
            replace_identifier("tuple[Path, Path]", "Path", "pathlib.Path"),
            "tuple[pathlib.Path, pathlib.Path]"
        );
    }

    #[test]
    fn test_replace_identifier_union() {
        assert_eq!(
            replace_identifier("Path | None", "Path", "pathlib.Path"),
            "pathlib.Path | None"
        );
    }

    #[test]
    fn test_replace_identifier_does_not_match_after_dot() {
        // "pathlib.Path" — the "Path" is preceded by a dot, so it must not be
        // replaced a second time (prevents infinite expansion loops).
        assert_eq!(
            replace_identifier("pathlib.Path", "Path", "xx.Path"),
            "pathlib.Path"
        );
    }
}
