use tombi_syntax::SyntaxKind::{
    BARE_KEY, BASIC_STRING, KEY_VALUE, KEY_VALUE_GROUP, LITERAL_STRING, MULTI_LINE_BASIC_STRING,
    MULTI_LINE_LITERAL_STRING,
};

use crate::string::{
    load_text, normalize_key_quotes, strip_quotes, update_content, update_content_wrapped, wrap_all_long_strings,
};

fn parse(source: &str) -> tombi_syntax::SyntaxNode {
    tombi_parser::parse(source).syntax_node().clone_for_update()
}

fn is_string_kind(kind: tombi_syntax::SyntaxKind) -> bool {
    matches!(
        kind,
        BASIC_STRING | LITERAL_STRING | MULTI_LINE_BASIC_STRING | MULTI_LINE_LITERAL_STRING
    )
}

fn load_text_helper(input: &str, kind: tombi_syntax::SyntaxKind) -> String {
    load_text(input, kind)
}

fn for_each_string<F>(root: &tombi_syntax::SyntaxNode, mut f: F)
where
    F: FnMut(&tombi_syntax::SyntaxNode),
{
    for entry in root.children_with_tokens() {
        let kind = entry.kind();
        if kind == KEY_VALUE {
            for child in entry.as_node().unwrap().children_with_tokens() {
                if is_string_kind(child.kind()) {
                    f(child.as_node().unwrap());
                }
            }
        } else if kind == KEY_VALUE_GROUP {
            for kv in entry.as_node().unwrap().children_with_tokens() {
                if kv.kind() == KEY_VALUE {
                    for child in kv.as_node().unwrap().children_with_tokens() {
                        if is_string_kind(child.kind()) {
                            f(child.as_node().unwrap());
                        }
                    }
                }
            }
        }
    }
}

fn apply_update_content<F>(toml: &str, transform: F) -> String
where
    F: Fn(&str) -> String,
{
    let root_ast = parse(toml);
    for_each_string(&root_ast, |node| update_content(node, &transform));
    root_ast.to_string()
}

fn apply_update_content_wrapped<F>(toml: &str, transform: F, column_width: usize, indent: &str) -> String
where
    F: Fn(&str) -> String,
{
    let root_ast = parse(toml);
    for_each_string(&root_ast, |node| {
        update_content_wrapped(node, &transform, column_width, indent)
    });
    root_ast.to_string()
}

fn literal_string_escape_helper(input: &str) -> String {
    apply_update_content(&format!("key = {input}"), |s| s.to_string())
}

#[test]
fn test_load_text_case_basic_string() {
    let result = load_text_helper("\"hello\"", BASIC_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_load_text_case_string_with_escaped_quote() {
    let result = load_text_helper("\"hello \\\"world\\\"\"", BASIC_STRING);
    insta::assert_snapshot!(result, @r#"hello "world""#);
}

#[test]
fn test_load_text_case_string_literal() {
    let result = load_text_helper("'hello'", LITERAL_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_load_text_case_multi_line_string() {
    let result = load_text_helper("\"\"\"hello\"\"\"", MULTI_LINE_BASIC_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_load_text_case_multi_line_literal() {
    let result = load_text_helper("'''hello'''", MULTI_LINE_LITERAL_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_load_text_case_ident() {
    let result = load_text_helper("name", BARE_KEY);
    insta::assert_snapshot!(result, @"name");
}

#[test]
fn test_load_text_empty_string() {
    let result = load_text("\"\"", BASIC_STRING);
    assert_eq!(result, "");
}

#[test]
fn test_load_text_escape_sequences() {
    let result = load_text(r#""hello\nworld""#, BASIC_STRING);
    assert_eq!(result, "hello\nworld");

    let result = load_text(r#""hello\tworld""#, BASIC_STRING);
    assert_eq!(result, "hello\tworld");

    let result = load_text(r#""hello\\world""#, BASIC_STRING);
    assert_eq!(result, "hello\\world");

    let result = load_text(r#""quote:\"text\"""#, BASIC_STRING);
    assert_eq!(result, r#"quote:"text""#);
}

#[test]
fn test_update_content_basic() {
    let result = apply_update_content(r#"name = "foo""#, |s| s.to_uppercase());
    assert!(result.contains("FOO"));
}

#[test]
fn test_update_content_no_change() {
    let result = apply_update_content(r#"name = "foo""#, |s| s.to_string());
    assert!(result.contains("foo"));
}

#[test]
fn test_update_content_string_literal_normalized() {
    let result = apply_update_content("name = 'foo'", |s| s.to_string());
    assert!(result.contains("\"foo\""), "Got: {}", result);
}

#[test]
fn test_update_content_multi_line_string() {
    let result = apply_update_content("desc = \"\"\"multi\nline\"\"\"", |s| s.replace('\n', " "));
    assert!(result.contains("\"multi line\""));
}

#[test]
fn test_issue_22_backslash_uses_basic_string() {
    let result = apply_update_content(r#"regex = 'MPL-2\.0'"#, |s| s.to_string());
    assert!(result.contains(r#""MPL-2\\.0""#), "Got: {}", result);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_b() {
    let result = literal_string_escape_helper(r"'\b'");
    insta::assert_snapshot!(result, @r#"key = "\\b""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_t() {
    let result = literal_string_escape_helper(r"'\t'");
    insta::assert_snapshot!(result, @r#"key = "\\t""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_n() {
    let result = literal_string_escape_helper(r"'\n'");
    insta::assert_snapshot!(result, @r#"key = "\\n""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_f() {
    let result = literal_string_escape_helper(r"'\f'");
    insta::assert_snapshot!(result, @r#"key = "\\f""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_r() {
    let result = literal_string_escape_helper(r"'\r'");
    insta::assert_snapshot!(result, @r#"key = "\\r""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_backslash() {
    let result = literal_string_escape_helper(r"'\\'");
    insta::assert_snapshot!(result, @r#"key = "\\\\""#);
}

#[test]
fn test_literal_string_escape_handling_case_unicode_4() {
    let result = literal_string_escape_helper(r"'\u0041'");
    insta::assert_snapshot!(result, @r#"key = "\\u0041""#);
}

#[test]
fn test_literal_string_escape_handling_case_unicode_8() {
    let result = literal_string_escape_helper(r"'\U00000041'");
    insta::assert_snapshot!(result, @r#"key = "\\U00000041""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_dot() {
    let result = literal_string_escape_helper(r"'\.'");
    insta::assert_snapshot!(result, @r#"key = "\\.""#);
}

#[test]
fn test_literal_string_escape_handling_case_backslash_s() {
    let result = literal_string_escape_helper(r"'\s'");
    insta::assert_snapshot!(result, @r#"key = "\\s""#);
}

#[test]
fn test_literal_string_escape_handling_case_unicode_short() {
    let result = literal_string_escape_helper(r"'\u04'");
    insta::assert_snapshot!(result, @r#"key = "\\u04""#);
}

#[test]
fn test_literal_string_escape_handling_case_unicode_8_short() {
    let result = literal_string_escape_helper(r"'\U0000004'");
    insta::assert_snapshot!(result, @r#"key = "\\U0000004""#);
}

#[test]
fn test_literal_string_escape_handling_case_no_backslash() {
    let result = literal_string_escape_helper(r"'hello'");
    insta::assert_snapshot!(result, @r#"key = "hello""#);
}

#[test]
fn test_issue_36_line_continuation() {
    let result = apply_update_content("desc = \"\"\"\\\n    hello\\\n\"\"\"", |s| s.to_string());
    assert!(result.contains("\"hello\""), "Got: {}", result);
}

#[test]
fn test_line_continuation_with_crlf() {
    let result = apply_update_content("desc = \"\"\"\\\r\n    hello\"\"\"", |s| s.to_string());
    assert!(result.contains("\"hello\""), "Got: {}", result);
}

#[test]
fn test_wrap_long_string_is_idempotent() {
    use crate::string::wrap_all_long_strings;

    let original_text = "This is a long description string that needs to exceed the default column width of one hundred and twenty characters to trigger wrapping.";
    let toml = format!("[project]\ndescription = \"{}\"", original_text);

    let root_ast = parse(&toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let after_first = root_ast.to_string();

    let root_ast2 = parse(&after_first);
    wrap_all_long_strings(&root_ast2, 120, "  ", &[]);
    let after_second = root_ast2.to_string();

    let root_ast3 = parse(&after_second);
    wrap_all_long_strings(&root_ast3, 120, "  ", &[]);
    let after_third = root_ast3.to_string();

    assert_eq!(
        after_first, after_second,
        "wrap_all_long_strings should be idempotent (first->second)"
    );
    assert_eq!(
        after_second, after_third,
        "wrap_all_long_strings should be idempotent (second->third)"
    );
}

#[test]
fn test_multiline_with_regular_escapes() {
    let result = apply_update_content("desc = \"\"\"hello\\nworld\"\"\"", |s| s.to_string());
    assert!(result.contains("\"hello\\nworld\""), "Got: {}", result);
}

#[test]
fn test_multiline_with_tabs_after_continuation() {
    let result = apply_update_content("desc = \"\"\"\\\n\t\thello\"\"\"", |s| s.to_string());
    assert!(result.contains("\"hello\""), "Got: {}", result);
}

#[test]
fn test_basic_string_stays_basic() {
    let result = apply_update_content(r#"name = "hello""#, |s| s.to_string());
    assert!(result.contains("\"hello\""), "Got: {}", result);
}

#[test]
fn test_literal_with_single_quote_uses_basic_string() {
    let result = apply_update_content(r#"name = "it's""#, |s| s.to_string());
    assert!(result.contains("\"it's\""), "Got: {}", result);
}

#[test]
fn test_basic_string_with_backslash_stays_basic() {
    let result = apply_update_content(r#"regex = "MPL-2\\.0""#, |s| s.to_string());
    assert!(result.contains(r#""MPL-2\\.0""#), "Got: {}", result);
}

#[test]
fn test_issue_150_prefer_double_quotes() {
    let result = apply_update_content("name = 'simple-string'", |s| s.to_string());
    assert!(
        result.contains("\"simple-string\""),
        "Expected double-quoted string, got: {}",
        result
    );
}

#[test]
fn test_issue_150_backslash_uses_basic_string() {
    let result = apply_update_content(r#"regex = 'path\\to\\file'"#, |s| s.to_string());
    assert!(
        result.contains(r#""path\\\\to\\\\file""#),
        "Expected basic string (prefer \"\"), got: {}",
        result
    );
}

#[test]
fn test_string_with_both_quotes_uses_basic_with_escaping() {
    let result = apply_update_content(r#"msg = "it's a \"test\"""#, |s| s.to_string());
    assert!(
        result.contains(r#""it's a \"test\"""#),
        "Expected basic string with escaped quotes (can't use literal due to '), got: {}",
        result
    );
}

#[test]
fn test_string_with_double_quote_uses_literal() {
    let result = apply_update_content(r#"msg = "say \"hello\"""#, |s| s.to_string());
    assert!(
        result.contains(r#"'say "hello"'"#),
        "Expected literal string (no escaping needed for \" in ''), got: {}",
        result
    );
}

#[test]
fn test_wrap_all_long_strings_wraps_long_string() {
    let toml = r#"description = "This is a very long description that exceeds the column width limit""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    assert!(result.contains(r#"""""#), "Expected multiline string, got: {}", result);
    assert!(result.contains(r#"\"#), "Expected line continuation, got: {}", result);
}

#[test]
fn test_wrap_all_long_strings_short_string_unchanged() {
    let toml = r#"name = "short""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert_eq!(result, r#"name = "short""#);
}

#[test]
fn test_wrap_all_long_strings_inline_table_not_wrapped() {
    let toml = r#"authors = [{ name = "A very long author name that would normally exceed column width" }]"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        !result.contains(r#"""""#),
        "Inline table strings should not be wrapped, got: {}",
        result
    );
}

#[test]
fn test_wrap_all_long_strings_converts_quote_style() {
    let toml = r#"msg = "say \"hello\"""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        result.contains(r#"'say "hello"'"#),
        "Expected literal string for double quotes, got: {}",
        result
    );
}

#[test]
fn test_wrap_all_long_strings_multiline_to_single() {
    let toml = "msg = \"\"\"\nno newlines here\"\"\"";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        result.contains(r#""no newlines here""#),
        "Expected single-line string, got: {}",
        result
    );
}

#[test]
fn test_wrap_string_very_long_multiple_wraps() {
    let toml = r#"description = "This is an extremely long description that will definitely need multiple line wraps to fit within the specified column width limit and should be properly wrapped with line continuations""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 60, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    description = """\
      This is an extremely long description that will \
      definitely need multiple line wraps to fit within the \
      specified column width limit and should be properly \
      wrapped with line continuations\
      """
    "#);
}

#[test]
fn test_wrap_string_with_spaces_at_break_points() {
    let toml = r#"description = "First part of the description and second part of the description""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 40, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    description = """\
      First part of the description and \
      second part of the description\
      """
    "#);
}

#[test]
fn test_wrap_string_with_indent_calculation() {
    let toml = r#"very_long_key_name = "This is a long string that needs wrapping""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "    ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"very_long_key_name = "This is a long string that needs wrapping""#);
}

#[test]
fn test_wrap_string_preserves_special_chars() {
    let toml = r#"msg = "String with \n newline \t tab and \\ backslash that is very long and needs wrapping""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    msg = """\
      String with \n newline \t tab and \\ backslash \
      that is very long and needs wrapping\
      """
    "#);
}

#[test]
fn test_wrap_string_with_unicode() {
    let toml = r#"description = "Unicode string with émojis 🎉 and special characters: αβγ that is quite long""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    description = """\
      Unicode string with émojis 🎉 and special \
      characters: αβγ that is quite long\
      """
    "#);
}

#[test]
fn test_wrap_string_exact_boundary() {
    let toml = r#"description = "Exactly eighty characters long to test boundary conditions for wrapping!!!!""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 80, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"description = "Exactly eighty characters long to test boundary conditions for wrapping!!!!""#);
}

#[test]
fn test_wrap_string_single_word_longer_than_width() {
    let toml = r#"url = "https://example.com/very/long/path/that/exceeds/column/width/limit/significantly""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 40, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    url = """\
      https://example.com/very/long/path/th\
      at/exceeds/column/width/limit/signifi\
      cantly\
      """
    "#);
}

#[test]
fn test_wrap_multiple_strings_in_document() {
    let toml = r#"
        description = "This is a very long description that needs wrapping"
        summary = "This is another very long summary that also needs wrapping"
        details = "And yet another long text field that requires wrapping"
    "#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"

          description = """\
    This is a very long description that needs \
    wrapping\
    """
          summary = """\
    This is another very long summary that also \
    needs wrapping\
    """
          details = """\
    And yet another long text field that requires \
    wrapping\
    """
    "#);
}

#[test]
fn test_wrap_string_with_double_quotes_inside() {
    let toml = r#"msg = "Text with \"quotes\" inside that is very long and needs wrapping for sure""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    msg = """\
      Text with \"quotes\" inside that is very long \
      and needs wrapping for sure\
      """
    "#);
}

#[test]
fn test_wrap_nested_in_array() {
    let toml = r#"items = ["This is a very long string in an array that might need wrapping"]"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    items = ["""\
      This is a very long string in an array that \
      might need wrapping\
      """]
    "#);
}

#[test]
fn test_update_content_wrapped() {
    let result = apply_update_content_wrapped(
        r#"name = "short""#,
        |s| format!("{} but now it's very long and will need wrapping at some point", s),
        50,
        "  ",
    );
    insta::assert_snapshot!(result, @r#"
    name = """\
      short but now it's very long and will need \
      wrapping at some point\
      """
    "#);
}

#[test]
fn test_strip_quotes_double_quotes() {
    let result = strip_quotes("\"hello\"");
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_strip_quotes_single_quotes() {
    let result = strip_quotes("'hello'");
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_strip_quotes_no_quotes() {
    let result = strip_quotes("hello");
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_strip_quotes_empty_string() {
    let result = strip_quotes("\"\"");
    insta::assert_snapshot!(result, @"");
}

#[test]
fn test_strip_quotes_triple_quotes() {
    let result = strip_quotes("\"\"\"hello\"\"\"");
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_normalize_key_quotes_simple_literal() {
    let root = parse("'key' = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"key = 1
");
}

#[test]
fn test_normalize_key_quotes_bare_key_unchanged() {
    let root = parse("key = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"key = 1
");
}

#[test]
fn test_normalize_key_quotes_basic_string_stripped() {
    let root = parse("\"key\" = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"key = 1
");
}

#[test]
fn test_normalize_key_quotes_dotted_with_literal() {
    let root = parse("lint.per-file-ignores.'tests/*' = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#"lint.per-file-ignores."tests/*" = 1
"#);
}

#[test]
fn test_normalize_key_quotes_dotted_mixed_styles() {
    let root = parse("lint.per-file-ignores.'tests/*' = 1\nlint.per-file-ignores.\"flexget/*\" = 2\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#"
    lint.per-file-ignores."tests/*" = 1
    lint.per-file-ignores."flexget/*" = 2
    "#);
}

#[test]
fn test_normalize_key_quotes_literal_with_backslash() {
    let root = parse("'path\\to\\file' = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#""path\\to\\file" = 1
"#);
}

#[test]
fn test_normalize_key_quotes_literal_with_double_quote() {
    let root = parse(
        r#"'say "hello"' = 1
"#,
    );
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#""say \"hello\"" = 1
"#);
}

#[test]
fn test_normalize_key_quotes_literal_with_backslash_and_quote() {
    let root = parse(
        r#"'c:\Users\"me"' = 1
"#,
    );
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#""c:\\Users\\\"me\"" = 1
"#);
}

#[test]
fn test_normalize_key_quotes_multiple_literal_segments() {
    let root = parse("'first'.'second' = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"first.second = 1
");
}

#[test]
fn test_normalize_key_quotes_preserves_basic_in_dotted() {
    let root = parse("bare.\"basic\".'literal' = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"bare.basic.literal = 1
");
}

#[test]
fn test_normalize_key_quotes_preserves_required_quotes() {
    let root = parse("\"key with spaces\" = 1\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#""key with spaces" = 1
"#);
}

#[test]
fn test_normalize_key_quotes_inline_table_keys() {
    let root = parse("val = { \"else\" = \"no\", \"then\" = \"yes\" }\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @r#"val = { else = "no", then = "yes" }
"#);
}

#[test]
fn test_normalize_key_quotes_no_keys() {
    let root = parse("# just a comment\n");
    normalize_key_quotes(&root);
    insta::assert_snapshot!(root.to_string(), @"# just a comment
");
}

#[test]
fn test_wrap_string_with_double_colon() {
    let toml = r#"classifier = "Programming Language :: Python :: 3 :: Only and more text here""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    classifier = """\
      Programming Language :: Python :: 3 :: \
      Only and more text here\
      """
    "#);
}

#[test]
fn test_wrap_string_no_spaces() {
    let toml = r#"url = "verylongurlwithoutanyspacesthatneedstobewrappedanyway""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 30, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    url = """\
      verylongurlwithoutanyspaces\
      thatneedstobewrappedanyway\
      """
    "#);
}

#[test]
fn test_wrap_with_control_characters() {
    let toml = "desc = \"has\\ttab\\nand newline that is quite long for wrapping\"";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 40, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    desc = """\
      has\ttab\nand newline that is quite \
      long for wrapping\
      """
    "#);
}

#[test]
fn test_wrap_short_string_no_wrap_needed() {
    let toml = r#"short = "hi""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"short = "hi""#);
}

#[test]
fn test_wrap_string_at_exact_column_boundary() {
    let toml = r#"x = "exactly""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 15, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"x = "exactly""#);
}

#[test]
fn test_load_text_multiline_with_leading_newline() {
    let result = load_text("\"\"\"\nhello\"\"\"", MULTI_LINE_BASIC_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_load_text_multiline_with_crlf() {
    let result = load_text("\"\"\"\r\nhello\"\"\"", MULTI_LINE_BASIC_STRING);
    insta::assert_snapshot!(result, @"hello");
}

#[test]
fn test_update_content_with_very_long_key() {
    let result = apply_update_content_wrapped(
        r#"this_is_a_very_long_key_name_that_takes_up_space = "short value""#,
        |s| s.to_string(),
        80,
        "  ",
    );
    insta::assert_snapshot!(result, @r#"this_is_a_very_long_key_name_that_takes_up_space = "short value""#);
}

#[test]
fn test_string_with_control_char_uses_basic() {
    let result = apply_update_content("key = 'has\\ttab'", |s| s.to_string());
    insta::assert_snapshot!(result, @r#"key = "has\\ttab""#);
}

#[test]
fn test_wrap_string_short_text_no_wrap_needed() {
    let toml = r#"k = "hi""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 200, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"k = "hi""#);
}

#[test]
fn test_wrap_string_text_shorter_than_max_len() {
    let toml = r#"x = "short""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 100, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"x = "short""#);
}

#[test]
fn test_wrap_string_in_inline_table_no_wrap() {
    let toml = r#"config = { description = "A very long description that would normally be wrapped but inline tables should not wrap" }"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 40, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"config = { description = "A very long description that would normally be wrapped but inline tables should not wrap" }"#);
}

#[test]
fn test_update_content_wrapped_short_no_wrap() {
    let result = apply_update_content_wrapped(r#"a = "b""#, |s| s.to_string(), 100, "  ");
    insta::assert_snapshot!(result, @r#"a = "b""#);
}

#[test]
fn test_update_content_wrapped_in_inline_table() {
    let toml = r#"x = { y = "very long string that would be wrapped in normal context but not in inline table" }"#;
    let root_ast = parse(toml);
    for_each_string(&root_ast, |string_node| {
        update_content_wrapped(string_node, |s| s.to_string(), 40, "  ");
    });
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"x = { y = "very long string that would be wrapped in normal context but not in inline table" }"#);
}

#[test]
fn test_wrap_considers_key_length() {
    let toml = r#"k = "This is a very long description that will definitely need wrapping""#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 40, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    k = """\
      This is a very long description that \
      will definitely need wrapping\
      """
    "#);
}

#[test]
fn test_issue_213_multiline_string_with_actual_newlines_preserved() {
    let toml = r#"parse = """(?x)
    (?P<major>0|[1-9]\d*)\.
    (?P<minor>0|[1-9]\d*)\.
    (?P<patch>0|[1-9]\d*)
    (?:
        -
        (?P<pre_release_label>[a-zA-Z-]+)
        \.
        (?P<pre_release_number>0|[1-9]\d*)
    )?
"""
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "    ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    parse = """(?x)
        (?P<major>0|[1-9]\d*)\.
        (?P<minor>0|[1-9]\d*)\.
        (?P<patch>0|[1-9]\d*)
        (?:
            -
            (?P<pre_release_label>[a-zA-Z-]+)
            \.
            (?P<pre_release_number>0|[1-9]\d*)
        )?
    """
    "#);
}

#[test]
fn test_issue_213_full_example() {
    let toml = r#"[tool.bumpversion]
current_version = "5.48.4-rc.0"
parse = """(?x)
    (?P<major>0|[1-9]\d*)\.
    (?P<minor>0|[1-9]\d*)\.
    (?P<patch>0|[1-9]\d*)
    (?:
        -                                    # dash separator for pre-release section
        (?P<pre_release_label>[a-zA-Z-]+)    # pre-release label
        \.
        (?P<pre_release_number>0|[1-9]\d*)  # pre-release version number
    )?                                       # pre-release section is optional
"""
serialize = [
    "{major}.{minor}.{patch}-{pre_release_label}.{pre_release_number}",
    "{major}.{minor}.{patch}",
]
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "    ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    [tool.bumpversion]
    current_version = "5.48.4-rc.0"
    parse = """(?x)
        (?P<major>0|[1-9]\d*)\.
        (?P<minor>0|[1-9]\d*)\.
        (?P<patch>0|[1-9]\d*)
        (?:
            -                                    # dash separator for pre-release section
            (?P<pre_release_label>[a-zA-Z-]+)    # pre-release label
            \.
            (?P<pre_release_number>0|[1-9]\d*)  # pre-release version number
        )?                                       # pre-release section is optional
    """
    serialize = [
        "{major}.{minor}.{patch}-{pre_release_label}.{pre_release_number}",
        "{major}.{minor}.{patch}",
    ]
    "#);
}

#[test]
fn test_skip_wrap_for_keys_exact_match() {
    let toml = r#"[tool.bumpversion]
parse = "This is a very long string that would normally be wrapped but we want to skip it for this key"
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "    ", &[String::from("tool.bumpversion.parse")]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    [tool.bumpversion]
    parse = "This is a very long string that would normally be wrapped but we want to skip it for this key"
    "#);
}

#[test]
fn test_skip_wrap_for_keys_wildcard_match() {
    let toml = r#"[tool.bumpversion]
parse = "This is a very long string that would normally be wrapped but we want to skip it"
serialize = "Another very long string that should also be skipped for this pattern match"
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "    ", &[String::from("tool.bumpversion.*")]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    [tool.bumpversion]
    parse = "This is a very long string that would normally be wrapped but we want to skip it"
    serialize = "Another very long string that should also be skipped for this pattern match"
    "#);
}

#[test]
fn test_skip_wrap_for_keys_star_pattern() {
    let toml = r#"[project]
description = "This is a very long description string that would normally be wrapped but asterisk pattern skips all"
[tool.other]
value = "Another very long string in a different section that should also be skipped"
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "    ", &[String::from("*")]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    [project]
    description = "This is a very long description string that would normally be wrapped but asterisk pattern skips all"
    [tool.other]
    value = "Another very long string in a different section that should also be skipped"
    "#);
}

#[test]
fn test_multiline_literal_with_backslashes_preserved() {
    // Regression: triple-literal strings containing `\.` (etc.) used to be converted to triple-basic, producing
    // invalid TOML because `\.` is not a valid basic-string escape.
    let toml = "[tool.black]\nexclude = '''\n(\n  \\.eggs\n  | \\.git\n)\n'''\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r"
    [tool.black]
    exclude = '''
    (
      \.eggs
      | \.git
    )
    '''
    ");
}

#[test]
fn test_multiline_literal_with_triple_single_quote_falls_back_to_basic() {
    let toml = "[t]\nv = '''line1\n\\.something\nlast'''\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        toml::from_str::<toml::Value>(&result).is_ok(),
        "output should be valid TOML, got: {result}"
    );
}

#[test]
fn test_multiline_basic_with_quotes_converts_to_literal() {
    // A multi-line basic string whose body contains `"` switches to the literal form so the quotes need no escaping.
    let toml = "[t]\nv = \"\"\"line1\nhas \\\"quote\\\"\nlast\"\"\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(result.contains("'''"), "expected triple-literal output, got: {result}");
    assert!(
        toml::from_str::<toml::Value>(&result).is_ok(),
        "output should be valid TOML, got: {result}"
    );
}

#[test]
fn test_multiline_basic_with_tab_and_cr_stays_basic() {
    // Exercise the `\r` and `\t` arms of the control-char check; each clears a different one of the `c != '\n'`,
    // `c != '\r'`, `c != '\t'` guards as the body is walked.
    let toml = "[t]\nv = \"\"\"abc\\tdef\\rghi\nmulti\"\"\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        toml::from_str::<toml::Value>(&result).is_ok(),
        "output should be valid TOML, got: {result}"
    );
}

#[test]
fn test_multiline_basic_with_backspace_blocks_literal_form() {
    // Cover the `chars().any(control)` branch of `can_use_multiline_literal_string`: a multi-line basic string
    // carrying a backspace escape can never become literal because backspace is a disallowed control character.
    let toml = "[t]\nv = \"\"\"abc\\bdef\nmulti\"\"\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(
        result.contains("\"\"\""),
        "output must remain triple-basic when literal form is unsafe: {result}"
    );
}

#[test]
fn test_single_to_multiline_basic_with_skip_wrap_falls_back_to_basic() {
    // Drive the `else` branch in the `if use_literal` arm of `preserve_newlines`: a single-line basic string
    // with embedded newlines and skip_wrap forces single_to_multiline=true, preserve_newlines=true, use_literal=false.
    let toml = "[tool.x]\nv = \"line one\\nline two\\nline three\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[String::from("*.v")]);
    let result = root_ast.to_string();
    assert!(result.contains("\"\"\""), "expected triple-basic output, got: {result}");
    assert!(
        toml::from_str::<toml::Value>(&result).is_ok(),
        "output should be valid TOML, got: {result}"
    );
}

#[test]
fn test_can_use_multiline_literal_string_rejects_triple_single_quote() {
    // Drive the `s.contains("'''")` short-circuit branch: a basic string carrying a quote (so `use_literal`
    // would normally fire) plus `'''` in the body must stay basic because the literal form cannot represent the
    // closing delimiter.
    let toml = "[t]\nv = \"\"\"\\\"abc '''inside''' xyz\nmulti\"\"\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(result.contains("\"\"\""), "output must remain triple-basic: {result}");
    assert!(
        toml::from_str::<toml::Value>(&result).is_ok(),
        "output should be valid TOML, got: {result}"
    );
}

#[test]
fn test_issue_388_load_text_multiline_basic_line_ending_backslash() {
    // A multi-line basic string using a line-ending backslash (trims the newline and the
    // next line's leading whitespace) must decode to its true value, not the raw escaped text.
    let token = "\"\"\"\\\necho \"one\"\necho \"two\"\\\n\"\"\"";
    let result = load_text(token, MULTI_LINE_BASIC_STRING);
    assert_eq!(result, "echo \"one\"\necho \"two\"");
}

#[test]
fn test_issue_388_line_ending_backslash_value_preserved_and_idempotent() {
    let toml = "[tool.example]\ncmd = \"\"\"\\\necho \"one\"\necho \"two\"\\\n\"\"\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();

    let parsed: toml::Value = toml::from_str(&result).unwrap();
    assert_eq!(
        parsed["tool"]["example"]["cmd"].as_str().unwrap(),
        "echo \"one\"\necho \"two\""
    );

    let root_ast2 = parse(&result);
    wrap_all_long_strings(&root_ast2, 120, "  ", &[]);
    assert_eq!(result, root_ast2.to_string(), "formatting must be idempotent");
}

#[test]
fn test_issue_388_leading_newline_preserved_literal_form() {
    // Value starts with a newline and contains `"`, so it is emitted as a triple-literal.
    // The builder must prepend an extra newline so TOML's first-newline trim is harmless.
    let toml = "v = \"\"\"\n\\necho \"x\" done\"\"\"\n";
    let original: toml::Value = toml::from_str(toml).unwrap();
    assert_eq!(original["v"].as_str().unwrap(), "\necho \"x\" done");

    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[]);
    let result = root_ast.to_string();
    assert!(result.contains("'''"), "expected triple-literal output, got: {result}");

    let parsed: toml::Value = toml::from_str(&result).unwrap();
    assert_eq!(parsed["v"].as_str().unwrap(), "\necho \"x\" done");
}

#[test]
fn test_issue_388_leading_newline_preserved_basic_form() {
    // Value starts with a newline and goes through the basic-preserving builder
    // (single-line -> multi-line via skip_wrap, literal form unsafe due to the newline).
    let toml = "[tool.x]\nv = \"\\nhello\"\n";
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 120, "  ", &[String::from("*.v")]);
    let result = root_ast.to_string();
    assert!(result.contains("\"\"\""), "expected triple-basic output, got: {result}");

    let parsed: toml::Value = toml::from_str(&result).unwrap();
    assert_eq!(parsed["tool"]["x"]["v"].as_str().unwrap(), "\nhello");
}

#[test]
fn test_skip_wrap_for_keys_partial_wildcard() {
    let toml = r#"[project]
description = "Short"
[tool.bumpversion]
parse = "This is a very long string that should be skipped because of the pattern"
[tool.other]
value = "This is a very long string that should be wrapped because it does not match pattern"
"#;
    let root_ast = parse(toml);
    wrap_all_long_strings(&root_ast, 50, "    ", &[String::from("*.parse")]);
    let result = root_ast.to_string();
    insta::assert_snapshot!(result, @r#"
    [project]
    description = "Short"
    [tool.bumpversion]
    parse = "This is a very long string that should be skipped because of the pattern"
    [tool.other]
    value = """\
        This is a very long string that should be \
        wrapped because it does not match pattern\
        """
    "#);
}
