use std::borrow::Cow;

use anyhow::{anyhow, Result};
use helix_core::{smallvec, SmallVec, Tendril};

#[derive(Debug, PartialEq, Eq)]
pub enum CaseChange {
    Upcase,
    Downcase,
    Capitalize,
}

#[derive(Debug, PartialEq, Eq)]
pub enum FormatItem {
    Text(Tendril),
    Capture(usize),
    CaseChange(usize, CaseChange),
    Conditional(usize, Option<Tendril>, Option<Tendril>),
}

#[derive(Debug, PartialEq, Eq)]
pub struct Regex {
    value: Tendril,
    replacement: Vec<FormatItem>,
    options: Tendril,
}

#[derive(Debug, PartialEq, Eq)]
pub enum SnippetElement<'a> {
    Tabstop {
        tabstop: usize,
    },
    Placeholder {
        tabstop: usize,
        value: Vec<SnippetElement<'a>>,
    },
    Choice {
        tabstop: usize,
        choices: Vec<Tendril>,
    },
    Variable {
        name: &'a str,
        default: Option<Vec<SnippetElement<'a>>>,
        regex: Option<Regex>,
    },
    Text(Tendril),
}

#[derive(Debug, PartialEq, Eq)]
pub struct Snippet<'a> {
    elements: Vec<SnippetElement<'a>>,
}

pub fn parse(s: &str) -> Result<Snippet<'_>> {
    parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
}

fn render_elements(
    snippet_elements: &[SnippetElement<'_>],
    insert: &mut Tendril,
    offset: &mut usize,
    tabstops: &mut Vec<(usize, (usize, usize))>,
    newline_with_offset: &str,
    include_placeholder: bool,
) {
    use SnippetElement::*;

    for element in snippet_elements {
        match element {
            Text(text) => {
                // small optimization to avoid calling replace when it's unnecessary
                let text = if text.contains('\n') {
                    Cow::Owned(text.replace('\n', newline_with_offset))
                } else {
                    Cow::Borrowed(text.as_str())
                };
                *offset += text.chars().count();
                insert.push_str(&text);
            }
            Variable {
                name: _,
                regex: _,
                r#default,
            } => {
                // TODO: variables. For now, fall back to the default, which defaults to "".
                render_elements(
                    r#default.as_deref().unwrap_or_default(),
                    insert,
                    offset,
                    tabstops,
                    newline_with_offset,
                    include_placeholder,
                );
            }
            &Tabstop { tabstop } => {
                tabstops.push((tabstop, (*offset, *offset)));
            }
            Placeholder {
                tabstop,
                value: inner_snippet_elements,
            } => {
                let start_offset = *offset;
                if include_placeholder {
                    render_elements(
                        inner_snippet_elements,
                        insert,
                        offset,
                        tabstops,
                        newline_with_offset,
                        include_placeholder,
                    );
                }
                tabstops.push((*tabstop, (start_offset, *offset)));
            }
            &Choice {
                tabstop,
                choices: _,
            } => {
                // TODO: choices
                tabstops.push((tabstop, (*offset, *offset)));
            }
        }
    }
}

#[allow(clippy::type_complexity)] // only used one time
pub fn render(
    snippet: &Snippet<'_>,
    newline_with_offset: &str,
    include_placeholder: bool,
) -> (Tendril, Vec<SmallVec<[(usize, usize); 1]>>) {
    let mut insert = Tendril::new();
    let mut tabstops = Vec::new();
    let mut offset = 0;

    render_elements(
        &snippet.elements,
        &mut insert,
        &mut offset,
        &mut tabstops,
        newline_with_offset,
        include_placeholder,
    );

    // sort in ascending order (except for 0, which should always be the last one (per lsp doc))
    tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n });

    // merge tabstops with the same index (we take advantage of the fact that we just sorted them
    // above to simply look backwards)
    let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new();
    {
        let mut prev = None;
        for (tabstop, r) in tabstops {
            if prev == Some(tabstop) {
                let len_1 = ntabstops.len() - 1;
                ntabstops[len_1].push(r);
            } else {
                prev = Some(tabstop);
                ntabstops.push(smallvec![r]);
            }
        }
    }

    (insert, ntabstops)
}

mod parser {
    use helix_core::Tendril;
    use helix_parsec::*;

    use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement};

    /*
    https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax

        any         ::= tabstop | placeholder | choice | variable | text
        tabstop     ::= '$' int | '${' int '}'
        placeholder ::= '${' int ':' any '}'
        choice      ::= '${' int '|' text (',' text)* '|}'
        variable    ::= '$' var | '${' var }'
                        | '${' var ':' any '}'
                        | '${' var '/' regex '/' (format | text)+ '/' options '}'
        format      ::= '$' int | '${' int '}'
                        | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
                        | '${' int ':+' if '}'
                        | '${' int ':?' if ':' else '}'
                        | '${' int ':-' else '}' | '${' int ':' else '}'
        regex       ::= Regular Expression value (ctor-string)
        options     ::= Regular Expression option (ctor-options)
        var         ::= [_a-zA-Z] [_a-zA-Z0-9]*
        int         ::= [0-9]+
        text        ::= .*
        if          ::= text
        else        ::= text
    */

    fn var<'a>() -> impl Parser<'a, Output = &'a str> {
        // var = [_a-zA-Z][_a-zA-Z0-9]*
        move |input: &'a str| {
            input
                .char_indices()
                .take_while(|(p, c)| {
                    *c == '_'
                        || if *p == 0 {
                            c.is_ascii_alphabetic()
                        } else {
                            c.is_ascii_alphanumeric()
                        }
                })
                .last()
                .map(|(index, c)| {
                    let index = index + c.len_utf8();
                    (&input[index..], &input[0..index])
                })
                .ok_or(input)
        }
    }

    const TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$'];
    const CHOICE_TEXT_ESCAPE_CHARS: &[char] = &['\\', '|', ','];

    fn text<'a>(
        escape_chars: &'static [char],
        term_chars: &'static [char],
    ) -> impl Parser<'a, Output = Tendril> {
        move |input: &'a str| {
            let mut chars = input.char_indices().peekable();
            let mut res = Tendril::new();
            while let Some((i, c)) = chars.next() {
                match c {
                    '\\' => {
                        if let Some(&(_, c)) = chars.peek() {
                            if escape_chars.contains(&c) {
                                chars.next();
                                res.push(c);
                                continue;
                            }
                        }
                        res.push('\\');
                    }
                    c if term_chars.contains(&c) => return Ok((&input[i..], res)),
                    c => res.push(c),
                }
            }

            Ok(("", res))
        }
    }

    fn digit<'a>() -> impl Parser<'a, Output = usize> {
        filter_map(take_while(|c| c.is_ascii_digit()), |s| s.parse().ok())
    }

    fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> {
        use CaseChange::*;

        choice!(
            map("upcase", |_| Upcase),
            map("downcase", |_| Downcase),
            map("capitalize", |_| Capitalize),
        )
    }

    fn format<'a>() -> impl Parser<'a, Output = FormatItem> {
        use FormatItem::*;

        choice!(
            // '$' int
            map(right("$", digit()), Capture),
            // '${' int '}'
            map(seq!("${", digit(), "}"), |seq| Capture(seq.1)),
            // '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
            map(seq!("${", digit(), ":/", case_change(), "}"), |seq| {
                CaseChange(seq.1, seq.3)
            }),
            // '${' int ':+' if '}'
            map(
                seq!("${", digit(), ":+", text(TEXT_ESCAPE_CHARS, &['}']), "}"),
                |seq| { Conditional(seq.1, Some(seq.3), None) }
            ),
            // '${' int ':?' if ':' else '}'
            map(
                seq!(
                    "${",
                    digit(),
                    ":?",
                    text(TEXT_ESCAPE_CHARS, &[':']),
                    ":",
                    text(TEXT_ESCAPE_CHARS, &['}']),
                    "}"
                ),
                |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) }
            ),
            // '${' int ':-' else '}' | '${' int ':' else '}'
            map(
                seq!(
                    "${",
                    digit(),
                    ":",
                    optional("-"),
                    text(TEXT_ESCAPE_CHARS, &['}']),
                    "}"
                ),
                |seq| { Conditional(seq.1, None, Some(seq.4)) }
            ),
        )
    }

    fn regex<'a>() -> impl Parser<'a, Output = Regex> {
        map(
            seq!(
                "/",
                // TODO parse as ECMAScript and convert to rust regex
                text(&['/'], &['/']),
                "/",
                zero_or_more(choice!(
                    format(),
                    // text doesn't parse $, if format fails we just accept the $ as text
                    map("$", |_| FormatItem::Text("$".into())),
                    map(text(&['\\', '/'], &['/', '$']), FormatItem::Text),
                )),
                "/",
                // vscode really doesn't allow escaping } here
                // so it's impossible to write a regex escape containing a }
                // we can consider deviating here and allowing the escape
                text(&[], &['}']),
            ),
            |(_, value, _, replacement, _, options)| Regex {
                value,
                replacement,
                options,
            },
        )
    }

    fn tabstop<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
        map(
            or(
                right("$", digit()),
                map(seq!("${", digit(), "}"), |values| values.1),
            ),
            |digit| SnippetElement::Tabstop { tabstop: digit },
        )
    }

    fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
        map(
            seq!(
                "${",
                digit(),
                ":",
                // according to the grammar there is just a single anything here.
                // However in the prose it is explained that placeholders can be nested.
                // The example there contains both a placeholder text and a nested placeholder
                // which indicates a list. Looking at the VSCode sourcecode, the placeholder
                // is indeed parsed as zero_or_more so the grammar is simply incorrect here
                zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
                "}"
            ),
            |seq| SnippetElement::Placeholder {
                tabstop: seq.1,
                value: seq.3,
            },
        )
    }

    fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
        map(
            seq!(
                "${",
                digit(),
                "|",
                sep(text(CHOICE_TEXT_ESCAPE_CHARS, &['|', ',']), ","),
                "|}",
            ),
            |seq| SnippetElement::Choice {
                tabstop: seq.1,
                choices: seq.3,
            },
        )
    }

    fn variable<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
        choice!(
            // $var
            map(right("$", var()), |name| SnippetElement::Variable {
                name,
                default: None,
                regex: None,
            }),
            // ${var}
            map(seq!("${", var(), "}",), |values| SnippetElement::Variable {
                name: values.1,
                default: None,
                regex: None,
            }),
            // ${var:default}
            map(
                seq!(
                    "${",
                    var(),
                    ":",
                    zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
                    "}",
                ),
                |values| SnippetElement::Variable {
                    name: values.1,
                    default: Some(values.3),
                    regex: None,
                }
            ),
            // ${var/value/format/options}
            map(seq!("${", var(), regex(), "}"), |values| {
                SnippetElement::Variable {
                    name: values.1,
                    default: None,
                    regex: Some(values.2),
                }
            }),
        )
    }

    fn anything<'a>(
        escape_chars: &'static [char],
        end_at_brace: bool,
    ) -> impl Parser<'a, Output = SnippetElement<'a>> {
        let term_chars: &[_] = if end_at_brace { &['$', '}'] } else { &['$'] };
        move |input: &'a str| {
            let parser = choice!(
                tabstop(),
                placeholder(),
                choice(),
                variable(),
                map("$", |_| SnippetElement::Text("$".into())),
                map(text(escape_chars, term_chars), SnippetElement::Text),
            );
            parser.parse(input)
        }
    }

    fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
        map(one_or_more(anything(TEXT_ESCAPE_CHARS, false)), |parts| {
            Snippet { elements: parts }
        })
    }

    pub fn parse(s: &str) -> Result<Snippet, &str> {
        snippet().parse(s).and_then(|(remainder, snippet)| {
            if remainder.is_empty() {
                Ok(snippet)
            } else {
                Err(remainder)
            }
        })
    }

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

        #[test]
        fn empty_string_is_error() {
            assert_eq!(Err(""), parse(""));
        }

        #[test]
        fn parse_placeholders_in_function_call() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![
                        Text("match(".into()),
                        Placeholder {
                            tabstop: 1,
                            value: vec!(Text("Arg1".into())),
                        },
                        Text(")".into())
                    ]
                }),
                parse("match(${1:Arg1})")
            )
        }

        #[test]
        fn unterminated_placeholder() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![Text("match(".into()), Text("$".into()), Text("{1:)".into())]
                }),
                parse("match(${1:)")
            )
        }

        #[test]
        fn parse_empty_placeholder() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![
                        Text("match(".into()),
                        Placeholder {
                            tabstop: 1,
                            value: vec![],
                        },
                        Text(")".into())
                    ]
                }),
                parse("match(${1:})")
            )
        }

        #[test]
        fn parse_placeholders_in_statement() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![
                        Text("local ".into()),
                        Placeholder {
                            tabstop: 1,
                            value: vec!(Text("var".into())),
                        },
                        Text(" = ".into()),
                        Placeholder {
                            tabstop: 1,
                            value: vec!(Text("value".into())),
                        },
                    ]
                }),
                parse("local ${1:var} = ${1:value}")
            )
        }

        #[test]
        fn parse_tabstop_nested_in_placeholder() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![Placeholder {
                        tabstop: 1,
                        value: vec!(Text("var, ".into()), Tabstop { tabstop: 2 },),
                    },]
                }),
                parse("${1:var, $2}")
            )
        }

        #[test]
        fn parse_placeholder_nested_in_placeholder() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![Placeholder {
                        tabstop: 1,
                        value: vec!(
                            Text("foo ".into()),
                            Placeholder {
                                tabstop: 2,
                                value: vec!(Text("bar".into())),
                            },
                        ),
                    },]
                }),
                parse("${1:foo ${2:bar}}")
            )
        }

        #[test]
        fn parse_all() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![
                        Text("hello ".into()),
                        Tabstop { tabstop: 1 },
                        Tabstop { tabstop: 2 },
                        Text(" ".into()),
                        Choice {
                            tabstop: 1,
                            choices: vec!["one".into(), "two".into(), "three".into()]
                        },
                        Text(" ".into()),
                        Variable {
                            name: "name",
                            default: Some(vec![Text("foo".into())]),
                            regex: None
                        },
                        Text(" ".into()),
                        Variable {
                            name: "var",
                            default: None,
                            regex: None
                        },
                        Text(" ".into()),
                        Variable {
                            name: "TM",
                            default: None,
                            regex: None
                        },
                    ]
                }),
                parse("hello $1${2} ${1|one,two,three|} ${name:foo} $var $TM")
            );
        }

        #[test]
        fn regex_capture_replace() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![Variable {
                        name: "TM_FILENAME",
                        default: None,
                        regex: Some(Regex {
                            value: "(.*).+$".into(),
                            replacement: vec![FormatItem::Capture(1), FormatItem::Text("$".into())],
                            options: Tendril::new(),
                        }),
                    }]
                }),
                parse("${TM_FILENAME/(.*).+$/$1$/}")
            );
        }

        #[test]
        fn rust_macro() {
            assert_eq!(
                Ok(Snippet {
                    elements: vec![
                        Text("macro_rules! ".into()),
                        Tabstop { tabstop: 1 },
                        Text(" {\n    (".into()),
                        Tabstop { tabstop: 2 },
                        Text(") => {\n        ".into()),
                        Tabstop { tabstop: 0 },
                        Text("\n    };\n}".into())
                    ]
                }),
                parse("macro_rules! $1 {\n    ($2) => {\n        $0\n    };\n}")
            );
        }

        fn assert_text(snippet: &str, parsed_text: &str) {
            let res = parse(snippet).unwrap();
            let text = crate::snippet::render(&res, "\n", true).0;
            assert_eq!(text, parsed_text)
        }

        #[test]
        fn robust_parsing() {
            assert_text("$", "$");
            assert_text("\\\\$", "\\$");
            assert_text("{", "{");
            assert_text("\\}", "}");
            assert_text("\\abc", "\\abc");
            assert_text("foo${f:\\}}bar", "foo}bar");
            assert_text("\\{", "\\{");
            assert_text("I need \\\\\\$", "I need \\$");
            assert_text("\\", "\\");
            assert_text("\\{{", "\\{{");
            assert_text("{{", "{{");
            assert_text("{{dd", "{{dd");
            assert_text("}}", "}}");
            assert_text("ff}}", "ff}}");
            assert_text("farboo", "farboo");
            assert_text("far{{}}boo", "far{{}}boo");
            assert_text("far{{123}}boo", "far{{123}}boo");
            assert_text("far\\{{123}}boo", "far\\{{123}}boo");
            assert_text("far{{id:bern}}boo", "far{{id:bern}}boo");
            assert_text("far{{id:bern {{basel}}}}boo", "far{{id:bern {{basel}}}}boo");
            assert_text(
                "far{{id:bern {{id:basel}}}}boo",
                "far{{id:bern {{id:basel}}}}boo",
            );
            assert_text(
                "far{{id:bern {{id2:basel}}}}boo",
                "far{{id:bern {{id2:basel}}}}boo",
            );
            assert_text("${}$\\a\\$\\}\\\\", "${}$\\a$}\\");
            assert_text("farboo", "farboo");
            assert_text("far{{}}boo", "far{{}}boo");
            assert_text("far{{123}}boo", "far{{123}}boo");
            assert_text("far\\{{123}}boo", "far\\{{123}}boo");
            assert_text("far`123`boo", "far`123`boo");
            assert_text("far\\`123\\`boo", "far\\`123\\`boo");
            assert_text("\\$far-boo", "$far-boo");
        }

        fn assert_snippet(snippet: &str, expect: &[SnippetElement]) {
            let parsed_snippet = parse(snippet).unwrap();
            assert_eq!(parsed_snippet.elements, expect.to_owned())
        }

        #[test]
        fn parse_variable() {
            use SnippetElement::*;
            assert_snippet(
                "$far-boo",
                &[
                    Variable {
                        name: "far",
                        default: None,
                        regex: None,
                    },
                    Text("-boo".into()),
                ],
            );
            assert_snippet(
                "far$farboo",
                &[
                    Text("far".into()),
                    Variable {
                        name: "farboo",
                        regex: None,
                        default: None,
                    },
                ],
            );
            assert_snippet(
                "far${farboo}",
                &[
                    Text("far".into()),
                    Variable {
                        name: "farboo",
                        regex: None,
                        default: None,
                    },
                ],
            );
            assert_snippet("$123", &[Tabstop { tabstop: 123 }]);
            assert_snippet(
                "$farboo",
                &[Variable {
                    name: "farboo",
                    regex: None,
                    default: None,
                }],
            );
            assert_snippet(
                "$far12boo",
                &[Variable {
                    name: "far12boo",
                    regex: None,
                    default: None,
                }],
            );
            assert_snippet(
                "000_${far}_000",
                &[
                    Text("000_".into()),
                    Variable {
                        name: "far",
                        regex: None,
                        default: None,
                    },
                    Text("_000".into()),
                ],
            );
        }

        #[test]
        fn parse_variable_transform() {
            assert_snippet(
                "${foo///}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: Tendril::new(),
                        replacement: Vec::new(),
                        options: Tendril::new(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/regex/format/gmi}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: "regex".into(),
                        replacement: vec![FormatItem::Text("format".into())],
                        options: "gmi".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/([A-Z][a-z])/format/}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: "([A-Z][a-z])".into(),
                        replacement: vec![FormatItem::Text("format".into())],
                        options: Tendril::new(),
                    }),
                    default: None,
                }],
            );

            // invalid regex TODO: reneable tests once we actually parse this regex flavour
            // assert_text(
            //     "${foo/([A-Z][a-z])/format/GMI}",
            //     "${foo/([A-Z][a-z])/format/GMI}",
            // );
            // assert_text(
            //     "${foo/([A-Z][a-z])/format/funky}",
            //     "${foo/([A-Z][a-z])/format/funky}",
            // );
            // assert_text("${foo/([A-Z][a-z]/format/}", "${foo/([A-Z][a-z]/format/}");
            assert_text(
                "${foo/regex\\/format/options}",
                "${foo/regex\\/format/options}",
            );

            // tricky regex
            assert_snippet(
                "${foo/m\\/atch/$1/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: "m/atch".into(),
                        replacement: vec![FormatItem::Capture(1)],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );

            // incomplete
            assert_text("${foo///", "${foo///");
            assert_text("${foo/regex/format/options", "${foo/regex/format/options");

            // format string
            assert_snippet(
                "${foo/.*/${0:fooo}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![FormatItem::Conditional(0, None, Some("fooo".into()))],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/${1}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![FormatItem::Capture(1)],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/$1/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![FormatItem::Capture(1)],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/This-$1-encloses/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("This-".into()),
                            FormatItem::Capture(1),
                            FormatItem::Text("-encloses".into()),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/complex${1:else}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("complex".into()),
                            FormatItem::Conditional(1, None, Some("else".into())),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/complex${1:-else}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("complex".into()),
                            FormatItem::Conditional(1, None, Some("else".into())),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/complex${1:+if}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("complex".into()),
                            FormatItem::Conditional(1, Some("if".into()), None),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/complex${1:?if:else}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("complex".into()),
                            FormatItem::Conditional(1, Some("if".into()), Some("else".into())),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${foo/.*/complex${1:/upcase}/i}",
                &[Variable {
                    name: "foo",
                    regex: Some(Regex {
                        value: ".*".into(),
                        replacement: vec![
                            FormatItem::Text("complex".into()),
                            FormatItem::CaseChange(1, CaseChange::Upcase),
                        ],
                        options: "i".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${TM_DIRECTORY/src\\//$1/}",
                &[Variable {
                    name: "TM_DIRECTORY",
                    regex: Some(Regex {
                        value: "src/".into(),
                        replacement: vec![FormatItem::Capture(1)],
                        options: Tendril::new(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${TM_SELECTED_TEXT/a/\\/$1/g}",
                &[Variable {
                    name: "TM_SELECTED_TEXT",
                    regex: Some(Regex {
                        value: "a".into(),
                        replacement: vec![FormatItem::Text("/".into()), FormatItem::Capture(1)],
                        options: "g".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${TM_SELECTED_TEXT/a/in\\/$1ner/g}",
                &[Variable {
                    name: "TM_SELECTED_TEXT",
                    regex: Some(Regex {
                        value: "a".into(),
                        replacement: vec![
                            FormatItem::Text("in/".into()),
                            FormatItem::Capture(1),
                            FormatItem::Text("ner".into()),
                        ],
                        options: "g".into(),
                    }),
                    default: None,
                }],
            );
            assert_snippet(
                "${TM_SELECTED_TEXT/a/end\\//g}",
                &[Variable {
                    name: "TM_SELECTED_TEXT",
                    regex: Some(Regex {
                        value: "a".into(),
                        replacement: vec![FormatItem::Text("end/".into())],
                        options: "g".into(),
                    }),
                    default: None,
                }],
            );
        }
        // TODO port more tests from https://github.com/microsoft/vscode/blob/dce493cb6e36346ef2714e82c42ce14fc461b15c/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts
    }
}