// 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 common::{get_stdout_string, TestEnvironment};

pub mod common;

#[test]
fn test_log_with_empty_revision() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    let stderr = test_env.jj_cmd_cli_error(&repo_path, &["log", "-r="]);
    insta::assert_snapshot!(stderr, @r###"
    error: a value is required for '--revisions <REVISIONS>' but none was supplied

    For more information, try '--help'.
    "###);
}

#[test]
fn test_log_legacy_range_operator() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r=@:"]);
    insta::assert_snapshot!(stdout, @r###"
    @  qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ~
    "###);
    insta::assert_snapshot!(stderr, @r###"
    The `:` revset operator is deprecated. Please switch to `::`.
    "###);
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r=:@"]);
    insta::assert_snapshot!(stdout, @r###"
    @  qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ◉  zzzzzzzzzzzz 1970-01-01 00:00:00.000 +00:00 000000000000
       (empty) (no description set)
    "###);
    insta::assert_snapshot!(stderr, @r###"
    The `:` revset operator is deprecated. Please switch to `::`.
    "###);
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r=root:@"]);
    insta::assert_snapshot!(stdout, @r###"
    @  qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ◉  zzzzzzzzzzzz 1970-01-01 00:00:00.000 +00:00 000000000000
       (empty) (no description set)
    "###);
    insta::assert_snapshot!(stderr, @r###"
    The `:` revset operator is deprecated. Please switch to `::`.
    "###);
    let (stdout, stderr) = test_env.jj_cmd_ok(
        &repo_path,
        &["log", "-r=x", "--config-toml", "revset-aliases.x = '@:'"],
    );
    insta::assert_snapshot!(stdout, @r###"
    @  qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ~
    "###);
    insta::assert_snapshot!(stderr, @r###"
    The `:` revset operator is deprecated. Please switch to `::`.
    "###);
}

#[test]
fn test_log_with_or_without_diff() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "add a file"]);
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "a new commit"]);
    std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap();

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description"]);
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    ◉  add a file
    ◉
    "###);

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-p"]);
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  Modified regular file file1:
    │     1    1: foo
    │          2: bar
    ◉  add a file
    │  Added regular file file1:
    │          1: foo
    ◉
    "###);

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--no-graph"]);
    insta::assert_snapshot!(stdout, @r###"
    a new commit
    add a file
    "###);

    // `-p` for default diff output, `-s` for summary
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-p", "-s"]);
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  M file1
    │  Modified regular file file1:
    │     1    1: foo
    │          2: bar
    ◉  add a file
    │  A file1
    │  Added regular file file1:
    │          1: foo
    ◉
    "###);

    // `-s` for summary, `--git` for git diff (which implies `-p`)
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s", "--git"]);
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  M file1
    │  diff --git a/file1 b/file1
    │  index 257cc5642c...3bd1f0e297 100644
    │  --- a/file1
    │  +++ b/file1
    │  @@ -1,1 +1,2 @@
    │   foo
    │  +bar
    ◉  add a file
    │  A file1
    │  diff --git a/file1 b/file1
    │  new file mode 100644
    │  index 0000000000..257cc5642c
    │  --- /dev/null
    │  +++ b/file1
    │  @@ -1,0 +1,1 @@
    │  +foo
    ◉
    "###);

    // `-p` enables default "summary" output, so `-s` is noop
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "log",
            "-T",
            "description",
            "-p",
            "-s",
            "--config-toml=ui.diff.format='summary'",
        ],
    );
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  M file1
    ◉  add a file
    │  A file1
    ◉
    "###);

    // `-p` enables default "color-words" diff output, so `--color-words` is noop
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "-p", "--color-words"],
    );
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  Modified regular file file1:
    │     1    1: foo
    │          2: bar
    ◉  add a file
    │  Added regular file file1:
    │          1: foo
    ◉
    "###);

    // `--git` enables git diff, so `-p` is noop
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "--no-graph", "-p", "--git"],
    );
    insta::assert_snapshot!(stdout, @r###"
    a new commit
    diff --git a/file1 b/file1
    index 257cc5642c...3bd1f0e297 100644
    --- a/file1
    +++ b/file1
    @@ -1,1 +1,2 @@
     foo
    +bar
    add a file
    diff --git a/file1 b/file1
    new file mode 100644
    index 0000000000..257cc5642c
    --- /dev/null
    +++ b/file1
    @@ -1,0 +1,1 @@
    +foo
    "###);

    // Cannot use both `--git` and `--color-words`
    let stderr = test_env.jj_cmd_cli_error(
        &repo_path,
        &[
            "log",
            "-T",
            "description",
            "--no-graph",
            "-p",
            "--git",
            "--color-words",
        ],
    );
    insta::assert_snapshot!(stderr, @r###"
    error: the argument '--git' cannot be used with '--color-words'

    Usage: jj log --template <TEMPLATE> --no-graph --patch --git [PATHS]...

    For more information, try '--help'.
    "###);

    // `-s` with or without graph
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s"]);
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  M file1
    ◉  add a file
    │  A file1
    ◉
    "###);
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "--no-graph", "-s"],
    );
    insta::assert_snapshot!(stdout, @r###"
    a new commit
    M file1
    add a file
    A file1
    "###);

    // `--git` implies `-p`, with or without graph
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "-r", "@", "--git"],
    );
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  diff --git a/file1 b/file1
    ~  index 257cc5642c...3bd1f0e297 100644
       --- a/file1
       +++ b/file1
       @@ -1,1 +1,2 @@
        foo
       +bar
    "###);
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "-r", "@", "--no-graph", "--git"],
    );
    insta::assert_snapshot!(stdout, @r###"
    a new commit
    diff --git a/file1 b/file1
    index 257cc5642c...3bd1f0e297 100644
    --- a/file1
    +++ b/file1
    @@ -1,1 +1,2 @@
     foo
    +bar
    "###);

    // `--color-words` implies `-p`, with or without graph
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "-r", "@", "--color-words"],
    );
    insta::assert_snapshot!(stdout, @r###"
    @  a new commit
    │  Modified regular file file1:
    ~     1    1: foo
               2: bar
    "###);
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "log",
            "-T",
            "description",
            "-r",
            "@",
            "--no-graph",
            "--color-words",
        ],
    );
    insta::assert_snapshot!(stdout, @r###"
    a new commit
    Modified regular file file1:
       1    1: foo
            2: bar
    "###);
}

#[test]
fn test_log_shortest_accessors() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");
    let render = |rev, template| {
        test_env.jj_cmd_success(
            &repo_path,
            &["log", "--no-graph", "-r", rev, "-T", template],
        )
    };
    test_env.add_config(
        r###"
        [template-aliases]
        'format_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
        "###,
    );

    std::fs::write(repo_path.join("file"), "original file\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "initial"]);
    test_env.jj_cmd_success(&repo_path, &["branch", "c", "original"]);
    insta::assert_snapshot!(
        render("original", r#"format_id(change_id) ++ " " ++ format_id(commit_id)"#),
        @"q[pvuntsmwlqt] b[a1a30916d29]");

    // Create a chain of 10 commits
    for i in 1..10 {
        test_env.jj_cmd_success(&repo_path, &["new", "-m", &format!("commit{i}")]);
        std::fs::write(repo_path.join("file"), format!("file {i}\n")).unwrap();
    }
    // Create 2^3 duplicates of the chain
    for _ in 0..3 {
        test_env.jj_cmd_success(&repo_path, &["duplicate", "description(commit)"]);
    }

    insta::assert_snapshot!(
        render("original", r#"format_id(change_id) ++ " " ++ format_id(commit_id)"#),
        @"qpv[untsmwlqt] ba1[a30916d29]");

    insta::assert_snapshot!(
        render("::@", r#"change_id.shortest() ++ " " ++ commit_id.shortest() ++ "\n""#),
        @r###"
    wq 03
    km f7
    kp e7
    zn 38
    yo 0cf
    vr 9e
    yq 06
    ro 1f
    mz 7b
    qpv ba1
    zzz 00
    "###);

    insta::assert_snapshot!(
        render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
        @r###"
    wq[nwkozpkust] 03[f51310b83e]
    km[kuslswpqwq] f7[7fb1909080]
    kp[qxywonksrl] e7[15ad5db646]
    zn[kkpsqqskkl] 38[622e54e2e5]
    yo[stqsxwqrlt] 0cf[42f60199c]
    vr[uxwmqvtpmx] 9e[6015e4e622]
    yq[osqzytrlsw] 06[f34d9b1475]
    ro[yxmykxtrkr] 1f[99a5e19891]
    mz[vwutvlkqwt] 7b[1f7dee65b4]
    qpv[untsmwlqt] ba1[a30916d29]
    zzz[zzzzzzzzz] 00[0000000000]
    "###);

    // Can get shorter prefixes in configured revset
    test_env.add_config(r#"revsets.short-prefixes = "(@----):""#);
    insta::assert_snapshot!(
        render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
        @r###"
    w[qnwkozpkust] 03[f51310b83e]
    km[kuslswpqwq] f[77fb1909080]
    kp[qxywonksrl] e[715ad5db646]
    z[nkkpsqqskkl] 3[8622e54e2e5]
    y[ostqsxwqrlt] 0c[f42f60199c]
    vr[uxwmqvtpmx] 9e[6015e4e622]
    yq[osqzytrlsw] 06f[34d9b1475]
    ro[yxmykxtrkr] 1f[99a5e19891]
    mz[vwutvlkqwt] 7b[1f7dee65b4]
    qpv[untsmwlqt] ba1[a30916d29]
    zzz[zzzzzzzzz] 00[0000000000]
    "###);

    // Can disable short prefixes by setting to empty string
    test_env.add_config(r#"revsets.short-prefixes = """#);
    insta::assert_snapshot!(
        render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
        @r###"
    wq[nwkozpkust] 03[f51310b83e]
    km[kuslswpqwq] f7[7fb1909080]
    kp[qxywonksrl] e7[15ad5db646]
    zn[kkpsqqskkl] 38[622e54e2e5]
    yo[stqsxwqrlt] 0cf[42f60199c]
    vr[uxwmqvtpmx] 9e[6015e4e622]
    yq[osqzytrlsw] 06f[34d9b1475]
    ro[yxmykxtrkr] 1f[99a5e19891]
    mz[vwutvlkqwt] 7b[1f7dee65b4]
    qpv[untsmwlqt] ba1[a30916d29]
    zzz[zzzzzzzzz] 00[0000000000]
    "###);
}

#[test]
fn test_log_prefix_highlight_styled() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    fn prefix_format(len: Option<usize>) -> String {
        format!(
            r###"
            separate(" ",
              "Change",
              change_id.shortest({0}),
              description.first_line(),
              commit_id.shortest({0}),
              branches,
            )
            "###,
            len.map(|l| l.to_string()).unwrap_or_default()
        )
    }

    std::fs::write(repo_path.join("file"), "original file\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "initial"]);
    test_env.jj_cmd_success(&repo_path, &["branch", "c", "original"]);
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-r", "original", "-T", &prefix_format(Some(12))]),
        @r###"
    @  Change qpvuntsmwlqt initial ba1a30916d29 original
    │
    ~
    "###
    );

    // Create a chain of 10 commits
    for i in 1..10 {
        test_env.jj_cmd_success(&repo_path, &["new", "-m", &format!("commit{i}")]);
        std::fs::write(repo_path.join("file"), format!("file {i}\n")).unwrap();
    }
    // Create 2^3 duplicates of the chain
    for _ in 0..3 {
        test_env.jj_cmd_success(&repo_path, &["duplicate", "description(commit)"]);
    }

    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-r", "original", "-T", &prefix_format(Some(12))]),
        @r###"
    ◉  Change qpvuntsmwlqt initial ba1a30916d29 original
    │
    ~
    "###
    );
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "--color=always",
            "log",
            "-r",
            "@-----------..@",
            "-T",
            &prefix_format(Some(12)),
        ],
    );
    insta::assert_snapshot!(stdout,
        @r###"
    @  Change wqnwkozpkust commit9 03f51310b83e
    ◉  Change kmkuslswpqwq commit8 f77fb1909080
    ◉  Change kpqxywonksrl commit7 e715ad5db646
    ◉  Change znkkpsqqskkl commit6 38622e54e2e5
    ◉  Change yostqsxwqrlt commit5 0cf42f60199c
    ◉  Change vruxwmqvtpmx commit4 9e6015e4e622
    ◉  Change yqosqzytrlsw commit3 06f34d9b1475
    ◉  Change royxmykxtrkr commit2 1f99a5e19891
    ◉  Change mzvwutvlkqwt commit1 7b1f7dee65b4
    ◉  Change qpvuntsmwlqt initial ba1a30916d29 original
    ◉  Change zzzzzzzzzzzz 000000000000
    "###
    );
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "--color=always",
            "log",
            "-r",
            "@-----------..@",
            "-T",
            &prefix_format(Some(3)),
        ],
    );
    insta::assert_snapshot!(stdout,
        @r###"
    @  Change wqn commit9 03f
    ◉  Change kmk commit8 f77
    ◉  Change kpq commit7 e71
    ◉  Change znk commit6 386
    ◉  Change yos commit5 0cf
    ◉  Change vru commit4 9e6
    ◉  Change yqo commit3 06f
    ◉  Change roy commit2 1f9
    ◉  Change mzv commit1 7b1
    ◉  Change qpv initial ba1 original
    ◉  Change zzz 000
    "###
    );
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "--color=always",
            "log",
            "-r",
            "@-----------..@",
            "-T",
            &prefix_format(None),
        ],
    );
    insta::assert_snapshot!(stdout,
        @r###"
    @  Change wq commit9 03
    ◉  Change km commit8 f7
    ◉  Change kp commit7 e7
    ◉  Change zn commit6 38
    ◉  Change yo commit5 0cf
    ◉  Change vr commit4 9e
    ◉  Change yq commit3 06
    ◉  Change ro commit2 1f
    ◉  Change mz commit1 7b
    ◉  Change qpv initial ba1 original
    ◉  Change zzz 00
    "###
    );
}

#[test]
fn test_log_prefix_highlight_counts_hidden_commits() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");
    test_env.add_config(
        r###"
        [revsets]
        short-prefixes = "" # Disable short prefixes
        [template-aliases]
        'format_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
        "###,
    );

    let prefix_format = r###"
    separate(" ",
      "Change",
      format_id(change_id),
      description.first_line(),
      format_id(commit_id),
      branches,
    )
    "###;

    std::fs::write(repo_path.join("file"), "original file\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "initial"]);
    test_env.jj_cmd_success(&repo_path, &["branch", "c", "original"]);
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", prefix_format]),
        @r###"
    @  Change q[pvuntsmwlqt] initial b[a1a30916d29] original
    ◉  Change z[zzzzzzzzzzz] 0[00000000000]
    "###
    );

    // Create 2^7 hidden commits
    test_env.jj_cmd_success(&repo_path, &["new", "root", "-m", "extra"]);
    for _ in 0..7 {
        test_env.jj_cmd_success(&repo_path, &["duplicate", "description(extra)"]);
    }
    test_env.jj_cmd_success(&repo_path, &["abandon", "description(extra)"]);

    // The unique prefixes became longer.
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", prefix_format]),
        @r###"
    @  Change w[qnwkozpkust] 44[4c3c5066d3]
    │ ◉  Change q[pvuntsmwlqt] initial ba[1a30916d29] original
    ├─╯
    ◉  Change z[zzzzzzzzzzz] 00[0000000000]
    "###
    );
    insta::assert_snapshot!(
        test_env.jj_cmd_failure(&repo_path, &["log", "-r", "4", "-T", prefix_format]),
        @r###"
    Error: Commit ID prefix "4" is ambiguous
    "###
    );
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-r", "44", "-T", prefix_format]),
        @r###"
    @  Change w[qnwkozpkust] 44[4c3c5066d3]
    │
    ~
    "###
    );
}

#[test]
fn test_log_shortest_length_parameter() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id.shortest(0)"]), @r###"
    @  2
    ◉  0
    "###);
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", "commit_id.shortest(100)"]), @r###"
    @  230dd059e1b059aefc0da06a2e5a7dbf22362f22
    ◉  0000000000000000000000000000000000000000
    "###);
}

#[test]
fn test_log_author_format() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "--revisions=@"]),
        @r###"
    @  qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ~
    "###
    );

    let decl = "template-aliases.'format_short_signature(signature)'";
    insta::assert_snapshot!(
        test_env.jj_cmd_success(
            &repo_path,
            &[
                "--config-toml",
                &format!("{decl}='signature.username()'"),
                "log",
                "--revisions=@",
            ],
        ),
        @r###"
    @  qpvuntsmwlqt test.user 2001-02-03 04:05:07.000 +07:00 230dd059e1b0
    │  (empty) (no description set)
    ~
    "###
    );
}

#[test]
fn test_log_divergence() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");
    let template = r#"description.first_line() ++ if(divergent, " !divergence!")"#;

    std::fs::write(repo_path.join("file"), "foo\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "description 1"]);
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
    // No divergence
    insta::assert_snapshot!(stdout, @r###"
    @  description 1
    ◉
    "###);

    // Create divergence
    test_env.jj_cmd_success(
        &repo_path,
        &["describe", "-m", "description 2", "--at-operation", "@-"],
    );
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
    insta::assert_snapshot!(stdout, @r###"
    Concurrent modification detected, resolving automatically.
    ◉  description 2 !divergence!
    │ @  description 1 !divergence!
    ├─╯
    ◉
    "###);
}

#[test]
fn test_log_reversed() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "first"]);
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "second"]);

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--reversed"]);
    insta::assert_snapshot!(stdout, @r###"
    ◉
    ◉  first
    @  second
    "###);

    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "--reversed", "--no-graph"],
    );
    insta::assert_snapshot!(stdout, @r###"
    first
    second
    "###);
}

#[test]
fn test_log_filtered_by_path() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "first"]);
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "second"]);
    std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap();
    std::fs::write(repo_path.join("file2"), "baz\n").unwrap();

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "file1"]);
    insta::assert_snapshot!(stdout, @r###"
    @  second
    ◉  first
    │
    ~
    "###);

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "file2"]);
    insta::assert_snapshot!(stdout, @r###"
    @  second
    │
    ~
    "###);

    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s", "file1"]);
    insta::assert_snapshot!(stdout, @r###"
    @  second
    │  M file1
    ◉  first
    │  A file1
    ~
    "###);

    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &["log", "-T", "description", "-s", "file2", "--no-graph"],
    );
    insta::assert_snapshot!(stdout, @r###"
    second
    A file2
    "###);

    // file() revset doesn't filter the diff.
    let stdout = test_env.jj_cmd_success(
        &repo_path,
        &[
            "log",
            "-T",
            "description",
            "-s",
            "-rfile(file2)",
            "--no-graph",
        ],
    );
    insta::assert_snapshot!(stdout, @r###"
    second
    M file1
    A file2
    "###);
}

#[test]
fn test_log_warn_path_might_be_revset() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    std::fs::write(repo_path.join("file1"), "foo\n").unwrap();

    // Don't warn if the file actually exists.
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "file1", "-T", "description"]);
    insta::assert_snapshot!(stdout, @r###"
    @
    │
    ~
    "###);
    insta::assert_snapshot!(stderr, @"");

    // Warn for `jj log .` specifically, for former Mercurial users.
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", ".", "-T", "description"]);
    insta::assert_snapshot!(stdout, @r###"
    @
    │
    ~
    "###);
    insta::assert_snapshot!(stderr, @r###"
    warning: The argument "." is being interpreted as a path, but this is often not useful because all non-empty commits touch '.'.  If you meant to show the working copy commit, pass -r '@' instead.
    "###);

    // ...but checking `jj log .` makes sense in a subdirectory.
    let subdir = repo_path.join("dir");
    std::fs::create_dir_all(&subdir).unwrap();
    let (stdout, stderr) = test_env.jj_cmd_ok(&subdir, &["log", "."]);
    insta::assert_snapshot!(stdout, @"");
    insta::assert_snapshot!(stderr, @"");

    // Warn for `jj log @` instead of `jj log -r @`.
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "@", "-T", "description"]);
    insta::assert_snapshot!(stdout, @"");
    insta::assert_snapshot!(stderr, @r###"
    warning: The argument "@" is being interpreted as a path. To specify a revset, pass -r "@" instead.
    "###);

    // Warn when there's no path with the provided name.
    let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "file2", "-T", "description"]);
    insta::assert_snapshot!(stdout, @"");
    insta::assert_snapshot!(stderr, @r###"
    warning: The argument "file2" is being interpreted as a path. To specify a revset, pass -r "file2" instead.
    "###);

    // If an explicit revision is provided, then suppress the warning.
    let (stdout, stderr) =
        test_env.jj_cmd_ok(&repo_path, &["log", "@", "-r", "@", "-T", "description"]);
    insta::assert_snapshot!(stdout, @"");
    insta::assert_snapshot!(stderr, @r###"
    "###);
}

#[test]
fn test_default_revset() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "add a file"]);

    // Set configuration to only show the root commit.
    test_env.add_config(r#"revsets.log = "root""#);

    // Log should only contain one line (for the root commit), and not show the
    // commit created above.
    assert_eq!(
        1,
        test_env
            .jj_cmd_success(&repo_path, &["log", "-T", "commit_id"])
            .lines()
            .count()
    );
}

#[test]
fn test_default_revset_per_repo() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "add a file"]);

    // Set configuration to only show the root commit.
    std::fs::write(
        repo_path.join(".jj/repo/config.toml"),
        r#"revsets.log = "root""#,
    )
    .unwrap();

    // Log should only contain one line (for the root commit), and not show the
    // commit created above.
    assert_eq!(
        1,
        test_env
            .jj_cmd_success(&repo_path, &["log", "-T", "commit_id"])
            .lines()
            .count()
    );
}

#[test]
fn test_multiple_revsets() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");
    for name in ["foo", "bar", "baz"] {
        test_env.jj_cmd_success(&repo_path, &["new", "-m", name]);
        test_env.jj_cmd_success(&repo_path, &["branch", "set", name]);
    }

    // Default revset should be overridden if one or more -r options are specified.
    test_env.add_config(r#"revsets.log = "root""#);

    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo"]),
        @r###"
    ◉  foo
    │
    ~
    "###);
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo", "-rbar", "-rbaz"]),
        @r###"
    @  baz
    ◉  bar
    ◉  foo
    │
    ~
    "###);
    insta::assert_snapshot!(
        test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo", "-rfoo"]),
        @r###"
    ◉  foo
    │
    ~
    "###);
}

#[test]
fn test_graph_template_color() {
    // Test that color codes from a multi-line template don't span the graph lines.
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    test_env.jj_cmd_success(
        &repo_path,
        &["describe", "-m", "first line\nsecond line\nthird line"],
    );
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "single line"]);

    test_env.add_config(
        r#"[colors]
        description = "red"
        "working_copy description" = "green"
        "#,
    );

    // First test without color for comparison
    let template = r#"label(if(current_working_copy, "working_copy"), description)"#;
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
    insta::assert_snapshot!(stdout, @r###"
    @  single line
    ◉  first line
    │  second line
    │  third line
    ◉
    "###);
    let stdout = test_env.jj_cmd_success(&repo_path, &["--color=always", "log", "-T", template]);
    insta::assert_snapshot!(stdout, @r###"
    @  single line
    ◉  first line
    │  second line
    │  third line
    ◉
    "###);
}

#[test]
fn test_graph_styles() {
    // Test that different graph styles are available.
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");

    test_env.jj_cmd_success(&repo_path, &["commit", "-m", "initial"]);
    test_env.jj_cmd_success(&repo_path, &["commit", "-m", "main branch 1"]);
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "main branch 2"]);
    test_env.jj_cmd_success(
        &repo_path,
        &["new", "-m", "side branch\nwith\nlong\ndescription"],
    );
    test_env.jj_cmd_success(
        &repo_path,
        &["new", "-m", "merge", r#"description("main branch 1")"#, "@"],
    );

    // Default (legacy) style
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
    insta::assert_snapshot!(stdout, @r###"
    @    merge
    ├─╮
    │ ◉  side branch
    │ │  with
    │ │  long
    │ │  description
    │ ◉  main branch 2
    ├─╯
    ◉  main branch 1
    ◉  initial
    ◉
    "###);

    // ASCII style
    test_env.add_config(r#"ui.graph.style = "ascii""#);
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
    insta::assert_snapshot!(stdout, @r###"
    @    merge
    |\
    | o  side branch
    | |  with
    | |  long
    | |  description
    | o  main branch 2
    |/
    o  main branch 1
    o  initial
    o
    "###);

    // Large ASCII style
    test_env.add_config(r#"ui.graph.style = "ascii-large""#);
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
    insta::assert_snapshot!(stdout, @r###"
    @     merge
    |\
    | \
    |  o  side branch
    |  |  with
    |  |  long
    |  |  description
    |  o  main branch 2
    | /
    |/
    o  main branch 1
    o  initial
    o
    "###);

    // Curved style
    test_env.add_config(r#"ui.graph.style = "curved""#);
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
    insta::assert_snapshot!(stdout, @r###"
    @    merge
    ├─╮
    │ ◉  side branch
    │ │  with
    │ │  long
    │ │  description
    │ ◉  main branch 2
    ├─╯
    ◉  main branch 1
    ◉  initial
    ◉
    "###);

    // Square style
    test_env.add_config(r#"ui.graph.style = "square""#);
    let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
    insta::assert_snapshot!(stdout, @r###"
    @    merge
    ├─┐
    │ ◉  side branch
    │ │  with
    │ │  long
    │ │  description
    │ ◉  main branch 2
    ├─┘
    ◉  main branch 1
    ◉  initial
    ◉
    "###);
}

#[test]
fn test_log_word_wrap() {
    let test_env = TestEnvironment::default();
    test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
    let repo_path = test_env.env_root().join("repo");
    let render = |args: &[&str], columns: u32, word_wrap: bool| {
        let mut args = args.to_vec();
        if word_wrap {
            args.push("--config-toml=ui.log-word-wrap=true");
        }
        let assert = test_env
            .jj_cmd(&repo_path, &args)
            .env("COLUMNS", columns.to_string())
            .assert()
            .success()
            .stderr("");
        get_stdout_string(&assert)
    };

    test_env.jj_cmd_success(&repo_path, &["commit", "-m", "main branch 1"]);
    test_env.jj_cmd_success(&repo_path, &["describe", "-m", "main branch 2"]);
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "side"]);
    test_env.jj_cmd_success(&repo_path, &["new", "-m", "merge", "@--", "@"]);

    // ui.log-word-wrap option applies to both graph/no-graph outputs
    insta::assert_snapshot!(render(&["log", "-r@"], 40, false), @r###"
    @  mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e
    │  (empty) merge
    ~
    "###);
    insta::assert_snapshot!(render(&["log", "-r@"], 40, true), @r###"
    @  mzvwutvlkqwt test.user@example.com
    │  2001-02-03 04:05:11.000 +07:00
    ~  68518a7e6c9e
       (empty) merge
    "###);
    insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, false), @r###"
    mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e
    (empty) merge
    "###);
    insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, true), @r###"
    mzvwutvlkqwt test.user@example.com
    2001-02-03 04:05:11.000 +07:00
    68518a7e6c9e
    (empty) merge
    "###);

    // Color labels should be preserved
    insta::assert_snapshot!(render(&["log", "-r@", "--color=always"], 40, true), @r###"
    @  mzvwutvlkqwt test.user@example.com
    │  2001-02-03 04:05:11.000 +07:00
    ~  68518a7e6c9e
       (empty) merge
    "###);

    // Graph width should be subtracted from the term width
    let template = r#""0 1 2 3 4 5 6 7 8 9""#;
    insta::assert_snapshot!(render(&["log", "-T", template], 10, true), @r###"
    @    0 1 2
    ├─╮  3 4 5
    │ │  6 7 8
    │ │  9
    │ ◉  0 1 2
    │ │  3 4 5
    │ │  6 7 8
    │ │  9
    │ ◉  0 1 2
    ├─╯  3 4 5
    │    6 7 8
    │    9
    ◉  0 1 2 3
    │  4 5 6 7
    │  8 9
    ◉  0 1 2 3
       4 5 6 7
       8 9
    "###);
    insta::assert_snapshot!(
        render(&["log", "-T", template, "--config-toml=ui.graph.style='legacy'"], 9, true),
        @r###"
    @   0 1 2
    |\  3 4 5
    | | 6 7 8
    | | 9
    | o 0 1 2
    | | 3 4 5
    | | 6 7 8
    | | 9
    | o 0 1 2
    |/  3 4 5
    |   6 7 8
    |   9
    o 0 1 2 3
    | 4 5 6 7
    | 8 9
    o 0 1 2 3
      4 5 6 7
      8 9
    "###);

    // Shouldn't panic with $COLUMNS < graph_width
    insta::assert_snapshot!(render(&["log", "-r@"], 0, true), @r###"
    @  mzvwutvlkqwt
    │  test.user@example.com
    ~  2001-02-03
       04:05:11.000
       +07:00
       68518a7e6c9e
       (empty)
       merge
    "###);
    insta::assert_snapshot!(render(&["log", "-r@"], 1, true), @r###"
    @  mzvwutvlkqwt
    │  test.user@example.com
    ~  2001-02-03
       04:05:11.000
       +07:00
       68518a7e6c9e
       (empty)
       merge
    "###);
}