use {super::*, serde::Serialize};

#[derive(Debug)]
struct Invocation<'src: 'run, 'run> {
  arguments: &'run [&'run str],
  recipe: &'run Recipe<'src>,
  settings: &'run Settings<'src>,
  scope: &'run Scope<'src, 'run>,
}

#[derive(Debug, PartialEq, Serialize)]
pub(crate) struct Justfile<'src> {
  pub(crate) aliases: Table<'src, Alias<'src>>,
  pub(crate) assignments: Table<'src, Assignment<'src>>,
  #[serde(rename = "first", serialize_with = "keyed::serialize_option")]
  pub(crate) default: Option<Rc<Recipe<'src>>>,
  #[serde(skip)]
  pub(crate) loaded: Vec<PathBuf>,
  pub(crate) modules: BTreeMap<String, Justfile<'src>>,
  pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
  pub(crate) settings: Settings<'src>,
  pub(crate) warnings: Vec<Warning>,
}

impl<'src> Justfile<'src> {
  pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
    let mut suggestions = self
      .recipes
      .keys()
      .map(|name| {
        (
          edit_distance(name, input),
          Suggestion { name, target: None },
        )
      })
      .chain(self.aliases.iter().map(|(name, alias)| {
        (
          edit_distance(name, input),
          Suggestion {
            name,
            target: Some(alias.target.name.lexeme()),
          },
        )
      }))
      .filter(|(distance, _suggestion)| distance < &3)
      .collect::<Vec<(usize, Suggestion)>>();
    suggestions.sort_by_key(|(distance, _suggestion)| *distance);

    suggestions
      .into_iter()
      .map(|(_distance, suggestion)| suggestion)
      .next()
  }

  pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
    let mut suggestions = self
      .assignments
      .keys()
      .map(|name| {
        (
          edit_distance(name, input),
          Suggestion { name, target: None },
        )
      })
      .filter(|(distance, _suggestion)| distance < &3)
      .collect::<Vec<(usize, Suggestion)>>();
    suggestions.sort_by_key(|(distance, _suggestion)| *distance);

    suggestions
      .into_iter()
      .map(|(_distance, suggestion)| suggestion)
      .next()
  }

  fn scope<'run>(
    &'run self,
    config: &'run Config,
    dotenv: &'run BTreeMap<String, String>,
    search: &'run Search,
    overrides: &BTreeMap<String, String>,
    parent: &'run Scope<'src, 'run>,
  ) -> RunResult<'src, Scope<'src, 'run>>
  where
    'src: 'run,
  {
    let mut scope = parent.child();
    let mut unknown_overrides = Vec::new();

    for (name, value) in overrides {
      if let Some(assignment) = self.assignments.get(name) {
        scope.bind(assignment.export, assignment.name, value.clone());
      } else {
        unknown_overrides.push(name.clone());
      }
    }

    if !unknown_overrides.is_empty() {
      return Err(Error::UnknownOverrides {
        overrides: unknown_overrides,
      });
    }

    Evaluator::evaluate_assignments(
      &self.assignments,
      config,
      dotenv,
      scope,
      &self.settings,
      search,
    )
  }

  pub(crate) fn run(
    &self,
    config: &Config,
    search: &Search,
    overrides: &BTreeMap<String, String>,
    arguments: &[String],
  ) -> RunResult<'src> {
    let unknown_overrides = overrides
      .keys()
      .filter(|name| !self.assignments.contains_key(name.as_str()))
      .cloned()
      .collect::<Vec<String>>();

    if !unknown_overrides.is_empty() {
      return Err(Error::UnknownOverrides {
        overrides: unknown_overrides,
      });
    }

    let dotenv = if config.load_dotenv {
      load_dotenv(config, &self.settings, &search.working_directory)?
    } else {
      BTreeMap::new()
    };

    let root = Scope::new();

    let scope = self.scope(config, &dotenv, search, overrides, &root)?;

    match &config.subcommand {
      Subcommand::Command {
        binary, arguments, ..
      } => {
        let mut command = if config.shell_command {
          let mut command = self.settings.shell_command(config);
          command.arg(binary);
          command
        } else {
          Command::new(binary)
        };

        command.args(arguments);

        command.current_dir(&search.working_directory);

        let scope = scope.child();

        command.export(&self.settings, &dotenv, &scope);

        let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
          Error::CommandInvoke {
            binary: binary.clone(),
            arguments: arguments.clone(),
            io_error,
          }
        })?;

        if !status.success() {
          return Err(Error::CommandStatus {
            binary: binary.clone(),
            arguments: arguments.clone(),
            status,
          });
        };

        return Ok(());
      }
      Subcommand::Evaluate { variable, .. } => {
        if let Some(variable) = variable {
          if let Some(value) = scope.value(variable) {
            print!("{value}");
          } else {
            return Err(Error::EvalUnknownVariable {
              suggestion: self.suggest_variable(variable),
              variable: variable.clone(),
            });
          }
        } else {
          let mut width = 0;

          for name in scope.names() {
            width = cmp::max(name.len(), width);
          }

          for binding in scope.bindings() {
            println!(
              "{0:1$} := \"{2}\"",
              binding.name.lexeme(),
              width,
              binding.value
            );
          }
        }

        return Ok(());
      }
      _ => {}
    }

    let argvec: Vec<&str> = if !arguments.is_empty() {
      arguments.iter().map(String::as_str).collect()
    } else if let Some(recipe) = &self.default {
      recipe.check_can_be_default_recipe()?;
      vec![recipe.name()]
    } else if self.recipes.is_empty() {
      return Err(Error::NoRecipes);
    } else {
      return Err(Error::NoDefaultRecipe);
    };

    let arguments = argvec.as_slice();

    let mut missing = Vec::new();
    let mut invocations = Vec::new();
    let mut remaining = arguments;
    let mut scopes = BTreeMap::new();
    let arena: Arena<Scope> = Arena::new();

    while let Some((first, mut rest)) = remaining.split_first() {
      if let Some((invocation, consumed)) = self.invocation(
        0,
        &mut Vec::new(),
        &arena,
        &mut scopes,
        config,
        &dotenv,
        search,
        &scope,
        first,
        rest,
      )? {
        rest = &rest[consumed..];
        invocations.push(invocation);
      } else {
        missing.push((*first).to_owned());
      }
      remaining = rest;
    }

    if !missing.is_empty() {
      let suggestion = if missing.len() == 1 {
        self.suggest_recipe(missing.first().unwrap())
      } else {
        None
      };
      return Err(Error::UnknownRecipes {
        recipes: missing,
        suggestion,
      });
    }

    let mut ran = BTreeSet::new();
    for invocation in invocations {
      let context = RecipeContext {
        settings: invocation.settings,
        config,
        scope: invocation.scope,
        search,
      };

      Self::run_recipe(
        &context,
        invocation.recipe,
        invocation.arguments,
        &dotenv,
        search,
        &mut ran,
      )?;
    }

    Ok(())
  }

  pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
    self.aliases.get(name)
  }

  pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
    self
      .recipes
      .get(name)
      .map(Rc::as_ref)
      .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
  }

  fn invocation<'run>(
    &'run self,
    depth: usize,
    path: &mut Vec<&'run str>,
    arena: &'run Arena<Scope<'src, 'run>>,
    scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
    config: &'run Config,
    dotenv: &'run BTreeMap<String, String>,
    search: &'run Search,
    parent: &'run Scope<'src, 'run>,
    first: &'run str,
    rest: &'run [&'run str],
  ) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
    if let Some(module) = self.modules.get(first) {
      path.push(first);

      let scope = if let Some(scope) = scopes.get(path) {
        scope
      } else {
        let scope = module.scope(config, dotenv, search, &BTreeMap::new(), parent)?;
        let scope = arena.alloc(scope);
        scopes.insert(path.clone(), scope);
        scopes.get(path).unwrap()
      };

      if rest.is_empty() {
        if let Some(recipe) = &module.default {
          recipe.check_can_be_default_recipe()?;
          return Ok(Some((
            Invocation {
              settings: &module.settings,
              recipe,
              arguments: &[],
              scope,
            },
            depth,
          )));
        }
        Err(Error::NoDefaultRecipe)
      } else {
        module.invocation(
          depth + 1,
          path,
          arena,
          scopes,
          config,
          dotenv,
          search,
          scope,
          rest[0],
          &rest[1..],
        )
      }
    } else if let Some(recipe) = self.get_recipe(first) {
      if recipe.parameters.is_empty() {
        Ok(Some((
          Invocation {
            arguments: &[],
            recipe,
            scope: parent,
            settings: &self.settings,
          },
          depth,
        )))
      } else {
        let argument_range = recipe.argument_range();
        let argument_count = cmp::min(rest.len(), recipe.max_arguments());
        if !argument_range.range_contains(&argument_count) {
          return Err(Error::ArgumentCountMismatch {
            recipe: recipe.name(),
            parameters: recipe.parameters.clone(),
            found: rest.len(),
            min: recipe.min_arguments(),
            max: recipe.max_arguments(),
          });
        }
        Ok(Some((
          Invocation {
            arguments: &rest[..argument_count],
            recipe,
            scope: parent,
            settings: &self.settings,
          },
          depth + argument_count,
        )))
      }
    } else {
      Ok(None)
    }
  }

  fn run_recipe(
    context: &RecipeContext<'src, '_>,
    recipe: &Recipe<'src>,
    arguments: &[&str],
    dotenv: &BTreeMap<String, String>,
    search: &Search,
    ran: &mut BTreeSet<Vec<String>>,
  ) -> RunResult<'src> {
    let mut invocation = vec![recipe.name().to_owned()];
    for argument in arguments {
      invocation.push((*argument).to_string());
    }

    if ran.contains(&invocation) {
      return Ok(());
    }

    if !context.config.yes && !recipe.confirm()? {
      return Err(Error::NotConfirmed {
        recipe: recipe.name(),
      });
    }

    let (outer, positional) = Evaluator::evaluate_parameters(
      context.config,
      dotenv,
      &recipe.parameters,
      arguments,
      context.scope,
      context.settings,
      search,
    )?;

    let scope = outer.child();

    let mut evaluator =
      Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);

    for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
      let arguments = arguments
        .iter()
        .map(|argument| evaluator.evaluate_expression(argument))
        .collect::<RunResult<Vec<String>>>()?;

      Self::run_recipe(
        context,
        recipe,
        &arguments.iter().map(String::as_ref).collect::<Vec<&str>>(),
        dotenv,
        search,
        ran,
      )?;
    }

    recipe.run(context, dotenv, scope.child(), search, &positional)?;

    {
      let mut ran = BTreeSet::new();

      for Dependency { recipe, arguments } in recipe.dependencies.iter().skip(recipe.priors) {
        let mut evaluated = Vec::new();

        for argument in arguments {
          evaluated.push(evaluator.evaluate_expression(argument)?);
        }

        Self::run_recipe(
          context,
          recipe,
          &evaluated.iter().map(String::as_ref).collect::<Vec<&str>>(),
          dotenv,
          search,
          &mut ran,
        )?;
      }
    }

    ran.insert(invocation);
    Ok(())
  }

  pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<'src, Dependency>> {
    let mut recipes = self
      .recipes
      .values()
      .map(AsRef::as_ref)
      .filter(|recipe| recipe.public())
      .collect::<Vec<&Recipe<Dependency>>>();

    if source_order {
      recipes.sort_by_key(|recipe| {
        (
          self
            .loaded
            .iter()
            .position(|path| path == recipe.name.path)
            .unwrap(),
          recipe.name.offset,
        )
      });
    }

    recipes
  }
}

impl<'src> ColorDisplay for Justfile<'src> {
  fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
    let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
    for (name, assignment) in &self.assignments {
      if assignment.export {
        write!(f, "export ")?;
      }
      write!(f, "{name} := {}", assignment.value)?;
      items -= 1;
      if items != 0 {
        write!(f, "\n\n")?;
      }
    }
    for alias in self.aliases.values() {
      write!(f, "{alias}")?;
      items -= 1;
      if items != 0 {
        write!(f, "\n\n")?;
      }
    }
    for recipe in self.recipes.values() {
      write!(f, "{}", recipe.color_display(color))?;
      items -= 1;
      if items != 0 {
        write!(f, "\n\n")?;
      }
    }
    Ok(())
  }
}

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

  use testing::compile;
  use Error::*;

  run_error! {
    name: unknown_recipes,
    src: "a:\nb:\nc:",
    args: ["a", "x", "y", "z"],
    error: UnknownRecipes {
      recipes,
      suggestion,
    },
    check: {
      assert_eq!(recipes, &["x", "y", "z"]);
      assert_eq!(suggestion, None);
    }
  }

  run_error! {
    name: unknown_recipes_show_alias_suggestion,
    src: "
      foo:
        echo foo

      alias z := foo
    ",
    args: ["zz"],
    error: UnknownRecipes {
      recipes,
      suggestion,
    },
    check: {
      assert_eq!(recipes, &["zz"]);
      assert_eq!(suggestion, Some(Suggestion {
        name: "z",
        target: Some("foo"),
      }
    ));
    }
  }

  // This test exists to make sure that shebang recipes run correctly.  Although
  // this script is still executed by a shell its behavior depends on the value of
  // a variable and continuing even though a command fails, whereas in plain
  // recipes variables are not available in subsequent lines and execution stops
  // when a line fails.
  run_error! {
    name: run_shebang,
    src: "
      a:
        #!/usr/bin/env sh
        code=200
          x() { return $code; }
            x
              x
    ",
    args: ["a"],
    error: Code {
      recipe,
      line_number,
      code,
      print_message,
    },
    check: {
      assert_eq!(recipe, "a");
      assert_eq!(code, 200);
      assert_eq!(line_number, None);
      assert!(print_message);
    }
  }

  run_error! {
    name: code_error,
    src: "
      fail:
        @exit 100
    ",
    args: ["fail"],
    error: Code {
      recipe,
      line_number,
      code,
      print_message,
    },
    check: {
      assert_eq!(recipe, "fail");
      assert_eq!(code, 100);
      assert_eq!(line_number, Some(2));
      assert!(print_message);
    }
  }

  run_error! {
    name: run_args,
    src: r#"
      a return code:
        @x() { {{return}} {{code + "0"}}; }; x
    "#,
    args: ["a", "return", "15"],
    error: Code {
      recipe,
      line_number,
      code,
      print_message,
    },
    check: {
      assert_eq!(recipe, "a");
      assert_eq!(code, 150);
      assert_eq!(line_number, Some(2));
      assert!(print_message);
    }
  }

  run_error! {
    name: missing_some_arguments,
    src: "a b c d:",
    args: ["a", "b", "c"],
    error: ArgumentCountMismatch {
      recipe,
      parameters,
      found,
      min,
      max,
    },
    check: {
      let param_names = parameters
        .iter()
        .map(|p| p.name.lexeme())
        .collect::<Vec<&str>>();
      assert_eq!(recipe, "a");
      assert_eq!(param_names, ["b", "c", "d"]);
      assert_eq!(found, 2);
      assert_eq!(min, 3);
      assert_eq!(max, 3);
    }
  }

  run_error! {
    name: missing_some_arguments_variadic,
    src: "a b c +d:",
    args: ["a", "B", "C"],
    error: ArgumentCountMismatch {
      recipe,
      parameters,
      found,
      min,
      max,
    },
    check: {
      let param_names = parameters
        .iter()
        .map(|p| p.name.lexeme())
        .collect::<Vec<&str>>();
      assert_eq!(recipe, "a");
      assert_eq!(param_names, ["b", "c", "d"]);
      assert_eq!(found, 2);
      assert_eq!(min, 3);
      assert_eq!(max, usize::MAX - 1);
    }
  }

  run_error! {
    name: missing_all_arguments,
    src: "a b c d:\n echo {{b}}{{c}}{{d}}",
    args: ["a"],
    error: ArgumentCountMismatch {
      recipe,
      parameters,
      found,
      min,
      max,
    },
    check: {
      let param_names = parameters
        .iter()
        .map(|p| p.name.lexeme())
        .collect::<Vec<&str>>();
      assert_eq!(recipe, "a");
      assert_eq!(param_names, ["b", "c", "d"]);
      assert_eq!(found, 0);
      assert_eq!(min, 3);
      assert_eq!(max, 3);
    }
  }

  run_error! {
    name: missing_some_defaults,
    src: "a b c d='hello':",
    args: ["a", "b"],
    error: ArgumentCountMismatch {
      recipe,
      parameters,
      found,
      min,
      max,
    },
    check: {
      let param_names = parameters
        .iter()
        .map(|p| p.name.lexeme())
        .collect::<Vec<&str>>();
      assert_eq!(recipe, "a");
      assert_eq!(param_names, ["b", "c", "d"]);
      assert_eq!(found, 1);
      assert_eq!(min, 2);
      assert_eq!(max, 3);
    }
  }

  run_error! {
    name: missing_all_defaults,
    src: "a b c='r' d='h':",
    args: ["a"],
    error: ArgumentCountMismatch {
      recipe,
      parameters,
      found,
      min,
      max,
    },
    check: {
      let param_names = parameters
        .iter()
        .map(|p| p.name.lexeme())
        .collect::<Vec<&str>>();
      assert_eq!(recipe, "a");
      assert_eq!(param_names, ["b", "c", "d"]);
      assert_eq!(found, 0);
      assert_eq!(min, 1);
      assert_eq!(max, 3);
    }
  }

  run_error! {
    name: unknown_overrides,
    src: "
      a:
       echo {{`f() { return 100; }; f`}}
    ",
    args: ["foo=bar", "baz=bob", "a"],
    error: UnknownOverrides { overrides },
    check: {
      assert_eq!(overrides, &["baz", "foo"]);
    }
  }

  run_error! {
    name: export_failure,
    src: r#"
      export foo := "a"
      baz := "c"
      export bar := "b"
      export abc := foo + bar + baz

      wut:
        echo $foo $bar $baz
    "#,
    args: ["--quiet", "wut"],
    error: Code {
      recipe,
      line_number,
      print_message,
      ..
    },
    check: {
      assert_eq!(recipe, "wut");
      assert_eq!(line_number, Some(7));
      assert!(print_message);
    }
  }

  fn case(input: &str, expected: &str) {
    let justfile = compile(input);
    let actual = format!("{}", justfile.color_display(Color::never()));
    assert_eq!(actual, expected);
    println!("Re-parsing...");
    let reparsed = compile(&actual);
    let redumped = format!("{}", reparsed.color_display(Color::never()));
    assert_eq!(redumped, actual);
  }

  #[test]
  fn parse_empty() {
    case(
      "

# hello


    ",
      "",
    );
  }

  #[test]
  fn parse_string_default() {
    case(
      r#"

foo a="b\t":


  "#,
      r#"foo a="b\t":"#,
    );
  }

  #[test]
  fn parse_multiple() {
    case(
      r"
a:
b:
", r"a:

b:",
    );
  }

  #[test]
  fn parse_variadic() {
    case(
      r"

foo +a:


  ",
      r"foo +a:",
    );
  }

  #[test]
  fn parse_variadic_string_default() {
    case(
      r#"

foo +a="Hello":


  "#,
      r#"foo +a="Hello":"#,
    );
  }

  #[test]
  fn parse_raw_string_default() {
    case(
      r"

foo a='b\t':


  ",
      r"foo a='b\t':",
    );
  }

  #[test]
  fn parse_export() {
    case(
      r#"
export a := "hello"

  "#,
      r#"export a := "hello""#,
    );
  }

  #[test]
  fn parse_alias_after_target() {
    case(
      r"
foo:
  echo a
alias f := foo
",
      r"alias f := foo

foo:
    echo a",
    );
  }

  #[test]
  fn parse_alias_before_target() {
    case(
      r"
alias f := foo
foo:
  echo a
",
      r"alias f := foo

foo:
    echo a",
    );
  }

  #[test]
  fn parse_alias_with_comment() {
    case(
      r"
alias f := foo #comment
foo:
  echo a
",
      r"alias f := foo

foo:
    echo a",
    );
  }

  #[test]
  fn parse_complex() {
    case(
      "
x:
y:
z:
foo := \"xx\"
bar := foo
goodbye := \"y\"
hello a b    c   : x y    z #hello
  #! blah
  #blarg
  {{ foo + bar}}abc{{ goodbye\t  + \"x\" }}xyz
  1
  2
  3
",
      "bar := foo

foo := \"xx\"

goodbye := \"y\"

hello a b c: x y z
    #! blah
    #blarg
    {{ foo + bar }}abc{{ goodbye + \"x\" }}xyz
    1
    2
    3

x:

y:

z:",
    );
  }

  #[test]
  fn parse_shebang() {
    case(
      "
practicum := 'hello'
install:
\t#!/bin/sh
\tif [[ -f {{practicum}} ]]; then
\t\treturn
\tfi
",
      "practicum := 'hello'

install:
    #!/bin/sh
    if [[ -f {{ practicum }} ]]; then
    \treturn
    fi",
    );
  }

  #[test]
  fn parse_simple_shebang() {
    case("a:\n #!\n  print(1)", "a:\n    #!\n     print(1)");
  }

  #[test]
  fn parse_assignments() {
    case(
      r#"a := "0"
c := a + b + a + b
b := "1"
"#,
      r#"a := "0"

b := "1"

c := a + b + a + b"#,
    );
  }

  #[test]
  fn parse_assignment_backticks() {
    case(
      "a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
      "a := `echo hello`

b := `echo goodbye`

c := a + b + a + b",
    );
  }

  #[test]
  fn parse_interpolation_backticks() {
    case(
      r#"a:
  echo {{  `echo hello` + "blarg"   }} {{   `echo bob`   }}"#,
      r#"a:
    echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
    );
  }

  #[test]
  fn eof_test() {
    case("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:");
  }

  #[test]
  fn string_quote_escape() {
    case(r#"a := "hello\"""#, r#"a := "hello\"""#);
  }

  #[test]
  fn string_escapes() {
    case(r#"a := "\n\t\r\"\\""#, r#"a := "\n\t\r\"\\""#);
  }

  #[test]
  fn parameters() {
    case(
      "a b c:
  {{b}} {{c}}",
      "a b c:
    {{ b }} {{ c }}",
    );
  }

  #[test]
  fn unary_functions() {
    case(
      "
x := arch()

a:
  {{os()}} {{os_family()}} {{num_cpus()}}",
      "x := arch()

a:
    {{ os() }} {{ os_family() }} {{ num_cpus() }}",
    );
  }

  #[test]
  fn env_functions() {
    case(
      r#"
x := env_var('foo',)

a:
  {{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
      r#"x := env_var('foo')

a:
    {{ env_var_or_default('foo' + 'bar', 'baz') }} {{ env_var(env_var("baz")) }}"#,
    );
  }

  #[test]
  fn parameter_default_string() {
    case(
      r#"
f x="abc":
"#,
      r#"f x="abc":"#,
    );
  }

  #[test]
  fn parameter_default_raw_string() {
    case(
      r"
f x='abc':
",
      r"f x='abc':",
    );
  }

  #[test]
  fn parameter_default_backtick() {
    case(
      r"
f x=`echo hello`:
",
      r"f x=`echo hello`:",
    );
  }

  #[test]
  fn parameter_default_concatenation_string() {
    case(
      r#"
f x=(`echo hello` + "foo"):
"#,
      r#"f x=(`echo hello` + "foo"):"#,
    );
  }

  #[test]
  fn parameter_default_concatenation_variable() {
    case(
      r#"
x := "10"
f y=(`echo hello` + x) +z="foo":
"#,
      r#"x := "10"

f y=(`echo hello` + x) +z="foo":"#,
    );
  }

  #[test]
  fn parameter_default_multiple() {
    case(
      r#"
x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):
"#,
      r#"x := "10"

f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
    );
  }

  #[test]
  fn concatenation_in_group() {
    case("x := ('0' + '1')", "x := ('0' + '1')");
  }

  #[test]
  fn string_in_group() {
    case("x := ('0'   )", "x := ('0')");
  }

  #[rustfmt::skip]
  #[test]
  fn escaped_dos_newlines() {
    case("@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
\t\tcargo test  --color always -- --color always 2>&1; \\\r
\t} | less\r
",
"@spam:
    { \\
    \tfiglet test; \\
    \tcargo build --color always 2>&1; \\
    \tcargo test  --color always -- --color always 2>&1; \\
    } | less");
  }
}
