//! Language-specific string escaping for e2e test code generation.

/// Escape a string for embedding in a Python string literal.
pub fn escape_python(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c if (c as u32) < 0x20 => {
                // Control character — emit \xHH escape so Python source remains valid.
                out.push_str(&format!("\\x{:02x}", c as u32));
            }
            c => out.push(c),
        }
    }
    out
}

/// Escape a string for embedding in a Rust string literal.
pub fn escape_rust(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Compute the number of # needed for a Rust raw string literal.
pub fn raw_string_hashes(s: &str) -> usize {
    let mut max_hashes = 0;
    let mut current = 0;
    let mut after_quote = false;
    for ch in s.chars() {
        if ch == '"' {
            after_quote = true;
            current = 0;
        } else if ch == '#' && after_quote {
            current += 1;
            max_hashes = max_hashes.max(current);
        } else {
            after_quote = false;
            current = 0;
        }
    }
    max_hashes + 1
}

/// Format a string as a Rust raw string literal (r#"..."#).
pub fn rust_raw_string(s: &str) -> String {
    let hashes = raw_string_hashes(s);
    let h: String = "#".repeat(hashes);
    format!("r{h}\"{s}\"{h}")
}

/// Escape a string for embedding in a JavaScript/TypeScript double-quoted string literal.
///
/// `$` does not need escaping in double-quoted strings (only in template literals).
/// Escaping it would produce `\$` which formatters flag as an unnecessary escape.
pub fn escape_js(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a JavaScript/TypeScript template literal (backtick string).
///
/// Template literals interpolate `${...}` and use backtick delimiters, so both
/// `` ` `` and `$` must be escaped to prevent unintended interpolation.
pub fn escape_js_template(s: &str) -> String {
    s.replace('\\', "\\\\").replace('`', "\\`").replace('$', "\\$")
}

/// Returns `true` if the string must use a Go interpreted (double-quoted) literal
/// rather than a raw (backtick) literal.
///
/// Go raw string literals cannot contain backtick characters or NUL bytes, and
/// `\r` inside a raw string is passed through as a literal CR which gofmt rejects.
fn go_needs_quoted(s: &str) -> bool {
    s.contains('`') || s.bytes().any(|b| b == 0 || b == b'\r')
}

/// Format a string as a Go string literal (backtick or quoted).
///
/// Prefers backtick raw literals for readability, but falls back to double-quoted
/// interpreted literals when the string contains characters that raw literals
/// cannot represent: backtick `` ` ``, NUL (`\x00`), or carriage return (`\r`).
pub fn go_string_literal(s: &str) -> String {
    if go_needs_quoted(s) {
        format!("\"{}\"", escape_go(s))
    } else {
        format!("`{s}`")
    }
}

/// Escape a string for embedding in a Go double-quoted string.
///
/// Handles all characters that cannot appear literally in a Go interpreted string:
/// `\\`, `"`, `\n`, `\r`, `\t`, and NUL (`\x00`). Other non-printable bytes are
/// emitted as `\xNN` hex escape sequences.
pub fn escape_go(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.bytes() {
        match b {
            b'\\' => out.push_str("\\\\"),
            b'"' => out.push_str("\\\""),
            b'\n' => out.push_str("\\n"),
            b'\r' => out.push_str("\\r"),
            b'\t' => out.push_str("\\t"),
            0 => out.push_str("\\x00"),
            // Other control characters or non-ASCII bytes: hex escape.
            b if b < 0x20 || b == 0x7f => {
                out.push_str(&format!("\\x{b:02x}"));
            }
            _ => out.push(b as char),
        }
    }
    out
}

/// Escape a string for embedding in a Java string literal.
pub fn escape_java(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a Kotlin double-quoted string literal.
/// Like Java escaping but also escapes `$` which triggers Kotlin string interpolation.
pub fn escape_kotlin(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('$', "\\$")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a C# string literal.
pub fn escape_csharp(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a PHP string literal.
pub fn escape_php(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('$', "\\$")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a double-quoted Ruby string literal.
pub fn escape_ruby(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('#', "\\#")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a single-quoted Ruby string literal.
/// Single-quoted Ruby strings only interpret `\\` and `\'`.
pub fn escape_ruby_single(s: &str) -> String {
    s.replace('\\', "\\\\").replace('\'', "\\'")
}

/// Convert a `{param}` template string to a Ruby double-quoted string with `#{param}` interpolation.
///
/// `{key}` placeholders are converted to `#{key}`. All other characters are escaped for
/// Ruby double-quoted strings. The returned value does NOT include the surrounding `"` quotes.
pub fn ruby_template_to_interpolation(template: &str) -> String {
    let mut out = String::with_capacity(template.len() + 8);
    let mut chars = template.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            '{' => {
                // Check if this is a {identifier} placeholder
                let is_ident_start = chars.peek().is_some_and(|&c| c.is_ascii_alphabetic() || c == '_');
                if is_ident_start {
                    // Collect the identifier
                    let mut ident = String::new();
                    while let Some(&c) = chars.peek() {
                        if c.is_ascii_alphanumeric() || c == '_' {
                            ident.push(chars.next().unwrap());
                        } else {
                            break;
                        }
                    }
                    if chars.peek() == Some(&'}') {
                        chars.next(); // consume '}'
                        out.push('#');
                        out.push('{');
                        out.push_str(&ident);
                        out.push('}');
                    } else {
                        // Not a valid {ident} placeholder — emit literally
                        out.push('{');
                        out.push_str(&ident);
                    }
                } else {
                    out.push('{');
                }
            }
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '#' => out.push_str("\\#"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            c => out.push(c),
        }
    }
    out
}

/// Convert a `{param}` template string to an R expression using `paste0()`.
///
/// `{key}` placeholders are converted to variable references in a `paste0(...)` call.
/// Literal text segments are R string literals. If the template has no placeholders,
/// a plain R string literal is returned. If the template is a single bare placeholder
/// like `{text}`, just the variable name is returned.
///
/// Examples:
/// - `"[BTN:{text}]"` → `paste0("[BTN:", text, "]")`
/// - `"--- {text} ---"` → `paste0("--- ", text, " ---")`
/// - `"{text}"` → `text`
/// - `"static"` → `"static"`
pub fn r_template_to_paste0(template: &str) -> String {
    enum Seg {
        Lit(String),
        Param(String),
    }
    let mut segments: Vec<Seg> = Vec::new();
    let mut lit = String::new();
    let mut chars = template.chars().peekable();
    while let Some(ch) = chars.next() {
        if ch == '{' {
            let is_ident_start = chars.peek().is_some_and(|&c| c.is_ascii_alphabetic() || c == '_');
            if is_ident_start {
                let mut ident = String::new();
                while let Some(&c) = chars.peek() {
                    if c.is_ascii_alphanumeric() || c == '_' {
                        ident.push(chars.next().unwrap());
                    } else {
                        break;
                    }
                }
                if chars.peek() == Some(&'}') {
                    chars.next();
                    if !lit.is_empty() {
                        segments.push(Seg::Lit(lit.clone()));
                        lit.clear();
                    }
                    segments.push(Seg::Param(ident));
                    continue;
                }
                lit.push('{');
                lit.push_str(&ident);
            } else {
                lit.push('{');
            }
        } else {
            lit.push(ch);
        }
    }
    if !lit.is_empty() {
        segments.push(Seg::Lit(lit));
    }
    match segments.as_slice() {
        [] => r#""""#.to_string(),
        [Seg::Param(p)] => p.clone(),
        segs => {
            let args: Vec<String> = segs
                .iter()
                .map(|s| match s {
                    Seg::Lit(l) => format!("\"{}\"", escape_r(l)),
                    Seg::Param(p) => p.clone(),
                })
                .collect();
            format!("paste0({})", args.join(", "))
        }
    }
}

/// Returns true if the string needs double quotes (contains control characters
/// that require escape sequences only available in double-quoted strings, or
/// apostrophes which are only properly escaped in double-quoted strings in Ruby).
pub fn ruby_needs_double_quotes(s: &str) -> bool {
    s.contains('\n') || s.contains('\r') || s.contains('\t') || s.contains('\0') || s.contains('\'')
}

/// Format a string as a Ruby literal, preferring single quotes but using double
/// quotes when the string contains apostrophes, control characters, or other
/// special chars that require escaping only available in double-quoted strings.
pub fn ruby_string_literal(s: &str) -> String {
    if ruby_needs_double_quotes(s) {
        format!("\"{}\"", escape_ruby(s))
    } else {
        format!("'{}'", escape_ruby_single(s))
    }
}

/// Escape a string for embedding in an Elixir string literal.
pub fn escape_elixir(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('#', "\\#")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in an R string literal.
pub fn escape_r(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a C string literal.
pub fn escape_c(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Sanitize an identifier for use as a test function name.
/// Replaces non-alphanumeric characters with underscores, strips leading digits,
/// and strips any underscores left dangling after the digit prefix so that
/// generators which prefix the result (e.g. `test_<ident>`) don't produce
/// double-underscore names like `test__foo` from fixture ids like `24_foo`.
pub fn sanitize_ident(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    for ch in s.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' {
            result.push(ch);
        } else {
            result.push('_');
        }
    }
    // Strip leading digits.
    let after_digits = result.trim_start_matches(|c: char| c.is_ascii_digit());
    // If we stripped any digits, also strip the underscores left behind so
    // callers like `format!("test_{name}")` don't yield `test__foo`.
    let trimmed = if after_digits.len() < result.len() {
        after_digits.trim_start_matches('_')
    } else {
        after_digits
    };
    if trimmed.is_empty() {
        "_".to_string()
    } else {
        trimmed.to_string()
    }
}

/// Convert a category name to a sanitized filename component.
pub fn sanitize_filename(s: &str) -> String {
    s.chars()
        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
        .collect::<String>()
        .to_lowercase()
}

/// Expand fixture template expressions in a string value.
///
/// Supported templates:
/// - `{{ repeat 'X' N times }}` — expands to the character X repeated N times
///
/// If no templates are found, the original string is returned unchanged.
pub fn expand_fixture_templates(s: &str) -> String {
    const PREFIX: &str = "{{ repeat '";
    const SUFFIX: &str = " times }}";

    let mut result = String::with_capacity(s.len());
    let mut remaining = s;

    while let Some(start) = remaining.find(PREFIX) {
        result.push_str(&remaining[..start]);
        let after_prefix = &remaining[start + PREFIX.len()..];

        // Expect character(s) followed by `' N times }}`
        if let Some(quote_pos) = after_prefix.find("' ") {
            let ch = &after_prefix[..quote_pos];
            let after_quote = &after_prefix[quote_pos + 2..];

            if let Some(end) = after_quote.find(SUFFIX) {
                let count_str = after_quote[..end].trim();
                if let Ok(count) = count_str.parse::<usize>() {
                    result.push_str(&ch.repeat(count));
                    remaining = &after_quote[end + SUFFIX.len()..];
                    continue;
                }
            }
        }

        // Template didn't match — emit the prefix literally and continue
        result.push_str(PREFIX);
        remaining = after_prefix;
    }
    result.push_str(remaining);
    result
}

/// Escape a string for embedding in a POSIX single-quoted shell string literal.
///
/// Wraps the string in single quotes and escapes embedded single quotes as `'\''`.
/// Single-quoted shell strings treat every character literally except `'`, so
/// no other escaping is needed.
pub fn escape_shell(s: &str) -> String {
    s.replace('\'', r"'\''")
}

/// Escape a string for embedding in a Gleam string literal.
pub fn escape_gleam(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

/// Escape a string for embedding in a Zig string literal.
pub fn escape_zig(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

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

    /// Go raw string literals (backticks) cannot contain NUL bytes — gofmt rejects them.
    /// Strings with NUL must fall back to a double-quoted interpreted literal with `\x00`.
    #[test]
    fn go_string_literal_nul_bytes_use_quoted_form() {
        let s = "Hello\x00World";
        let lit = go_string_literal(s);
        // Must not contain a raw NUL byte
        assert!(
            !lit.as_bytes().contains(&0u8),
            "go_string_literal emitted a NUL byte — gofmt would reject this: {lit:?}"
        );
        // Must be a double-quoted string, not a backtick raw string
        assert!(
            lit.starts_with('"'),
            "expected double-quoted string for NUL input, got: {lit:?}"
        );
        // The NUL must be represented as \\x00
        assert!(
            lit.contains("\\x00"),
            "expected \\x00 escape sequence for NUL byte, got: {lit:?}"
        );
    }

    /// Strings with carriage return must also use the double-quoted form
    /// because Go raw strings cannot represent `\r`.
    #[test]
    fn go_string_literal_carriage_return_uses_quoted_form() {
        let s = "line1\r\nline2";
        let lit = go_string_literal(s);
        assert!(
            !lit.as_bytes().contains(&b'\r'),
            "go_string_literal emitted a literal CR — gofmt would reject this: {lit:?}"
        );
        assert!(
            lit.starts_with('"'),
            "expected double-quoted string for CR input, got: {lit:?}"
        );
    }

    /// Strings with only printable chars and no backtick should still use the
    /// readable backtick form.
    #[test]
    fn go_string_literal_plain_string_uses_backtick() {
        let s = "Hello World\nwith newline";
        let lit = go_string_literal(s);
        assert!(
            lit.starts_with('`'),
            "expected backtick form for plain string, got: {lit:?}"
        );
    }

    /// Strings that contain a backtick must fall back to double-quoted form.
    #[test]
    fn go_string_literal_backtick_in_string_uses_quoted_form() {
        let s = "has `backtick`";
        let lit = go_string_literal(s);
        assert!(
            lit.starts_with('"'),
            "expected double-quoted form when string contains backtick, got: {lit:?}"
        );
    }

    /// Fixture ids with a numeric prefix (`24_cookie_samesite_strict`) must not
    /// produce names like `_cookie_samesite_strict` that, when prefixed with
    /// `test_`, yield the invalid-looking `test__cookie_samesite_strict`.
    #[test]
    fn sanitize_ident_strips_leading_underscore_after_digit_prefix() {
        assert_eq!(sanitize_ident("24_cookie_samesite_strict"), "cookie_samesite_strict");
        assert_eq!(sanitize_ident("01_foo"), "foo");
        assert_eq!(sanitize_ident("9bar"), "bar");
    }

    /// Genuine leading underscores (no preceding digits) are preserved so
    /// fixture ids that intentionally start with `_` round-trip unchanged.
    #[test]
    fn sanitize_ident_preserves_leading_underscore_without_digits() {
        assert_eq!(sanitize_ident("_foo"), "_foo");
        assert_eq!(sanitize_ident("__bar"), "__bar");
    }

    /// Strings consisting only of digits (and the underscores left behind)
    /// collapse to the placeholder `_` since the result would otherwise be empty.
    #[test]
    fn sanitize_ident_all_digits_returns_underscore_placeholder() {
        assert_eq!(sanitize_ident("123"), "_");
        assert_eq!(sanitize_ident("12_"), "_");
    }

    /// Non-leading digits and underscores are untouched.
    #[test]
    fn sanitize_ident_preserves_interior_chars() {
        assert_eq!(sanitize_ident("foo_42_bar"), "foo_42_bar");
        assert_eq!(sanitize_ident("foo.bar-baz"), "foo_bar_baz");
    }

    /// Ruby strings with apostrophes must use double quotes to properly escape them.
    #[test]
    fn ruby_string_with_apostrophe_uses_double_quotes() {
        let s = "Tests JWT rejection when token is provided without 'Bearer ' prefix.";
        let result = ruby_string_literal(s);
        assert!(
            result.starts_with('"') && result.ends_with('"'),
            "String with apostrophe must use double quotes, got: {}",
            result
        );
        assert!(
            result.contains("Bearer"),
            "String content must be preserved, got: {}",
            result
        );
    }
}
