// Copyright 2025 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 std::sync::Arc;

use assert_matches::assert_matches;
use jj_lib::backend::CommitId;
use jj_lib::bisect::BisectionResult;
use jj_lib::bisect::Bisector;
use jj_lib::bisect::Evaluation;
use jj_lib::bisect::NextStep;
use jj_lib::repo::Repo;
use jj_lib::revset::ResolvedRevsetExpression;
use pollster::FutureExt as _;
use testutils::TestRepo;
use testutils::write_random_commit;
use testutils::write_random_commit_with_parents;

fn test_bisection<'a>(
    repo: &dyn Repo,
    input_range: &Arc<ResolvedRevsetExpression>,
    results: impl IntoIterator<Item = (&'a CommitId, Evaluation)>,
) -> BisectionResult {
    let mut bisector = Bisector::new(repo, input_range.clone()).block_on().unwrap();
    let mut iter = results.into_iter().enumerate();
    loop {
        match bisector.next_step().block_on().unwrap() {
            NextStep::Evaluate(commit) => {
                let (i, (expected_id, result)) =
                    iter.next().expect("More commits than expected were tested");
                assert_eq!(
                    commit.id(),
                    expected_id,
                    "Attempt to test unexpected commit at iteration {i}"
                );
                bisector.mark(commit.id().clone(), result);
            }
            NextStep::Done(bisection_result) => {
                assert!(iter.next().is_none(), "Finished earlier than expected");
                return bisection_result;
            }
        }
    }
}

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

    let input_range = ResolvedRevsetExpression::none();
    let expected_tests = [];
    let result = test_bisection(repo.as_ref(), &input_range, expected_tests);
    assert_matches!(result, BisectionResult::Indeterminate);
}

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

    let mut tx = repo.start_transaction();
    let root_commit = repo.store().root_commit();
    let commit1 = write_random_commit(tx.repo_mut());
    let commit2 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
    let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit2]);
    let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);
    let commit5 = write_random_commit_with_parents(tx.repo_mut(), &[&commit4]);
    let commit6 = write_random_commit_with_parents(tx.repo_mut(), &[&commit5]);
    let commit7 = write_random_commit_with_parents(tx.repo_mut(), &[&commit6]);

    let input_range = ResolvedRevsetExpression::commit(commit7.id().clone()).ancestors();

    // Root commit is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Bad),
        (commit1.id(), Evaluation::Bad),
        (root_commit.id(), Evaluation::Bad),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![root_commit.clone()]));

    // Commit 1 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Bad),
        (commit1.id(), Evaluation::Bad),
        (root_commit.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit1.clone()]));

    // Commit 3 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Bad),
        (commit1.id(), Evaluation::Good),
        (commit2.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit3.clone()]));

    // Commit 5 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Good),
        (commit5.id(), Evaluation::Bad),
        (commit4.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit5.clone()]));

    // Commit 7 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Good),
        (commit5.id(), Evaluation::Good),
        (commit6.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit7.clone()]));

    // Commit 2 is the first bad commit but commit 3 is skipped
    let expected_tests = [
        (commit3.id(), Evaluation::Skip),
        (commit2.id(), Evaluation::Bad),
        (root_commit.id(), Evaluation::Good),
        (commit1.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit2.clone()]));

    // Commit 4 is the first bad commit but commit 3 is skipped
    let expected_tests = [
        (commit3.id(), Evaluation::Skip),
        (commit2.id(), Evaluation::Good),
        (commit5.id(), Evaluation::Bad),
        (commit4.id(), Evaluation::Bad),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    // TODO: Indicate in the result that we're unsure if commit4 was the first bad
    // commit because the commit before it in the set was skipped.
    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));

    // Commit 7 is the first bad commit but commits before 6 were skipped
    // TODO: Avoid testing every commit near first skipped commit. Test e.g. commit
    // 1 and commit 5 once we see that commit 3 was indeterminate.
    let expected_tests = [
        (commit3.id(), Evaluation::Skip),
        (commit2.id(), Evaluation::Skip),
        (commit4.id(), Evaluation::Skip),
        (commit1.id(), Evaluation::Skip),
        (commit5.id(), Evaluation::Skip),
        (root_commit.id(), Evaluation::Skip),
        (commit6.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit7.clone()]));

    // Gaps in the input range are allowed
    let input_range = ResolvedRevsetExpression::commits(vec![
        commit7.id().clone(),
        commit4.id().clone(),
        commit2.id().clone(),
        commit1.id().clone(),
    ]);
    // Commit 4 is the first bad commit
    let expected_tests = [
        (commit2.id(), Evaluation::Good),
        (commit4.id(), Evaluation::Bad),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));
}

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

    // 7
    // |\
    // 5 6
    // | |
    // 3 4
    // | |
    // 1 2
    // |/
    // 0
    let mut tx = repo.start_transaction();
    let root_commit = repo.store().root_commit();
    let commit1 = write_random_commit(tx.repo_mut());
    let commit2 = write_random_commit(tx.repo_mut());
    let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
    let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit2]);
    let commit5 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);
    let commit6 = write_random_commit_with_parents(tx.repo_mut(), &[&commit4]);
    let commit7 = write_random_commit_with_parents(tx.repo_mut(), &[&commit5, &commit6]);

    let input_range = ResolvedRevsetExpression::commit(commit7.id().clone()).ancestors();

    // Root commit is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Bad),
        (root_commit.id(), Evaluation::Bad),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![root_commit.clone()]));

    // Commit 3 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Bad),
        (root_commit.id(), Evaluation::Good),
        (commit1.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit3.clone()]));

    // Commit 4 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Good),
        (commit4.id(), Evaluation::Bad),
        (commit2.id(), Evaluation::Good),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));

    // Commit 6 is the first bad commit
    let expected_tests = [
        (commit3.id(), Evaluation::Good),
        (commit4.id(), Evaluation::Good),
        (commit5.id(), Evaluation::Good),
        (commit6.id(), Evaluation::Bad),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Found(vec![commit6.clone()]));
}

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

    // 1 2
    // |/
    // 0
    let mut tx = repo.start_transaction();
    let commit1 = write_random_commit(tx.repo_mut());
    let commit2 = write_random_commit(tx.repo_mut());

    let input_range =
        ResolvedRevsetExpression::commits(vec![commit1.id().clone(), commit2.id().clone()]);

    // Both commit 1 and commit 2 are (implicitly) the first bad commits
    let expected_tests = [];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(
        result,
        BisectionResult::Found(vec![commit2.clone(), commit1.clone()])
    );
}

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

    let mut tx = repo.start_transaction();
    let commit1 = write_random_commit(tx.repo_mut());
    let commit2 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);
    let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit2]);
    let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);
    let commit5 = write_random_commit_with_parents(tx.repo_mut(), &[&commit4]);
    let commit6 = write_random_commit_with_parents(tx.repo_mut(), &[&commit5]);
    let commit7 = write_random_commit_with_parents(tx.repo_mut(), &[&commit6]);

    let input_range = ResolvedRevsetExpression::commit(commit7.id().clone()).ancestors();

    // Commit 5 is an abort commit
    let expected_tests = [
        (commit3.id(), Evaluation::Good),
        (commit5.id(), Evaluation::Abort),
    ];
    let result = test_bisection(tx.repo(), &input_range, expected_tests);
    assert_eq!(result, BisectionResult::Abort);
}
