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

type Scopes<'src, 'run> = BTreeMap<
  Modulepath,
  (
    &'run Justfile<'src>,
    &'run Scope<'src, 'run>,
    &'run BTreeMap<String, String>,
  ),
>;

#[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<Arc<Recipe<'src>>>,
  pub(crate) doc: Option<String>,
  #[serde(skip)]
  pub(crate) functions: Table<'src, FunctionDefinition<'src>>,
  pub(crate) groups: Vec<StringLiteral<'src>>,
  #[serde(skip)]
  pub(crate) loaded: Vec<PathBuf>,
  pub(crate) module_path: Modulepath,
  pub(crate) modules: Table<'src, Self>,
  #[serde(skip)]
  pub(crate) name: Option<Name<'src>>,
  #[serde(skip)]
  pub(crate) private: bool,
  pub(crate) recipes: Table<'src, Arc<Recipe<'src>>>,
  pub(crate) settings: Settings,
  pub(crate) source: PathBuf,
  pub(crate) unexports: HashSet<String>,
  #[serde(skip)]
  pub(crate) unstable_features: BTreeSet<UnstableFeature>,
  pub(crate) warnings: Vec<Warning>,
  #[serde(skip)]
  pub(crate) working_directory: PathBuf,
}

impl<'src> Justfile<'src> {
  fn find_suggestion(
    input: &str,
    candidates: impl Iterator<Item = Suggestion<'src>>,
  ) -> Option<Suggestion<'src>> {
    candidates
      .map(|suggestion| (strsim::levenshtein(input, suggestion.name), suggestion))
      .filter(|(distance, _suggestion)| *distance < 3)
      .min_by_key(|(distance, _suggestion)| *distance)
      .map(|(_distance, suggestion)| suggestion)
  }

  pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
    Self::find_suggestion(
      input,
      self
        .recipes
        .values()
        .filter(|recipe| recipe.is_public())
        .map(|recipe| Suggestion {
          name: recipe.name(),
          target: None,
        })
        .chain(
          self
            .aliases
            .values()
            .filter(|alias| alias.is_public())
            .map(|alias| Suggestion {
              name: alias.name.lexeme(),
              target: Some(alias.target.name.lexeme()),
            }),
        ),
    )
  }

  pub(crate) fn suggest_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
    Self::find_suggestion(
      input,
      self
        .modules
        .keys()
        .map(|name| Suggestion { name, target: None }),
    )
  }

  pub(crate) fn suggest_variable_or_submodule(&self, input: &str) -> Option<Suggestion<'src>> {
    Self::find_suggestion(
      input,
      self
        .assignments
        .keys()
        .chain(self.modules.keys())
        .map(|name| Suggestion { name, target: None }),
    )
  }

  fn evaluate_scopes<'run>(
    &'run self,
    config: &'run Config,
    dotenv_arena: &'run Arena<BTreeMap<String, String>>,
    overrides: &'run HashMap<Number, String>,
    parent_dotenv: Option<&'run BTreeMap<String, String>>,
    lazy: bool,
    root: &'run Scope<'src, 'run>,
    scope_arena: &'run Arena<Scope<'src, 'run>>,
    scopes: &mut Scopes<'src, 'run>,
    search: &'run Search,
    variable_references: &HashSet<Number>,
  ) -> RunResult<'src> {
    let dotenv = if config.load_dotenv {
      let working_directory = if self.is_submodule() {
        &self.working_directory
      } else {
        &search.working_directory
      };
      load_dotenv(config, &self.settings, working_directory)?
    } else {
      BTreeMap::new()
    };

    let dotenv = if let Some(parent_dotenv) = parent_dotenv {
      parent_dotenv
        .iter()
        .map(|(key, value)| (key.clone(), value.clone()))
        .chain(dotenv)
        .collect()
    } else {
      dotenv
    };

    let dotenv = dotenv_arena.alloc(dotenv);

    let lazy = lazy || self.settings.lazy;

    let scope = Evaluator::evaluate_assignments(
      config,
      dotenv,
      self,
      overrides,
      root,
      search,
      lazy.then_some(variable_references),
    )?;

    let scope = scope_arena.alloc(scope);
    scopes.insert(self.module_path.clone(), (self, scope, dotenv));

    for module in self.modules.values() {
      module.evaluate_scopes(
        config,
        dotenv_arena,
        overrides,
        Some(dotenv),
        lazy,
        scope,
        scope_arena,
        scopes,
        search,
        variable_references,
      )?;
    }

    Ok(())
  }

  pub(crate) fn run(
    &self,
    config: &Config,
    search: &Search,
    arguments: &[String],
    overrides: &HashMap<Number, String>,
  ) -> RunResult<'src> {
    let root = Scope::root();
    let dotenv_arena = Arena::new();
    let scope_arena = Arena::new();
    let mut scopes = BTreeMap::new();

    match &config.subcommand {
      Subcommand::Choose { .. } | Subcommand::Run { .. } => {
        let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();

        let invocations = InvocationParser::parse_invocations(self, &arguments)?;

        if config.one && invocations.len() > 1 {
          return Err(Error::ExcessInvocations {
            invocations: invocations.len(),
          });
        }

        let mut variable_references = HashSet::new();

        let mut stack = Vec::new();

        for invocation in &invocations {
          stack.push(invocation.recipe);
        }

        while let Some(recipe) = stack.pop() {
          variable_references.extend(&recipe.variable_references);
          for dependency in &recipe.dependencies {
            stack.push(&dependency.recipe);
          }
        }

        self.evaluate_scopes(
          config,
          &dotenv_arena,
          overrides,
          None,
          false,
          &root,
          &scope_arena,
          &mut scopes,
          search,
          &variable_references,
        )?;

        let ran = Ran::default();
        for invocation in invocations {
          Self::run_recipe(
            &invocation.arguments,
            config,
            false,
            overrides,
            &ran,
            invocation.recipe,
            &scopes,
            search,
          )?;
        }

        Ok(())
      }
      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::resolve(binary)
        };

        command
          .args(arguments)
          .current_dir(&search.working_directory);

        self.evaluate_scopes(
          config,
          &dotenv_arena,
          overrides,
          None,
          true,
          &root,
          &scope_arena,
          &mut scopes,
          search,
          &HashSet::new(),
        )?;

        let (_module, scope, dotenv) = scopes.get(&self.module_path).unwrap();
        let scope = scope.child();

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

        let (result, caught) = command.status_guard();

        let status = result.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,
          });
        }

        if let Some(signal) = caught {
          return Err(Error::Interrupted { signal });
        }

        Ok(())
      }
      Subcommand::Evaluate { format, path } => {
        let (module, variable, variable_references) = self.evaluation_target(path)?;

        self.evaluate_scopes(
          config,
          &dotenv_arena,
          overrides,
          None,
          true,
          &root,
          &scope_arena,
          &mut scopes,
          search,
          &variable_references,
        )?;

        let scope = scopes.get(&module.module_path).unwrap().1;

        if let Some(variable) = variable {
          print!("{}", scope.value(variable).unwrap());
        } else {
          let width = scope.names().fold(0, |max, name| name.len().max(max));

          for binding in scope.bindings() {
            if !binding.private {
              match format {
                EvaluateFormat::Just => {
                  println!("{0:1$} := \"{2}\"", binding.name, width, binding.value);
                }
                EvaluateFormat::Shell => {
                  if binding.export || module.settings.export {
                    print!("export ");
                  }
                  print!("{}=\"", binding.name.lexeme().replace('-', "_"));
                  for c in binding.value.chars() {
                    if matches!(c, '!' | '"' | '$' | '\\' | '`') {
                      print!("\\");
                    }
                    print!("{c}");
                  }
                  println!("\"");
                }
              }
            }
          }
        }

        Ok(())
      }
      _ => unreachable!(),
    }
  }

  pub(crate) fn evaluation_target<'a>(
    &'a self,
    path: &'a Modulepath,
  ) -> RunResult<'src, (&'a Justfile<'a>, Option<&'a str>, HashSet<Number>)> {
    let mut current = self;

    let mut variable = None;

    for (i, component) in path.components.iter().enumerate() {
      let last = i + 1 == path.components.len();

      if last && current.assignments.contains_key(component) {
        variable = Some(component.as_ref());
        break;
      }

      if let Some(module) = current.modules.get(component) {
        current = module;
      } else if last {
        return Err(Error::EvalUnknownSubmoduleOrVariable {
          suggestion: current.suggest_variable_or_submodule(component),
          component: component.into(),
        });
      } else {
        return Err(Error::EvalUnknownSubmodule {
          suggestion: current.suggest_submodule(component),
          component: component.into(),
        });
      }
    }

    let variable_references = if let Some(variable) = variable {
      HashSet::from([current.assignments.get(variable).unwrap().number])
    } else {
      current
        .assignments
        .values()
        .filter(|assignment| !assignment.private)
        .map(|assignment| assignment.number)
        .collect()
    };

    Ok((current, variable, variable_references))
  }

  pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
    if let Some(&unstable_feature) = self.unstable_features.first() {
      config.require_unstable(self, unstable_feature)?;
    }

    for module in self.modules.values() {
      module.check_unstable(config)?;
    }

    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(Arc::as_ref)
      .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
  }

  pub(crate) fn is_submodule(&self) -> bool {
    self.name.is_some()
  }

  pub(crate) fn name(&self) -> &'src str {
    self.name.map(|name| name.lexeme()).unwrap_or_default()
  }

  fn run_recipe(
    arguments: &[Vec<String>],
    config: &Config,
    is_dependency: bool,
    overrides: &HashMap<Number, String>,
    ran: &Ran,
    recipe: &Recipe<'src>,
    scopes: &Scopes<'src, '_>,
    search: &Search,
  ) -> RunResult<'src> {
    let mutex = ran.mutex(recipe, arguments);

    let mut guard = mutex.lock().unwrap();

    if *guard {
      return Ok(());
    }

    let (module, scope, dotenv) = scopes
      .get(recipe.module_path())
      .expect("failed to retrieve scope for module");

    let context = ExecutionContext {
      config,
      dotenv,
      module,
      overrides,
      search,
    };

    let (outer, positional, env) = Evaluator::evaluate_parameters(
      arguments,
      &context,
      is_dependency,
      &recipe.parameters,
      recipe,
      scope,
    )?;

    let scope = outer.child();

    let mut evaluator = Evaluator::new(&context, BTreeMap::new(), true, &scope);

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

    Self::run_dependencies(
      config,
      &context,
      recipe.priors(),
      &mut evaluator,
      overrides,
      ran,
      recipe,
      scopes,
      search,
    )?;

    recipe.run(&context, &env, is_dependency, &positional, &scope)?;

    Self::run_dependencies(
      config,
      &context,
      recipe.subsequents(),
      &mut evaluator,
      overrides,
      &Ran::default(),
      recipe,
      scopes,
      search,
    )?;

    *guard = true;

    Ok(())
  }

  fn run_dependencies<'run>(
    config: &Config,
    context: &ExecutionContext<'src, 'run>,
    dependencies: &[Dependency<'src>],
    evaluator: &mut Evaluator<'src, 'run>,
    overrides: &HashMap<Number, String>,
    ran: &Ran,
    recipe: &Recipe<'src>,
    scopes: &Scopes<'src, 'run>,
    search: &Search,
  ) -> RunResult<'src> {
    if context.config.no_dependencies {
      return Ok(());
    }

    let mut evaluated = Vec::new();
    for Dependency { recipe, arguments } in dependencies {
      let mut grouped = Vec::new();
      for group in arguments {
        let evaluated_group = group
          .iter()
          .map(|argument| evaluator.evaluate_expression(argument))
          .collect::<RunResult<Vec<String>>>()?;
        grouped.push(evaluated_group);
      }
      evaluated.push((recipe, grouped));
    }

    if recipe.is_parallel() {
      thread::scope::<_, RunResult>(|thread_scope| {
        let mut handles = Vec::new();
        for (recipe, arguments) in evaluated {
          handles.push(thread_scope.spawn(move || {
            Self::run_recipe(
              &arguments, config, true, overrides, ran, recipe, scopes, search,
            )
          }));
        }
        for handle in handles {
          handle
            .join()
            .map_err(|_| Error::internal("parallel dependency thread panicked"))??;
        }
        Ok(())
      })?;
    } else {
      for (recipe, arguments) in evaluated {
        Self::run_recipe(
          &arguments, config, true, overrides, ran, recipe, scopes, search,
        )?;
      }
    }

    Ok(())
  }

  pub(crate) fn public_modules(&self, config: &Config) -> Vec<&Justfile> {
    let mut modules = self
      .modules
      .values()
      .filter(|module| !module.private)
      .collect::<Vec<&Justfile>>();

    if config.unsorted {
      modules.sort_by_key(|module| {
        module
          .name
          .map(|name| name.token.offset)
          .unwrap_or_default()
      });
    }

    modules
  }

  pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {
    let mut recipes = self
      .recipes
      .values()
      .map(AsRef::as_ref)
      .filter(|recipe| recipe.is_public())
      .collect::<Vec<&Recipe>>();

    if config.unsorted {
      recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
    }

    recipes
  }

  pub(crate) fn public_recipes_recursive(&self, config: &Config) -> Vec<&Recipe> {
    let mut recipes = Vec::new();

    let mut stack = vec![self];
    while let Some(current) = stack.pop() {
      for recipe in current.public_recipes(config) {
        recipes.push(recipe);
      }

      for module in current.public_modules(config).into_iter().rev() {
        stack.push(module);
      }
    }

    recipes
  }

  pub(crate) fn public_aliases_recursive(&self, config: &Config) -> Vec<(&Alias, &Modulepath)> {
    let mut aliases = Vec::new();

    let mut stack = vec![self];
    while let Some(current) = stack.pop() {
      for alias in current.aliases.values() {
        if alias.is_public() {
          aliases.push((alias, &current.module_path));
        }
      }

      for module in current.public_modules(config).into_iter().rev() {
        stack.push(module);
      }
    }

    aliases
  }

  pub(crate) fn groups(&self) -> Vec<&str> {
    self
      .groups
      .iter()
      .map(|group| group.cooked.as_str())
      .collect()
  }

  pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {
    let mut groups = Vec::new();

    for recipe in self.recipes.values() {
      if recipe.is_public() {
        for group in recipe.groups() {
          groups.push((recipe.import_offsets.as_slice(), recipe.name.offset, group));
        }
      }
    }

    for submodule in self.public_modules(config) {
      for group in submodule.groups() {
        groups.push((&[], submodule.name.unwrap().offset, group.to_string()));
      }
    }

    if config.unsorted {
      groups.sort();
    } else {
      groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));
    }

    let mut seen = HashSet::new();

    groups.retain(|(_, _, group)| seen.insert(group.clone()));

    groups.into_iter().map(|(_, _, group)| group).collect()
  }
}

impl ColorDisplay for Justfile<'_> {
  fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
    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(())
  }
}

impl<'src> Keyed<'src> for Justfile<'src> {
  fn key(&self) -> &'src str {
    self.name()
  }
}

#[cfg(test)]
mod tests {
  use {super::*, Error::*, testing::compile};

  run_error! {
    name: unknown_recipe_no_suggestion,
    src: "a:\nb:\nc:",
    args: ["a", "xyz", "y", "z"],
    error: UnknownRecipe {
      recipe,
      suggestion,
    },
    check: {
      assert_eq!(recipe, "xyz");
      assert_eq!(suggestion, None);
    }
  }

  run_error! {
    name: unknown_recipe_with_suggestion,
    src: "a:\nb:\nc:",
    args: ["a", "x", "y", "z"],
    error: UnknownRecipe {
      recipe,
      suggestion,
    },
    check: {
      assert_eq!(recipe, "x");
      assert_eq!(suggestion, Some(Suggestion {
        name: "a",
        target: None,
      }));
    }
  }

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

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

  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: "
      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: PositionalArgumentCountMismatch {
      recipe,
      found,
      min,
      max,
    },
    check: {
      assert_eq!(recipe.name(), "a");
      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: PositionalArgumentCountMismatch {
      recipe,
      found,
      min,
      max,
    },
    check: {
      assert_eq!(recipe.name(), "a");
      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: PositionalArgumentCountMismatch {
      recipe,
      found,
      min,
      max,
    },
    check: {
      assert_eq!(recipe.name(), "a");
      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: PositionalArgumentCountMismatch {
      recipe,
      found,
      min,
      max,
    },
    check: {
      assert_eq!(recipe.name(), "a");
      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: PositionalArgumentCountMismatch {
      recipe,
      found,
      min,
      max,
    },
    check: {
      assert_eq!(recipe.name(), "a");
      assert_eq!(found, 0);
      assert_eq!(min, 1);
      assert_eq!(max, 3);
    }
  }

  run_error! {
    name: export_failure,
    src: "
      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);
    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(
      "
a:
b:
", "a:

b:",
    );
  }

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

foo +a:


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

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

foo +a='Hello':


  ",
      "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(
      "
export a := 'hello'

  ",
      "export a := 'hello'",
    );
  }

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

foo:
    echo a",
    );
  }

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

foo:
    echo a",
    );
  }

  #[test]
  fn parse_alias_with_comment() {
    case(
      "
alias f := foo # comment
foo:
  echo a
",
      "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(
      "a := '0'
c := a + b + a + b
b := '1'
",
      "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(
      "a:
  echo {{  `echo hello` + 'blarg'   }} {{   `echo bob`   }}",
      "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(
      "
x := env_var('foo',)

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

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

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

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

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

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

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

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

  #[test]
  fn parameter_default_multiple() {
    case(
      "
x := '10'
f y=(`echo hello` + x) +z=('foo' + 'bar'):
",
      "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");
  }
}
