// Copyright 2023 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 itertools::Itertools;
use jj_lib::backend::{FileId, TreeValue};
use jj_lib::conflicts::Conflict;
use jj_lib::merged_tree::{MergedTree, MergedTreeValue};
use jj_lib::repo::Repo;
use jj_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
use jj_lib::tree::merge_trees;
use testutils::{write_file, write_normal_file, TestRepo};

fn file_value(file_id: &FileId) -> TreeValue {
    TreeValue::File {
        id: file_id.clone(),
        executable: false,
    }
}

#[test]
fn test_from_legacy_tree() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;
    let store = repo.store();

    let mut tree_builder = store.tree_builder(repo.store().empty_tree_id().clone());

    // file1: regular file without conflicts
    let file1_path = RepoPath::from_internal_string("no_conflict");
    let file1_id = write_normal_file(&mut tree_builder, &file1_path, "foo");

    // file2: 3-way conflict
    let file2_path = RepoPath::from_internal_string("3way");
    let file2_v1_id = write_file(store.as_ref(), &file2_path, "file2_v1");
    let file2_v2_id = write_file(store.as_ref(), &file2_path, "file2_v2");
    let file2_v3_id = write_file(store.as_ref(), &file2_path, "file2_v3");
    let file2_conflict = Conflict::new(
        vec![Some(file_value(&file2_v1_id))],
        vec![
            Some(file_value(&file2_v2_id)),
            Some(file_value(&file2_v3_id)),
        ],
    );
    let file2_conflict_id = store.write_conflict(&file2_path, &file2_conflict).unwrap();
    tree_builder.set(file2_path.clone(), TreeValue::Conflict(file2_conflict_id));

    // file3: modify/delete conflict
    let file3_path = RepoPath::from_internal_string("modify_delete");
    let file3_v1_id = write_file(store.as_ref(), &file3_path, "file3_v1");
    let file3_v2_id = write_file(store.as_ref(), &file3_path, "file3_v2");
    let file3_conflict = Conflict::new(
        vec![Some(file_value(&file3_v1_id))],
        vec![Some(file_value(&file3_v2_id)), None],
    );
    let file3_conflict_id = store.write_conflict(&file3_path, &file3_conflict).unwrap();
    tree_builder.set(file3_path.clone(), TreeValue::Conflict(file3_conflict_id));

    // file4: add/add conflict
    let file4_path = RepoPath::from_internal_string("add_add");
    let file4_v1_id = write_file(store.as_ref(), &file4_path, "file4_v1");
    let file4_v2_id = write_file(store.as_ref(), &file4_path, "file4_v2");
    let file4_conflict = Conflict::new(
        vec![None],
        vec![
            Some(file_value(&file4_v1_id)),
            Some(file_value(&file4_v2_id)),
        ],
    );
    let file4_conflict_id = store.write_conflict(&file4_path, &file4_conflict).unwrap();
    tree_builder.set(file4_path.clone(), TreeValue::Conflict(file4_conflict_id));

    // file5: 5-way conflict
    let file5_path = RepoPath::from_internal_string("5way");
    let file5_v1_id = write_file(store.as_ref(), &file5_path, "file5_v1");
    let file5_v2_id = write_file(store.as_ref(), &file5_path, "file5_v2");
    let file5_v3_id = write_file(store.as_ref(), &file5_path, "file5_v3");
    let file5_v4_id = write_file(store.as_ref(), &file5_path, "file5_v4");
    let file5_v5_id = write_file(store.as_ref(), &file5_path, "file5_v5");
    let file5_conflict = Conflict::new(
        vec![
            Some(file_value(&file5_v1_id)),
            Some(file_value(&file5_v2_id)),
        ],
        vec![
            Some(file_value(&file5_v3_id)),
            Some(file_value(&file5_v4_id)),
            Some(file_value(&file5_v5_id)),
        ],
    );
    let file5_conflict_id = store.write_conflict(&file5_path, &file5_conflict).unwrap();
    tree_builder.set(file5_path.clone(), TreeValue::Conflict(file5_conflict_id));

    // dir1: directory without conflicts
    let dir1_basename = RepoPathComponent::from("dir1");
    write_normal_file(
        &mut tree_builder,
        &RepoPath::root()
            .join(&dir1_basename)
            .join(&RepoPathComponent::from("file")),
        "foo",
    );

    let tree_id = tree_builder.write_tree();
    let tree = store.get_tree(&RepoPath::root(), &tree_id).unwrap();

    let merged_tree = MergedTree::from_legacy_tree(tree.clone());
    assert_eq!(
        merged_tree.value(&RepoPathComponent::from("missing")),
        MergedTreeValue::Resolved(None)
    );
    // file1: regular file without conflicts
    assert_eq!(
        merged_tree.value(&file1_path.components()[0]),
        MergedTreeValue::Resolved(Some(&TreeValue::File {
            id: file1_id,
            executable: false,
        }))
    );
    // file2: 3-way conflict
    assert_eq!(
        merged_tree.value(&file2_path.components()[0]),
        MergedTreeValue::Conflict(Conflict::new(
            vec![Some(file_value(&file2_v1_id)), None],
            vec![
                Some(file_value(&file2_v2_id)),
                Some(file_value(&file2_v3_id)),
                None,
            ],
        ))
    );
    // file3: modify/delete conflict
    assert_eq!(
        merged_tree.value(&file3_path.components()[0]),
        MergedTreeValue::Conflict(Conflict::new(
            vec![Some(file_value(&file3_v1_id)), None],
            vec![Some(file_value(&file3_v2_id)), None, None],
        ))
    );
    // file4: add/add conflict
    assert_eq!(
        merged_tree.value(&file4_path.components()[0]),
        MergedTreeValue::Conflict(Conflict::new(
            vec![None, None],
            vec![
                Some(file_value(&file4_v1_id)),
                Some(file_value(&file4_v2_id)),
                None
            ],
        ))
    );
    // file5: 5-way conflict
    assert_eq!(
        merged_tree.value(&file5_path.components()[0]),
        MergedTreeValue::Conflict(Conflict::new(
            vec![
                Some(file_value(&file5_v1_id)),
                Some(file_value(&file5_v2_id)),
            ],
            vec![
                Some(file_value(&file5_v3_id)),
                Some(file_value(&file5_v4_id)),
                Some(file_value(&file5_v5_id)),
            ],
        ))
    );
    // file6: directory without conflicts
    assert_eq!(
        merged_tree.value(&dir1_basename),
        MergedTreeValue::Resolved(tree.value(&dir1_basename))
    );
}

#[test]
fn test_resolve_success() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;

    let unchanged_path = RepoPath::from_internal_string("unchanged");
    let trivial_file_path = RepoPath::from_internal_string("trivial-file");
    let trivial_hunk_path = RepoPath::from_internal_string("trivial-hunk");
    let both_added_dir_path = RepoPath::from_internal_string("added-dir");
    let both_added_dir_file1_path = both_added_dir_path.join(&RepoPathComponent::from("file1"));
    let both_added_dir_file2_path = both_added_dir_path.join(&RepoPathComponent::from("file2"));
    let emptied_dir_path = RepoPath::from_internal_string("to-become-empty");
    let emptied_dir_file1_path = emptied_dir_path.join(&RepoPathComponent::from("file1"));
    let emptied_dir_file2_path = emptied_dir_path.join(&RepoPathComponent::from("file2"));
    let base1 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_file_path, "base1"),
            (&trivial_hunk_path, "line1\nline2\nline3\n"),
            (&emptied_dir_file1_path, "base1"),
            (&emptied_dir_file2_path, "base1"),
        ],
    );
    let side1 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_file_path, "base1"),
            (&trivial_hunk_path, "line1 side1\nline2\nline3\n"),
            (&both_added_dir_file1_path, "side1"),
            (&emptied_dir_file2_path, "base1"),
        ],
    );
    let side2 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_file_path, "side2"),
            (&trivial_hunk_path, "line1\nline2\nline3 side2\n"),
            (&both_added_dir_file2_path, "side2"),
            (&emptied_dir_file1_path, "base1"),
        ],
    );
    let expected = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_file_path, "side2"),
            (&trivial_hunk_path, "line1 side1\nline2\nline3 side2\n"),
            (&both_added_dir_file1_path, "side1"),
            (&both_added_dir_file2_path, "side2"),
        ],
    );

    let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2]));
    let resolved = tree.resolve().unwrap();
    let resolved_tree = resolved.as_resolved().unwrap().clone();
    assert_eq!(
        resolved_tree,
        expected,
        "actual entries: {:#?}, expected entries {:#?}",
        resolved_tree.entries().collect_vec(),
        expected.entries().collect_vec()
    );
}

#[test]
fn test_resolve_root_becomes_empty() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;
    let store = repo.store();

    let path1 = RepoPath::from_internal_string("dir1/file");
    let path2 = RepoPath::from_internal_string("dir2/file");
    let base1 = testutils::create_tree(repo, &[(&path1, "base1"), (&path2, "base1")]);
    let side1 = testutils::create_tree(repo, &[(&path2, "base1")]);
    let side2 = testutils::create_tree(repo, &[(&path1, "base1")]);

    let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2]));
    let resolved = tree.resolve().unwrap();
    assert_eq!(resolved.as_resolved().unwrap().id(), store.empty_tree_id());
}

#[test]
fn test_resolve_with_conflict() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;

    // The trivial conflict should be resolved but the non-trivial should not (and
    // cannot)
    let trivial_path = RepoPath::from_internal_string("dir1/trivial");
    let conflict_path = RepoPath::from_internal_string("dir2/file_conflict");
    let base1 =
        testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "base1")]);
    let side1 =
        testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
    let side2 =
        testutils::create_tree(repo, &[(&trivial_path, "base1"), (&conflict_path, "side2")]);
    let expected_base1 =
        testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "base1")]);
    let expected_side1 =
        testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side1")]);
    let expected_side2 =
        testutils::create_tree(repo, &[(&trivial_path, "side1"), (&conflict_path, "side2")]);

    let tree = MergedTree::new(Conflict::new(vec![base1], vec![side1, side2]));
    let resolved_tree = tree.resolve().unwrap();
    assert_eq!(
        resolved_tree,
        Conflict::new(vec![expected_base1], vec![expected_side1, expected_side2])
    )
}

#[test]
fn test_conflict_iterator() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;

    let unchanged_path = RepoPath::from_internal_string("dir/subdir/unchanged");
    let trivial_path = RepoPath::from_internal_string("dir/subdir/trivial");
    let trivial_hunk_path = RepoPath::from_internal_string("dir/non_trivial");
    let file_conflict_path = RepoPath::from_internal_string("dir/subdir/file_conflict");
    let modify_delete_path = RepoPath::from_internal_string("dir/subdir/modify_delete");
    let same_add_path = RepoPath::from_internal_string("dir/subdir/same_add");
    let different_add_path = RepoPath::from_internal_string("dir/subdir/different_add");
    let dir_file_path = RepoPath::from_internal_string("dir/subdir/dir_file");
    let added_dir_path = RepoPath::from_internal_string("dir/new_dir");
    let modify_delete_dir_path = RepoPath::from_internal_string("dir/modify_delete_dir");
    let base1 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_path, "base"),
            (&trivial_hunk_path, "line1\nline2\nline3\n"),
            (&file_conflict_path, "base"),
            (&modify_delete_path, "base"),
            // no same_add_path
            // no different_add_path
            (&dir_file_path, "base"),
            // no added_dir_path
            (
                &modify_delete_dir_path.join(&RepoPathComponent::from("base")),
                "base",
            ),
        ],
    );
    let side1 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_path, "base"),
            (&file_conflict_path, "side1"),
            (&trivial_hunk_path, "line1 side1\nline2\nline3\n"),
            (&modify_delete_path, "modified"),
            (&same_add_path, "same"),
            (&different_add_path, "side1"),
            (&dir_file_path, "side1"),
            (
                &added_dir_path.join(&RepoPathComponent::from("side1")),
                "side1",
            ),
            (
                &modify_delete_dir_path.join(&RepoPathComponent::from("side1")),
                "side1",
            ),
        ],
    );
    let side2 = testutils::create_tree(
        repo,
        &[
            (&unchanged_path, "unchanged"),
            (&trivial_path, "side2"),
            (&file_conflict_path, "side2"),
            (&trivial_hunk_path, "line1\nline2\nline3 side2\n"),
            // no modify_delete_path
            (&same_add_path, "same"),
            (&different_add_path, "side2"),
            (&dir_file_path.join(&RepoPathComponent::from("dir")), "new"),
            (
                &added_dir_path.join(&RepoPathComponent::from("side2")),
                "side2",
            ),
            // no modify_delete_dir_path
        ],
    );

    let tree = MergedTree::new(Conflict::new(
        vec![base1.clone()],
        vec![side1.clone(), side2.clone()],
    ));
    let conflicts = tree.conflicts().collect_vec();
    let conflict_at = |path: &RepoPath| {
        Conflict::new(
            vec![base1.path_value(path)],
            vec![side1.path_value(path), side2.path_value(path)],
        )
    };
    // We initially also get a conflict in trivial_hunk_path because we had
    // forgotten to resolve conflicts
    assert_eq!(
        conflicts,
        vec![
            (trivial_hunk_path.clone(), conflict_at(&trivial_hunk_path)),
            (different_add_path.clone(), conflict_at(&different_add_path)),
            (dir_file_path.clone(), conflict_at(&dir_file_path)),
            (file_conflict_path.clone(), conflict_at(&file_conflict_path)),
            (modify_delete_path.clone(), conflict_at(&modify_delete_path)),
        ]
    );

    // After we resolve conflicts, there are only non-trivial conflicts left
    let tree = MergedTree::Merge(tree.resolve().unwrap());
    let conflicts = tree.conflicts().collect_vec();
    assert_eq!(
        conflicts,
        vec![
            (different_add_path.clone(), conflict_at(&different_add_path)),
            (dir_file_path.clone(), conflict_at(&dir_file_path)),
            (file_conflict_path.clone(), conflict_at(&file_conflict_path)),
            (modify_delete_path.clone(), conflict_at(&modify_delete_path)),
        ]
    );

    let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap();
    let legacy_conflicts = MergedTree::legacy(merged_legacy_tree)
        .conflicts()
        .collect_vec();
    assert_eq!(legacy_conflicts, conflicts);
}
#[test]
fn test_conflict_iterator_higher_arity() {
    let test_repo = TestRepo::init(true);
    let repo = &test_repo.repo;

    let two_sided_path = RepoPath::from_internal_string("dir/2-sided");
    let three_sided_path = RepoPath::from_internal_string("dir/3-sided");
    let base1 = testutils::create_tree(
        repo,
        &[(&two_sided_path, "base1"), (&three_sided_path, "base1")],
    );
    let base2 = testutils::create_tree(
        repo,
        &[(&two_sided_path, "base2"), (&three_sided_path, "base2")],
    );
    let side1 = testutils::create_tree(
        repo,
        &[(&two_sided_path, "side1"), (&three_sided_path, "side1")],
    );
    let side2 = testutils::create_tree(
        repo,
        &[(&two_sided_path, "base1"), (&three_sided_path, "side2")],
    );
    let side3 = testutils::create_tree(
        repo,
        &[(&two_sided_path, "side3"), (&three_sided_path, "side3")],
    );

    let tree = MergedTree::new(Conflict::new(
        vec![base1.clone(), base2.clone()],
        vec![side1.clone(), side2.clone(), side3.clone()],
    ));
    let conflicts = tree.conflicts().collect_vec();
    let conflict_at = |path: &RepoPath| {
        Conflict::new(
            vec![base1.path_value(path), base2.path_value(path)],
            vec![
                side1.path_value(path),
                side2.path_value(path),
                side3.path_value(path),
            ],
        )
    };
    // Both paths have the full, unsimplified conflict (3-sided)
    assert_eq!(
        conflicts,
        vec![
            (two_sided_path.clone(), conflict_at(&two_sided_path)),
            (three_sided_path.clone(), conflict_at(&three_sided_path))
        ]
    );
    // Iterating over conflicts in a legacy tree yields the simplified conflict at
    // each path
    let merged_legacy_tree = merge_trees(&side1, &base1, &side2).unwrap();
    let merged_legacy_tree = merge_trees(&merged_legacy_tree, &base2, &side3).unwrap();
    let legacy_conflicts = MergedTree::legacy(merged_legacy_tree)
        .conflicts()
        .collect_vec();
    assert_eq!(
        legacy_conflicts,
        vec![
            (
                two_sided_path.clone(),
                Conflict::new(
                    vec![base2.path_value(&two_sided_path)],
                    vec![
                        side1.path_value(&two_sided_path),
                        side3.path_value(&two_sided_path),
                    ],
                )
            ),
            (three_sided_path.clone(), conflict_at(&three_sided_path))
        ]
    );
}