use crate::parser::{
    query::{
        Definition, Directive, Document, FragmentDefinition, OperationDefinition, Selection,
        SelectionSet, Text, Type, TypeCondition, Value, VariableDefinition,
    },
    tokenizer::{Kind, Token, TokenStream},
};
use combine::StreamOnce;
use thiserror::Error;

/// Error minifying query
#[derive(Error, Debug)]
#[error("query minify error: {}", _0)]
pub struct MinifyError(String);

pub fn minify_query(source: &str) -> Result<String, MinifyError> {
    let mut bits: Vec<&str> = Vec::new();
    let mut stream = TokenStream::new(source);
    let mut prev_was_punctuator = false;

    loop {
        match stream.uncons() {
            Ok(x) => {
                let token: Token = x;
                let is_non_punctuator = token.kind != Kind::Punctuator;

                if prev_was_punctuator && is_non_punctuator {
                    bits.push(" ");
                }

                bits.push(token.value);
                prev_was_punctuator = is_non_punctuator;
            }
            Err(ref e) if e == &combine::easy::Error::end_of_input() => break,
            Err(e) => return Err(MinifyError(e.to_string())),
        }
    }

    Ok(bits.join(""))
}

/// Minify a document according to the same rules as `minify_query`
pub fn minify_query_document<'a, T: Text<'a>>(doc: &Document<'a, T>) -> String {
    let mut minifier = Minifier::new();
    minifier.write_document(doc);
    minifier.buffer
}

/// Minifier builds a minified GraphQL query string from a parsed AST.
///
/// This struct uses a single-pass traversal of the document AST, writing directly to a buffer
/// instead of creating intermediate string representations. This approach is significantly
/// faster than the `minify_query` approach which required converting the AST to a Display string first.
///
/// Key optimizations:
/// - Direct buffer writing avoids intermediate allocations
/// - Reusable buffers for number formatting (itoa, ryu) reduce allocation overhead
/// - Tracking `last_was_non_punctuator` allows spacing without post-processing
struct Minifier {
    /// Accumulates the minified output as we traverse the AST
    buffer: String,
    /// Current indentation level (in spaces). Used only for block string formatting.
    block_indent: u16,
    /// Tracks whether the last character written were non-punctuator tokens (identifiers, keywords).
    /// This is essential for maintaining valid GraphQL syntax: identifiers must be separated by spaces.
    /// For example: "query Foo" requires a space between "query" and "Foo", but "query{" doesn't.
    last_was_non_punctuator: bool,
    /// Reusable buffer for converting integers to strings using the `itoa` crate,
    /// that's optimized for fast, allocation-free integer formatting.
    int_buffer: itoa::Buffer,
    /// Reusable buffer for converting floats to strings using the `ryu` crate.
    floats_buffer: ryu::Buffer,
}

impl Minifier {
    fn new() -> Self {
        Self {
            // preallocate a buffer size to minimize reallocations,
            // most queries will fit within 1KB
            buffer: String::with_capacity(1024),
            last_was_non_punctuator: false,
            block_indent: 2,
            int_buffer: itoa::Buffer::new(),
            floats_buffer: ryu::Buffer::new(),
        }
    }

    /// Writes a non-punctuator token (identifier, keyword) to the output.
    ///
    /// We add a space before this token if the last thing written was also a non-punctuator.
    /// We mark ourselves as the last token written, so the next non-punctuator knows to add a space.
    #[inline(always)]
    fn write_non_punctuator(&mut self, s: &str) {
        if self.last_was_non_punctuator {
            self.buffer.push(' ');
        }
        self.buffer.push_str(s);
        self.last_was_non_punctuator = true;
    }

    /// Writes a punctuator token (operator) to the output without spacing logic.
    ///
    /// Punctuators are: {([:!$=@ and so on.
    /// These never need spaces around them in minified GraphQL (e.g., "a:b", not "a : b").
    ///
    /// We reset `last_was_non_punctuator` to false because punctuators don't require spacing before the next token.
    #[inline(always)]
    fn write_punctuator(&mut self, s: &str) {
        self.buffer.push_str(s);
        self.last_was_non_punctuator = false;
    }

    /// Writes a single-character punctuator to the output.
    ///
    /// This is a single-character variant of `write_punctuator` for better performance.
    /// Using `push(char)` is faster than `push_str()` for single characters.
    #[inline(always)]
    fn write_punctuator_char(&mut self, c: char) {
        self.buffer.push(c);
        self.last_was_non_punctuator = false;
    }

    /// Writes the top-level document by iterating through all definitions
    #[inline]
    fn write_document<'a, T: Text<'a>>(&mut self, doc: &Document<'a, T>) {
        for def in &doc.definitions {
            self.write_definition(def);
        }
    }

    #[inline]
    fn write_definition<'a, T: Text<'a>>(&mut self, def: &Definition<'a, T>) {
        match def {
            Definition::Operation(op) => self.write_operation(op),
            Definition::Fragment(frag) => self.write_fragment(frag),
        }
    }

    #[inline]
    fn write_operation<'a, T: Text<'a>>(&mut self, op: &OperationDefinition<'a, T>) {
        match op {
            OperationDefinition::SelectionSet(set) => self.write_selection_set(set),
            OperationDefinition::Query(q) => {
                self.write_non_punctuator("query");
                if let Some(ref name) = q.name {
                    self.write_non_punctuator(name.as_ref());
                }
                self.write_variable_definitions(&q.variable_definitions);
                self.write_directives(&q.directives);
                self.write_selection_set(&q.selection_set);
            }
            OperationDefinition::Mutation(m) => {
                self.write_non_punctuator("mutation");
                if let Some(ref name) = m.name {
                    self.write_non_punctuator(name.as_ref());
                }
                self.write_variable_definitions(&m.variable_definitions);
                self.write_directives(&m.directives);
                self.write_selection_set(&m.selection_set);
            }
            OperationDefinition::Subscription(s) => {
                self.write_non_punctuator("subscription");
                if let Some(ref name) = s.name {
                    self.write_non_punctuator(name.as_ref());
                }
                self.write_variable_definitions(&s.variable_definitions);
                self.write_directives(&s.directives);
                self.write_selection_set(&s.selection_set);
            }
        }
    }

    #[inline]
    fn write_fragment<'a, T: Text<'a>>(&mut self, frag: &FragmentDefinition<'a, T>) {
        self.write_non_punctuator("fragment");
        self.write_non_punctuator(frag.name.as_ref());
        self.write_type_condition(&frag.type_condition);
        self.write_directives(&frag.directives);
        self.write_selection_set(&frag.selection_set);
    }

    /// No separators between items,
    /// each item's trailing punctuation determines spacing.
    #[inline]
    fn write_selection_set<'a, T: Text<'a>>(&mut self, set: &SelectionSet<'a, T>) {
        self.write_punctuator_char('{');
        for item in &set.items {
            self.write_selection(item);
        }
        self.write_punctuator_char('}');
    }

    #[inline]
    fn write_selection<'a, T: Text<'a>>(&mut self, selection: &Selection<'a, T>) {
        match selection {
            Selection::Field(f) => {
                if let Some(ref alias) = f.alias {
                    self.write_non_punctuator(alias.as_ref());
                    self.write_punctuator_char(':');
                }
                self.write_non_punctuator(f.name.as_ref());
                self.write_arguments(&f.arguments);
                self.write_directives(&f.directives);
                if !f.selection_set.items.is_empty() {
                    self.write_selection_set(&f.selection_set);
                }
            }
            Selection::FragmentSpread(fs) => {
                self.write_punctuator("...");
                self.write_non_punctuator(fs.fragment_name.as_ref());
                self.write_directives(&fs.directives);
            }
            Selection::InlineFragment(ifrag) => {
                self.write_punctuator("...");
                if let Some(ref tc) = ifrag.type_condition {
                    self.write_type_condition(tc);
                }
                self.write_directives(&ifrag.directives);
                self.write_selection_set(&ifrag.selection_set);
            }
        }
    }

    #[inline]
    fn write_type_condition<'a, T: Text<'a>>(&mut self, tc: &TypeCondition<'a, T>) {
        match tc {
            TypeCondition::On(name) => {
                self.write_non_punctuator("on");
                self.write_non_punctuator(name.as_ref());
            }
        }
    }

    #[inline]
    fn write_variable_definitions<'a, T: Text<'a>>(&mut self, vars: &[VariableDefinition<'a, T>]) {
        if vars.is_empty() {
            return;
        }
        self.write_punctuator_char('(');
        for var in vars {
            self.write_punctuator_char('$');
            self.write_non_punctuator(var.name.as_ref());
            self.write_punctuator_char(':');
            self.write_type(&var.var_type);
            if let Some(ref def) = var.default_value {
                self.write_punctuator_char('=');
                self.write_value(def);
            }
        }
        self.write_punctuator_char(')');
    }

    #[inline]
    fn write_type<'a, T: Text<'a>>(&mut self, ty: &Type<'a, T>) {
        match ty {
            Type::NamedType(name) => self.write_non_punctuator(name.as_ref()),
            Type::ListType(inner) => {
                self.write_punctuator_char('[');
                self.write_type(inner);
                self.write_punctuator_char(']');
            }
            Type::NonNullType(inner) => {
                self.write_type(inner);
                self.write_punctuator_char('!');
            }
        }
    }

    #[inline]
    fn write_directives<'a, T: Text<'a>>(&mut self, dirs: &[Directive<'a, T>]) {
        for dir in dirs {
            self.write_punctuator_char('@');
            self.write_non_punctuator(dir.name.as_ref());
            self.write_arguments(&dir.arguments);
        }
    }

    #[inline]
    fn write_arguments<'a, T: Text<'a>>(&mut self, args: &[(T::Value, Value<'a, T>)]) {
        if args.is_empty() {
            return;
        }
        self.write_punctuator_char('(');
        for (name, val) in args {
            self.write_non_punctuator(name.as_ref());
            self.write_punctuator_char(':');
            self.write_value(val);
        }
        self.write_punctuator_char(')');
    }

    /// Writes a string value with proper escaping and formatting.
    ///
    /// This method handles three different cases to optimize for common scenarios:
    ///
    /// Case 1: Simple strings (the fast path)
    /// If the string contains no escaping-needed characters and no newlines, we write it
    /// directly with quotes. This is the most common case and avoids character-by-character scanning.
    ///
    /// Case 2: Strings needing escaping but no newlines
    /// We iterate through each character and escape special characters:
    /// - Control characters get special escapes
    /// - Quotes and backslashes: escaped with backslash
    /// - Regular characters: copied as-is
    ///
    /// Case 3: Block strings (multi-line with newlines)
    /// GraphQL supports block strings (triple quotes) for multi-line content.
    /// Block strings preserve line breaks, escape triple-quote sequences, and use indentation.
    /// We use this format if the string contains newlines.
    ///
    /// Spacing: Strings are non-punctuators that require spacing when preceded by another non-punctuator.
    /// We add the space at the beginning if needed, maintaining `last_was_non_punctuator = true`.
    #[inline(always)]
    pub fn write_quoted(&mut self, s: &str) {
        if self.last_was_non_punctuator {
            self.buffer.push(' ');
        }

        let bytes = s.as_bytes();
        let mut has_newline = false;
        let mut needs_escaping = false;

        for &byte in bytes {
            if byte == b'\n' {
                has_newline = true;
            // Check for control characters that need escaping. The parser accepts raw control characters in strings,
            // so we must escape them here to produce valid minified output.
            } else if byte == b'"' || byte == b'\\' || byte < 0x20 || byte == 0x7F {
                needs_escaping = true;
            }

            if has_newline && needs_escaping {
                break;
            }
        }

        if !needs_escaping && !has_newline {
            self.buffer.reserve(s.len() + 2);
            self.buffer.push('"');
            self.buffer.push_str(s);
            self.buffer.push('"');
            self.last_was_non_punctuator = true;
            return;
        }

        if !has_newline {
            use std::fmt::Write;
            // Reserve extra space for escape sequences. Most strings need 2 bytes for quotes,
            // and ~16 bytes accounts for typical escape sequences (e.g., \", \\, \u00XX).
            // The buffer will grow dynamically if this estimate is too small.
            self.buffer.reserve(s.len() + 16);
            self.buffer.push('"');
            for c in s.chars() {
                match c {
                    '\r' => self.buffer.push_str(r"\r"),
                    '\n' => self.buffer.push_str(r"\n"),
                    '\t' => self.buffer.push_str(r"\t"),
                    '"' => self.buffer.push_str("\\\""),
                    '\\' => self.buffer.push_str(r"\\"),
                    // Regular text characters. These are safe to write directly without escaping.
                    '\u{0020}'..='\u{FFFF}' => self.buffer.push(c),
                    // Characters outside the printable range are escaped as \uXXXX.
                    _ => write!(&mut self.buffer, "\\u{:04}", c as u32).unwrap(),
                }
            }
            self.buffer.push('"');
        } else {
            // Block strings can expand significantly with indentation and escape sequences.
            // Reserve space upfront to avoid repeated reallocations.
            self.buffer.reserve(s.len() + 32);
            self.buffer.push_str(r#"""""#);
            self.buffer.push('\n');

            self.block_indent += 2;

            for line in s.lines() {
                if !line.trim().is_empty() {
                    self.indent();
                    let mut last_pos = 0;
                    for (pos, _) in line.match_indices(r#"""""#) {
                        self.buffer.push_str(&line[last_pos..pos]);
                        self.buffer.push_str(r#"\"""#);
                        last_pos = pos + 3;
                    }
                    self.buffer.push_str(&line[last_pos..]);
                }
                self.buffer.push('\n');
            }

            self.block_indent -= 2;
            self.indent();

            self.buffer.push_str(r#"""""#);
        }
        self.last_was_non_punctuator = true;
    }

    /// Writes the current indentation level (in spaces).
    #[inline]
    pub fn indent(&mut self) {
        for _ in 0..self.block_indent {
            self.buffer.push(' ');
        }
    }

    #[inline]
    fn write_value<'a, T: Text<'a>>(&mut self, val: &Value<'a, T>) {
        match val {
            Value::Variable(name) => {
                self.write_punctuator_char('$');
                self.write_non_punctuator(name.as_ref());
            }
            Value::Int(n) => {
                // Use itoa's format method for fast, accurate integer-to-string conversion.
                // itoa is much faster than standard Rust integer formatting and produces
                // minimal output (no unnecessary padding or precision).
                let s = self.int_buffer.format(n.0);
                if self.last_was_non_punctuator {
                    self.buffer.push(' ');
                }
                self.buffer.push_str(s);
                self.last_was_non_punctuator = true;
            }
            Value::Float(f) => {
                // Use ryu's format method for fast, accurate float-to-string conversion.
                // ryu produces the shortest decimal representation that correctly round-trips,
                // which is more efficient than using Display or other methods.
                let s = self.floats_buffer.format(*f);
                if self.last_was_non_punctuator {
                    self.buffer.push(' ');
                }
                self.buffer.push_str(s);
                self.last_was_non_punctuator = true;
            }
            Value::String(s) => self.write_quoted(s),
            Value::Boolean(b) => self.write_non_punctuator(if *b { "true" } else { "false" }),
            Value::Null => self.write_non_punctuator("null"),
            Value::Enum(name) => self.write_non_punctuator(name.as_ref()),
            Value::List(items) => {
                self.write_punctuator_char('[');
                for item in items {
                    self.write_value(item);
                }
                self.write_punctuator_char(']');
            }
            Value::Object(fields) => {
                self.write_punctuator_char('{');
                for (name, val) in fields {
                    self.write_non_punctuator(name.as_ref());
                    self.write_punctuator_char(':');
                    self.write_value(val);
                }
                self.write_punctuator_char('}');
            }
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn strip_ignored_characters() {
        let source = "
        query SomeQuery($foo: String!, $bar: String) {
            someField(foo: $foo, bar: $bar) {
                a
                b {
                    ... on B {
                        c
                        d
                    }
                }
            }
        }
        ";

        let minified =
            super::minify_query(source.to_string().as_str()).expect("minification failed");

        assert_eq!(
            &minified,
            "query SomeQuery($foo:String!$bar:String){someField(foo:$foo bar:$bar){a b{...on B{c d}}}}"
        );
    }

    #[test]
    fn unexpected_token() {
        let source = "
        query foo {
            bar;
        }
        ";

        let minified = super::minify_query(source.to_string().as_str());

        assert!(minified.is_err());

        assert_eq!(
            minified.unwrap_err().to_string(),
            "query minify error: Unexpected unexpected character ';'"
        );
    }

    #[test]
    fn minify_document_test() {
        let source = "
        query SomeQuery($foo: String!, $bar: String) {
            someField(foo: $foo, bar: $bar) {
                a
                b {
                    ... on B {
                        c
                        d
                    }
                }
            }
        }
        ";

        let doc =
            crate::parser::query::grammar::parse_query::<String>(source).expect("parse failed");
        let minified_doc = super::minify_query_document(&doc);
        let minified_query = super::minify_query(source).expect("minification failed");

        assert_eq!(minified_doc, minified_query);
    }

    #[test]
    fn minify_document_complex() {
        let source = r#"
        mutation DoSomething($input: UpdateInput! = { a: 1, b: "foo" }) @opt(level: 1) {
            updateItem(id: "123", data: $input) {
                id
                ... on Item {
                    name
                    tags
                }
                ...FragmentName
            }
        }
        fragment FragmentName on Item {
            owner {
                id
                email
            }
        }
        "#;

        let doc =
            crate::parser::query::grammar::parse_query::<String>(source).expect("parse failed");
        let minified_doc = super::minify_query_document(&doc);
        let minified_query = super::minify_query(source).expect("minification failed");

        assert_eq!(minified_doc, minified_query);
    }

    #[test]
    fn test_minify_directive_args() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/directive_args.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        // In all the tests below, I first use snapshots to assert the output of `minify_query`,
        // and only then compare it with the output of `minify_query_document`.
        // While I could compare the outputs directly without snapshots,
        // this setup makes sure that future changes in either function are caught independently.
        insta::assert_snapshot!(minified_query, @r#"query{node@dir(a:1 b:"2" c:true d:false e:null)}"#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_directive_args_multiline() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/directive_args_multiline.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"query{node@dir(a:1 b:"2" c:true d:false e:null)}"#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_fragment() {
        let source = std::fs::read_to_string("src/parser/tests/queries/fragment.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"fragment frag on Friend{node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_fragment_spread() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/fragment_spread.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node{id...something}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_inline_fragment() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/inline_fragment.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node{id...on User{name}}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_inline_fragment_dir() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/inline_fragment_dir.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node{id...on User@defer{name}}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_kitchen_sink() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/kitchen-sink.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"
        query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{key:"value" block:"""

              block string uses \"""

          """})}{unnamed(truthy:true falsey:false nullish:null)query}
        "#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );

        insta::assert_snapshot!(minified_document, @r#"query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{key:"value" block:"block string uses \"\"\""})}{unnamed(truthy:true falsey:false nullish:null)query}"#);

        // The output is different because the parsed AST normalizes multiline strings,
        // while minify_query preserves the original formatting.
        // The minify_document has no idea about the original formatting.
    }

    #[test]
    fn test_minify_kitchen_sink_canonical() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/kitchen-sink_canonical.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"query queryName($foo:ComplexType$site:Site=MOBILE){whoever123is:node(id:[123 456]){id...on User@defer{field2{id alias:field1(first:10 after:$foo)@include(if:$foo){id...frag}}}...@skip(unless:$foo){id}...{id}}}mutation likeStory{like(story:123)@defer{story{id}}}subscription StoryLikeSubscription($input:StoryLikeSubscribeInput){storyLikeSubscribe(input:$input){story{likers{count}likeSentence{text}}}}fragment frag on Friend{foo(size:$size bar:$b obj:{block:"block string uses \"\"\"" key:"value"})}{unnamed(truthy:true falsey:false nullish:null)query}"#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_minimal() {
        let source = std::fs::read_to_string("src/parser/tests/queries/minimal.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"{a}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_minimal_mutation() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/minimal_mutation.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"mutation{notify}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_minimal_query() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/minimal_query.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_mutation_directive() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/mutation_directive.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"mutation@directive{node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_mutation_nameless_vars() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/mutation_nameless_vars.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"mutation($first:Int$second:Int){field1(first:$first)field2(second:$second)}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_named_query() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/named_query.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo{field}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_nested_selection() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/nested_selection.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node{id}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_aliases() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_aliases.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{an_alias:node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_arguments() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_arguments.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:1)}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_arguments_multiline() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_arguments_multiline.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:1)node(id:1 one:3)}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_array_argument_multiline() {
        let source = std::fs::read_to_string(
            "src/parser/tests/queries/query_array_argument_multiline.graphql",
        )
        .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:[5 6 7])}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_directive() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_directive.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query@directive{node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_list_argument() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_list_argument.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:1 list:[123 456])}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_nameless_vars() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_nameless_vars.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query($first:Int$second:Int){field1(first:$first)field2(second:$second)}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_nameless_vars_multiple_fields() {
        let source = std::fs::read_to_string(
            "src/parser/tests/queries/query_nameless_vars_multiple_fields.graphql",
        )
        .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query($houseId:String!$streetNumber:Int!){house(id:$houseId){id name lat lng}street(number:$streetNumber){id}houseStreet(id:$houseId number:$streetNumber){id}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_nameless_vars_multiple_fields_canonical() {
        let source = std::fs::read_to_string(
            "src/parser/tests/queries/query_nameless_vars_multiple_fields_canonical.graphql",
        )
        .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query($houseId:String!$streetNumber:Int!){house(id:$houseId){id name lat lng}street(number:$streetNumber){id}houseStreet(id:$houseId number:$streetNumber){id}}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );

        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_object_argument() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_object_argument.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:1 obj:{key1:123 key2:456})}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_object_argument_multiline() {
        let source = std::fs::read_to_string(
            "src/parser/tests/queries/query_object_argument_multiline.graphql",
        )
        .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query{node(id:1 obj:{key1:123 key2:456})}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_var_default_float() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_var_default_float.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo($site:Float=0.5){field}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_var_default_list() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_var_default_list.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo($site:[Int]=[123 456]){field}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_var_default_object() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_var_default_object.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo($site:Site={url:null}){field}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_var_default_string() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_var_default_string.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"query Foo($site:String="string"){field}"#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_query_var_defaults() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_var_defaults.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo($site:Site=MOBILE){field}");
    }

    #[test]
    fn test_minify_query_vars() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/query_vars.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"query Foo($arg:SomeType){field}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_string_literal() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/string_literal.graphql").unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"query{node(id:"hello")}"#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_subscription_directive() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/subscription_directive.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @"subscription@directive{node}");

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }

    #[test]
    fn test_minify_triple_quoted_literal() {
        let source =
            std::fs::read_to_string("src/parser/tests/queries/triple_quoted_literal.graphql")
                .unwrap();
        let minified_query = super::minify_query(&source).unwrap();
        insta::assert_snapshot!(minified_query, @r#"
        query{node(id:"""
            Hello,
              world!
          """)}
        "#);

        let minified_document = super::minify_query_document(
            &crate::parser::query::grammar::parse_query::<String>(&source).unwrap(),
        );
        assert_eq!(
            minified_query, minified_document,
            "minify_query and minify_document outputs differ"
        );
    }
}
