// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use indoc::indoc;

use crate::common::CommandOutput;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;

#[test]
fn test_templater_parse_error() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");
    let render = |template| get_template_output(&work_dir, "@-", template);

    // Typo
    test_env.add_config(
        r###"
    [template-aliases]
    'conflicting' = ''
    'shorted()' = ''
    'socat(x)' = 'x'
    'format_id(id)' = 'id.sort()'
    "###,
    );
    insta::assert_snapshot!(render(r#"conflicts"#), @"
    ------- stderr -------
    Error: Failed to parse template: Keyword `conflicts` doesn't exist
    Caused by:  --> 1:1
      |
    1 | conflicts
      | ^-------^
      |
      = Keyword `conflicts` doesn't exist
    Hint: Did you mean `conflict`, `conflicted_files`, `conflicting`?
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(render(r#"commit_id.shorter()"#), @"
    ------- stderr -------
    Error: Failed to parse template: Method `shorter` doesn't exist for type `CommitId`
    Caused by:  --> 1:11
      |
    1 | commit_id.shorter()
      |           ^-----^
      |
      = Method `shorter` doesn't exist for type `CommitId`
    Hint: Did you mean `short`, `shortest`?
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(render(r#"oncat()"#), @"
    ------- stderr -------
    Error: Failed to parse template: Function `oncat` doesn't exist
    Caused by:  --> 1:1
      |
    1 | oncat()
      | ^---^
      |
      = Function `oncat` doesn't exist
    Hint: Did you mean `concat`, `socat`?
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(render(r#""".lines().map(|s| se)"#), @r#"
    ------- stderr -------
    Error: Failed to parse template: Keyword `se` doesn't exist
    Caused by:  --> 1:20
      |
    1 | "".lines().map(|s| se)
      |                    ^^
      |
      = Keyword `se` doesn't exist
    Hint: Did you mean `s`, `self`?
    [EOF]
    [exit status: 1]
    "#);
    insta::assert_snapshot!(render(r#"format_id(commit_id)"#), @"
    ------- stderr -------
    Error: Failed to parse template: In alias `format_id(id)`
    Caused by:
    1:  --> 1:1
      |
    1 | format_id(commit_id)
      | ^------------------^
      |
      = In alias `format_id(id)`
    2:  --> 1:4
      |
    1 | id.sort()
      |    ^--^
      |
      = Method `sort` doesn't exist for type `CommitId`
    Hint: Did you mean `short`, `shortest`?
    [EOF]
    [exit status: 1]
    ");

    // Unexpanded identifier in string pattern
    insta::assert_snapshot!(render(r#"''.match(exact:foo)"#), @"
    ------- stderr -------
    Error: Failed to parse template: Expected string literal
    Caused by:  --> 1:16
      |
    1 | ''.match(exact:foo)
      |                ^-^
      |
      = Expected string literal
    [EOF]
    [exit status: 1]
    ");

    // "at least N arguments"
    insta::assert_snapshot!(render("separate()"), @"
    ------- stderr -------
    Error: Failed to parse template: Function `separate`: Expected at least 1 arguments
    Caused by:  --> 1:10
      |
    1 | separate()
      |          ^
      |
      = Function `separate`: Expected at least 1 arguments
    [EOF]
    [exit status: 1]
    ");

    // -Tbuiltin shows the predefined builtin_* aliases. This isn't 100%
    // guaranteed, but is nice.
    insta::assert_snapshot!(render(r#"builtin"#), @"
    ------- stderr -------
    Error: Failed to parse template: Keyword `builtin` doesn't exist
    Caused by:  --> 1:1
      |
    1 | builtin
      | ^-----^
      |
      = Keyword `builtin` doesn't exist
    Hint: Did you mean `builtin_config_list`, `builtin_config_list_detailed`, `builtin_draft_commit_description`, `builtin_draft_commit_description_with_diff`, `builtin_evolog_compact`, `builtin_log_comfortable`, `builtin_log_compact`, `builtin_log_compact_full_description`, `builtin_log_detailed`, `builtin_log_node`, `builtin_log_node_ascii`, `builtin_log_oneline`, `builtin_log_redacted`, `builtin_op_log_comfortable`, `builtin_op_log_compact`, `builtin_op_log_node`, `builtin_op_log_node_ascii`, `builtin_op_log_oneline`, `builtin_op_log_redacted`?
    [EOF]
    [exit status: 1]
    ");
}

#[test]
fn test_templater_parse_warning() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    let template = indoc! {r#"
        separate(' ',
          git_head,
          git_refs,
        )
    "#};
    let output = work_dir.run_jj(["log", "-r@", "-T", template]);
    insta::assert_snapshot!(output, @"
    @  false
    │
    ~
    [EOF]
    ------- stderr -------
    Warning: In template expression
     --> 2:3
      |
    2 |   git_head,
      |   ^------^
      |
      = commit.git_head() is deprecated; use .contained_in('first_parent(@)') instead
    Warning: In template expression
     --> 3:3
      |
    3 |   git_refs,
      |   ^------^
      |
      = commit.git_refs() is deprecated; use .remote_bookmarks()/tags() instead
    [EOF]
    ");
    let output = work_dir.run_jj(["op", "log", "-T", "tags"]);
    insta::assert_snapshot!(output, @"
    @
    ○
    [EOF]
    ------- stderr -------
    Warning: In template expression
     --> 1:1
      |
    1 | tags
      | ^--^
      |
      = operation.tags() is deprecated; use .attributes() instead
    [EOF]
    ");
}

#[test]
fn test_templater_upper_lower() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");
    let render = |template| get_colored_template_output(&work_dir, "@-", template);

    insta::assert_snapshot!(
        render(r#"change_id.shortest(4).upper() ++ change_id.shortest(4).upper().lower()"#),
        @"[1m[38;5;5mZ[0m[38;5;8mZZZ[1m[38;5;5mz[0m[38;5;8mzzz[39m[EOF]");
    insta::assert_snapshot!(
        render(r#""Hello".upper() ++ "Hello".lower()"#), @"HELLOhello[EOF]");
}

#[test]
fn test_templater_alias() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");
    let render = |template| get_template_output(&work_dir, "@-", template);

    test_env.add_config(
        r###"
    [template-aliases]
    'my_commit_id' = 'commit_id.short()'
    'syntax_error' = 'foo.'
    'name_error' = 'unknown_id'
    'recurse' = 'recurse1'
    'recurse1' = 'recurse2()'
    'recurse2()' = 'recurse'
    'identity(x)' = 'x'
    'coalesce2(x, y)' = 'if(x, x, y)'
    'deprecated()' = 'separate(" ", git_head, git_refs)'
    'format_commit_summary_with_refs(x, y)' = 'x.commit_id()'
    'builtin_log_node' = '"#"'
    'builtin_op_log_node' = '"#"'
    'builtin_log_node_ascii' = '"#"'
    'builtin_op_log_node_ascii' = '"#"'
    "###,
    );

    insta::assert_snapshot!(render("my_commit_id"), @"000000000000[EOF]");
    insta::assert_snapshot!(render("identity(my_commit_id)"), @"000000000000[EOF]");

    insta::assert_snapshot!(render("commit_id ++ syntax_error"), @"
    ------- stderr -------
    Error: Failed to parse template: In alias `syntax_error`
    Caused by:
    1:  --> 1:14
      |
    1 | commit_id ++ syntax_error
      |              ^----------^
      |
      = In alias `syntax_error`
    2:  --> 1:5
      |
    1 | foo.
      |     ^---
      |
      = expected <identifier>
    [EOF]
    [exit status: 1]
    ");

    insta::assert_snapshot!(render("commit_id ++ name_error"), @"
    ------- stderr -------
    Error: Failed to parse template: In alias `name_error`
    Caused by:
    1:  --> 1:14
      |
    1 | commit_id ++ name_error
      |              ^--------^
      |
      = In alias `name_error`
    2:  --> 1:1
      |
    1 | unknown_id
      | ^--------^
      |
      = Keyword `unknown_id` doesn't exist
    [EOF]
    [exit status: 1]
    ");

    insta::assert_snapshot!(render(r#"identity(identity(commit_id.short("")))"#), @r#"
    ------- stderr -------
    Error: Failed to parse template: In alias `identity(x)`
    Caused by:
    1:  --> 1:1
      |
    1 | identity(identity(commit_id.short("")))
      | ^-------------------------------------^
      |
      = In alias `identity(x)`
    2:  --> 1:1
      |
    1 | x
      | ^
      |
      = In function parameter `x`
    3:  --> 1:10
      |
    1 | identity(identity(commit_id.short("")))
      |          ^---------------------------^
      |
      = In alias `identity(x)`
    4:  --> 1:1
      |
    1 | x
      | ^
      |
      = In function parameter `x`
    5:  --> 1:35
      |
    1 | identity(identity(commit_id.short("")))
      |                                   ^^
      |
      = Expected expression of type `Integer`, but actual type is `String`
    [EOF]
    [exit status: 1]
    "#);

    insta::assert_snapshot!(render("commit_id ++ recurse"), @"
    ------- stderr -------
    Error: Failed to parse template: In alias `recurse`
    Caused by:
    1:  --> 1:14
      |
    1 | commit_id ++ recurse
      |              ^-----^
      |
      = In alias `recurse`
    2:  --> 1:1
      |
    1 | recurse1
      | ^------^
      |
      = In alias `recurse1`
    3:  --> 1:1
      |
    1 | recurse2()
      | ^--------^
      |
      = In alias `recurse2()`
    4:  --> 1:1
      |
    1 | recurse
      | ^-----^
      |
      = Alias `recurse` expanded recursively
    [EOF]
    [exit status: 1]
    ");

    insta::assert_snapshot!(render("identity()"), @"
    ------- stderr -------
    Error: Failed to parse template: Function `identity`: Expected 1 arguments
    Caused by:  --> 1:10
      |
    1 | identity()
      |          ^
      |
      = Function `identity`: Expected 1 arguments
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(render("identity(commit_id, commit_id)"), @"
    ------- stderr -------
    Error: Failed to parse template: Function `identity`: Expected 1 arguments
    Caused by:  --> 1:10
      |
    1 | identity(commit_id, commit_id)
      |          ^------------------^
      |
      = Function `identity`: Expected 1 arguments
    [EOF]
    [exit status: 1]
    ");

    insta::assert_snapshot!(render(r#"coalesce2(label("x", "not boolean"), "")"#), @r#"
    ------- stderr -------
    Error: Failed to parse template: In alias `coalesce2(x, y)`
    Caused by:
    1:  --> 1:1
      |
    1 | coalesce2(label("x", "not boolean"), "")
      | ^--------------------------------------^
      |
      = In alias `coalesce2(x, y)`
    2:  --> 1:4
      |
    1 | if(x, x, y)
      |    ^
      |
      = In function parameter `x`
    3:  --> 1:11
      |
    1 | coalesce2(label("x", "not boolean"), "")
      |           ^-----------------------^
      |
      = Expected expression of type `Boolean`, but actual type is `Template`
    [EOF]
    [exit status: 1]
    "#);

    insta::assert_snapshot!(render("(-my_commit_id)"), @"
    ------- stderr -------
    Error: Failed to parse template: In alias `my_commit_id`
    Caused by:
    1:  --> 1:3
      |
    1 | (-my_commit_id)
      |   ^----------^
      |
      = In alias `my_commit_id`
    2:  --> 1:1
      |
    1 | commit_id.short()
      | ^---------------^
      |
      = Expected expression of type `Integer`, but actual type is `String`
    [EOF]
    [exit status: 1]
    ");

    let output = work_dir.run_jj(["log", "-r@", "-Tdeprecated()"]);
    insta::assert_snapshot!(output, @r#"
    #  false
    │
    ~
    [EOF]
    ------- stderr -------
    Warning: In template expression
     --> 1:1
      |
    1 | deprecated()
      | ^----------^
      |
      = In alias `deprecated()`
     --> 1:15
      |
    1 | separate(" ", git_head, git_refs)
      |               ^------^
      |
      = commit.git_head() is deprecated; use .contained_in('first_parent(@)') instead
    Warning: In template expression
     --> 1:1
      |
    1 | deprecated()
      | ^----------^
      |
      = In alias `deprecated()`
     --> 1:25
      |
    1 | separate(" ", git_head, git_refs)
      |                         ^------^
      |
      = commit.git_refs() is deprecated; use .remote_bookmarks()/tags() instead
    [EOF]
    "#);
}

#[test]
fn test_templater_alias_override() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    test_env.add_config(
        r#"
    [template-aliases]
    'f(x)' = '"user"'
    "#,
    );

    // 'f(x)' should be overridden by --config 'f(a)'. If aliases were sorted
    // purely by name, 'f(a)' would come first.
    let output = work_dir.run_jj([
        "log",
        "--no-graph",
        "-r@",
        "-T",
        r#"f(_)"#,
        r#"--config=template-aliases.'f(a)'='"arg"'"#,
    ]);
    insta::assert_snapshot!(output, @"arg[EOF]");
}

#[test]
fn test_templater_bad_alias_decl() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    test_env.add_config(
        r###"
    [template-aliases]
    'badfn(a, a)' = 'a'
    'my_commit_id' = 'commit_id.short()'
    "###,
    );

    // Invalid declaration should be warned and ignored.
    let output = work_dir.run_jj(["log", "--no-graph", "-r@-", "-Tmy_commit_id"]);
    insta::assert_snapshot!(output, @"
    000000000000[EOF]
    ------- stderr -------
    Warning: Failed to load `template-aliases.badfn(a, a)`:  --> 1:7
      |
    1 | badfn(a, a)
      |       ^--^
      |
      = Redefinition of function parameter
    [EOF]
    ");
}

#[test]
fn test_templater_config_function() {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");
    let render = |template| get_template_output(&work_dir, "@-", template);

    insta::assert_snapshot!(
        render("config('user.name')"),
        @r#""Test User"[EOF]"#);
    insta::assert_snapshot!(
        render("config('user')"),
        @r#"{ email = "test.user@example.com", name = "Test User" }[EOF]"#);
    insta::assert_snapshot!(render("config('invalid name')"), @"
    ------- stderr -------
    Error: Failed to parse template: Failed to parse config name
    Caused by:
    1:  --> 1:8
      |
    1 | config('invalid name')
      |        ^------------^
      |
      = Failed to parse config name
    2: TOML parse error at line 1, column 9
      |
    1 | invalid name
      |         ^
    unexpected content, expected nothing

    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(render("config('unknown')"), @"");
}

#[must_use]
fn get_template_output(work_dir: &TestWorkDir, rev: &str, template: &str) -> CommandOutput {
    work_dir.run_jj(["log", "--no-graph", "-r", rev, "-T", template])
}

#[must_use]
fn get_colored_template_output(work_dir: &TestWorkDir, rev: &str, template: &str) -> CommandOutput {
    work_dir.run_jj([
        "log",
        "--color=always",
        "--no-graph",
        "-r",
        rev,
        "-T",
        template,
    ])
}
