"""Unit tests for reviewboard.diffviewer.commit_utils."""

from __future__ import annotations

from typing import TYPE_CHECKING

import kgb

from reviewboard.diffviewer.commit_utils import (CommitHistoryDiffEntry,
                                                 diff_histories,
                                                 exclude_ancestor_filediffs,
                                                 get_base_and_tip_commits,
                                                 get_file_exists_in_history)
from reviewboard.diffviewer.models import DiffCommit
from reviewboard.scmtools.core import FileLookupContext, UNKNOWN
from reviewboard.scmtools.errors import FileNotFoundError
from reviewboard.testing.testcase import BaseFileDiffAncestorTests, TestCase

if TYPE_CHECKING:
    from typelets.json import JSONDict


class GetFileExistsInHistoryTests(kgb.SpyAgency, TestCase):
    """Unit tests for get_file_exists_in_history."""

    fixtures = ['test_scmtools']

    def test_added_in_parent_with_revision(self):
        """Testing get_file_exists_in_history for a file added in the parent
        commit for an SCM that uses file revisions
        """
        repository = self.create_repository()
        validation_info = {
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': 'a' * 40,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision='a' * 40))

    def test_added_in_parent_without_revision(self):
        """Testing get_file_exists_in_history for a file added in the parent
        commit for an SCM that doesn't use file revisions
        """
        repository = self.create_repository(tool_name='Mercurial')
        validation_info = {
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision=UNKNOWN))

    def test_added_in_grandparent_with_revision(self):
        """Testing get_file_exists_in_history for a file added in a
        grandparent commit for an SCM that uses file revisions
        """
        repository = self.create_repository()
        validation_info = {
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [],
                    'modified': [],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': 'a' * 40,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r2',
            path='foo',
            revision='a' * 40))

    def test_added_in_grandparent_without_revision(self):
        """Testing get_file_exists_in_history for a file added in a
        grandparent commit for an SCM that doesn't use file revisions
        """
        repository = self.create_repository(tool_name='Mercurial')
        validation_info = {
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [],
                    'modified': [],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r2',
            path='foo',
            revision=UNKNOWN))

    def test_removed_in_parent_without_revision(self):
        """Testing get_file_exists_in_history for a file removed in a parent
        revision for an SCM that uses file revisions
        """
        repository = self.create_repository()
        target_path = 'foo'
        target_revision = 'a' * 40

        self.spy_on(
            repository.get_file_exists,
            call_fake=self._make_get_file_exists_in_history(target_path,
                                                            target_revision))

        validation_info = {
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [],
                    'removed': [{
                        'filename': target_path,
                        'revision': target_revision,
                    }],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path=target_path,
            revision=target_revision))

        self.assertSpyCalledWith(
            repository.get_file_exists,
            path=target_path,
            revision=target_revision)

    def test_removed_in_parent_unknown_revision(self):
        """Testing get_file_exists_in_history for a file removed in a parent
        commit for an SCM that does not use file revisions
        """
        repository = self.create_repository(tool_name='Mercurial')

        validation_info = {
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [],
                    'removed': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                },
            },
        }

        self.assertFalse(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision=UNKNOWN))

    def test_removed_readded_in_parent_unknown_revision(self):
        """Testing get_file_exists_in_history for a file removed and re-added
        in parent commits for an SCM that does not use file revisions
        """
        repository = self.create_repository(tool_name='Mercurial')

        validation_info = {
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [],
                    'removed': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                },
            },
        }

        self.assertFalse(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision=UNKNOWN))

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r2',
            path='foo',
            revision=UNKNOWN))

    def test_modified_in_parent_with_revision(self):
        """Testing get_file_exists_in_history for a file modified in a
        parent revision for an SCM that uses file revisions
        """
        repository = self.create_repository()
        self.spy_on(
            repository.get_file_exists,
            call_fake=self._make_get_file_exists_in_history('foo', 'a' * 40))

        validation_info = {
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'foo',
                        'revision': 'c' * 40,
                    }],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'foo',
                        'revision': 'b' * 40,
                    }],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r2',
            path='foo',
            revision='c' * 40))
        self.assertSpyNotCalled(repository.get_file_exists)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision='b' * 40))
        self.assertSpyNotCalled(repository.get_file_exists)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r0',
            path='foo',
            revision='a' * 40))
        self.assertSpyCalledWith(
            repository.get_file_exists,
            path='foo',
            revision='a' * 40)

    def test_modified_in_parent_unknown_revision(self):
        """Testing get_file_exists_in_history for a file modified in a
        parent revision for an SCM that does not use file revision
        """
        repository = self.create_repository(tool_name='Mercurial')
        self.spy_on(
            repository.get_file_exists,
            call_fake=self._make_get_file_exists_in_history('foo', UNKNOWN))

        validation_info = {
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'foo',
                        'revision': UNKNOWN,
                    }],
                    'removed': [],
                },
            },
        }

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r2',
            path='foo',
            revision=UNKNOWN))
        self.assertSpyNotCalled(repository.get_file_exists)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r1',
            path='foo',
            revision=UNKNOWN))
        self.assertSpyNotCalled(repository.get_file_exists)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r0',
            path='foo',
            revision=UNKNOWN))
        self.assertSpyCalledWith(
            repository.get_file_exists,
            path='foo',
            revision=UNKNOWN)

    def test_without_per_file_revisions_and_modified(self) -> None:
        """Testing get_file_exists_in_history without per-file revision
        information and file is in modified file entry
        """
        repository = self.create_repository()
        self.spy_on(
            repository.get_file_exists,
            call_fake=self._make_get_file_exists_in_history('foo', UNKNOWN))

        validation_info = {
            'r3': {
                'parent_id': 'r2',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'bar',
                        'revision': 'd' * 40,
                    }],
                    'removed': [],
                },
            },
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'bar',
                        'revision': 'c' * 40,
                    }],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [],
                    'modified': [{
                        'filename': 'foo',
                        'revision': 'b' * 40,
                    }],
                    'removed': [],
                },
            },
        }

        diff_extra_data: JSONDict = {
            'has_per_file_revisions': False,
        }

        # Check foo.
        context = FileLookupContext(diff_extra_data=diff_extra_data)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r3',
            path='foo',
            revision='x' * 40,
            context=context))
        self.assertEqual(context.file_extra_data, {
            '__validated_parent_id': 'r1',
        })
        self.assertSpyNotCalled(repository.get_file_exists)

        # Check bar.
        context = FileLookupContext(diff_extra_data=diff_extra_data)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r3',
            path='bar',
            revision='x' * 40,
            context=context))
        self.assertEqual(context.file_extra_data, {
            '__validated_parent_id': 'r3',
        })
        self.assertSpyNotCalled(repository.get_file_exists)

    def test_without_per_file_revisions_and_added(self) -> None:
        """Testing get_file_exists_in_history without per-file revision
        information and file is in added file entry
        """
        repository = self.create_repository()
        self.spy_on(
            repository.get_file_exists,
            op=kgb.SpyOpRaise(FileNotFoundError(path='xxx',
                                                revision='xxx')))

        validation_info = {
            'r3': {
                'parent_id': 'r2',
                'tree': {
                    'added': [{
                        'filename': 'bar',
                        'revision': 'd' * 40,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
            'r2': {
                'parent_id': 'r1',
                'tree': {
                    'added': [{
                        'filename': 'xxx',
                        'revision': 'c' * 40,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
            'r1': {
                'parent_id': 'r0',
                'tree': {
                    'added': [{
                        'filename': 'foo',
                        'revision': 'b' * 40,
                    }],
                    'modified': [],
                    'removed': [],
                },
            },
        }

        diff_extra_data: JSONDict = {
            'has_per_file_revisions': False,
        }

        # Check foo.
        context = FileLookupContext(diff_extra_data=diff_extra_data)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r3',
            path='foo',
            revision='x' * 40,
            context=context))
        self.assertEqual(context.file_extra_data, {
            '__validated_parent_id': 'r1',
        })
        self.assertSpyNotCalled(repository.get_file_exists)

        # Check bar.
        context = FileLookupContext(diff_extra_data=diff_extra_data)

        self.assertTrue(get_file_exists_in_history(
            validation_info=validation_info,
            repository=repository,
            parent_id='r3',
            path='bar',
            revision='x' * 40,
            context=context))
        self.assertEqual(context.file_extra_data, {
            '__validated_parent_id': 'r3',
        })
        self.assertSpyNotCalled(repository.get_file_exists)

    def _make_get_file_exists_in_history(self, target_path, target_revision):
        """Return a fake get_file_exists_in_history method for a repository.

        Args:
            target_path (unicode):
                The path that should report as existing in the repository.

            target_revision (unicode):
                The revision of the file.

        Returns:
            callable:
            A function that only returns True when called with the given
            ``target_path`` and ``target_revision``.
        """
        def get_file_exists_in_history(repository, path, revision, *args,
                                       **kwargs):
            return path == target_path and revision == target_revision

        return get_file_exists_in_history


class ExcludeAncestorFileDiffsTests(BaseFileDiffAncestorTests):
    """Unit tests for commit_utils.exclude_ancestor_filediffs."""

    def setUp(self):
        super(ExcludeAncestorFileDiffsTests, self).setUp()

        self.set_up_filediffs()

    def test_exclude(self):
        """Testing exclude_ancestor_filediffs"""
        self._test_excluded(exclude_ancestor_filediffs(self.filediffs))

    def test_exclude_query_count(self):
        """Testing exclude_ancestor_filediffs query count"""
        num_queries = len(self.filediffs)

        with self.assertNumQueries(num_queries):
            result = exclude_ancestor_filediffs(self.filediffs)

        self._test_excluded(result)

    def test_exclude_query_count_precomputed(self):
        """Testing exclude_ancestor_filediffs query count when the ancestors
        are pre-computed
        """
        for filediff in self.filediffs:
            filediff.get_ancestors(minimal=False, filediffs=self.filediffs)

        with self.assertNumQueries(0):
            result = exclude_ancestor_filediffs(self.filediffs)

        self._test_excluded(result)

    def _test_excluded(self, result):
        """Test that the given set of FileDiffs matches the expected results.

        Args:
            result (list of reviewboard.diffviewer.models.filediff.FileDiff):
                The FileDiffs that were returned from :py:func:`~reviewboard.
                diffviewer.commit_utils.exclude_ancestor_filediffs`.

        Raises:
            AssertionError:
                The FileDiffs do not match the expected results.
        """
        by_details = {
            (
                filediff.commit_id,
                filediff.source_file,
                filediff.source_revision,
                filediff.dest_file,
                filediff.dest_detail,
            ): filediff
            for filediff in self.filediffs
        }

        expected = {
            by_details[details]
            for details in (
                (2, 'baz', '7601807', 'baz', '280beb2'),
                (3, 'foo', '257cc56', 'qux', '03b37a0'),
                (3, 'corge', 'e69de29', 'corge', 'f248ba3'),
                (4, 'bar', '5716ca5', 'quux', 'e69de29'),
            )
        }

        self.assertEqual(expected, set(result))


class DiffHistoriesTests(TestCase):
    """Unit tests for reviewboard.diffviewer.commit_utils.diff_histories."""

    def test_diff_histories_identical(self):
        """Testing diff_histories with identical histories"""
        new_history = old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[0],
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[1],
                new_commit=new_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[2],
                new_commit=new_history[2]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_added(self):
        """Testing diff_histories with a new history that adds commits at the
        end of the history
        """
        old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
        ]

        new_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[0],
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[1],
                new_commit=new_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[2]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_removed(self):
        """Testing diff_histories with a new history that removes commits"""
        old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        new_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[0],
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[1],
                new_commit=new_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[2]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_added_start(self):
        """Testing diff_histories with a new history that adds commits at the
        start of the history
        """
        old_history = [
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        new_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[2]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_removed_start(self):
        """Testing diff_histories with a new history that removes commits at
        the start of the history
        """
        old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        new_history = [
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[2]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[1]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_added_middle(self):
        """Testing diff_histories with a new history that adds commits in the
        middle of the history
        """
        old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r2'),
        ]

        new_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[0],
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[2]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)

    def test_diff_histories_removed_middle(self):
        """Testing diff_histories with a new history that removes commits in
        the middle of the history
        """
        old_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r1'),
            DiffCommit(commit_id='r2'),
        ]

        new_history = [
            DiffCommit(commit_id='r0'),
            DiffCommit(commit_id='r2'),
        ]

        expected_result = [
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_UNMODIFIED,
                old_commit=old_history[0],
                new_commit=new_history[0]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[1]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_REMOVED,
                old_commit=old_history[2]),
            CommitHistoryDiffEntry(
                entry_type=CommitHistoryDiffEntry.COMMIT_ADDED,
                new_commit=new_history[1]),
        ]

        self.assertEqual(list(diff_histories(old_history, new_history)),
                         expected_result)


class GetBaseAndTipCommitsTests(TestCase):
    """Unit tests for commit_utils.get_base_and_tip_commits."""

    fixtures = ['test_scmtools']

    def setUp(self):
        super(GetBaseAndTipCommitsTests, self).setUp()

        self.repository = self.create_repository(tool_name='Git')
        self.diffset = self.create_diffset(repository=self.repository)
        self.commits = {
            commit.pk: commit
            for commit in (
                self.create_diffcommit(diffset=self.diffset, **kwargs)
                for kwargs in (
                    {'commit_id': 'r1', 'parent_id': 'r0'},
                    {'commit_id': 'r2', 'parent_id': 'r1'},
                    {'commit_id': 'r3', 'parent_id': 'r2'},
                    {'commit_id': 'r4', 'parent_id': 'r3'},
                    {'commit_id': 'r5', 'parent_id': 'r4'},
                    {'commit_id': 'r6', 'parent_id': 'r5'},
                )
            )
        }

    def test_get_base_and_tip_commits_no_commits_no_diffset(self):
        """Testing get_base_and_tip_commits with no commits and no diffset
        raises ValueError
        """
        with self.assertRaises(ValueError):
            get_base_and_tip_commits(base_commit_id=1,
                                     tip_commit_id=5)

    def test_get_base_and_tip_commits_commits_no_diffset(self):
        """Testing get_base_and_tip_commits with commits and no diffset"""
        with self.assertNumQueries(0):
            base, tip = get_base_and_tip_commits(
                base_commit_id=1,
                tip_commit_id=5,
                commits=list(self.commits.values()))

        self.assertEqual(self.commits[1], base)
        self.assertEqual(self.commits[5], tip)

    def test_get_base_and_tip_commits_no_commits_diffset(self):
        """Testing get_base_and_tip_commits with a diffset and no commits"""
        with self.assertNumQueries(1):
            base, tip = get_base_and_tip_commits(
                base_commit_id=1,
                tip_commit_id=5,
                diffset=self.diffset)

        self.assertEqual(self.commits[1], base)
        self.assertEqual(self.commits[5], tip)

    def test_get_base_and_tip_commits_with_commits_invalid_commit_id(self):
        """Testing get_base_and_tip_commits with commits and an invalid commit
        ID
        """
        base, tip = get_base_and_tip_commits(
            base_commit_id=7,
            tip_commit_id=5000,
            commits=list(self.commits.values()))

        self.assertIsNone(base)
        self.assertIsNone(tip)

    def test_get_base_and_tip_commits_with_diffset_invalid_commit_id(self):
        """Testing get_base_and_tip_commits with commits and an invalid commit
        ID
        """
        other_diffset = self.create_diffset(repository=self.repository)
        other_commits = [
            self.create_diffcommit(diffset=other_diffset, **kwargs)
            for kwargs in (
                {'commit_id': 'r1', 'parent_id': 'r0'},
                {'commit_id': 'r2', 'parent_id': 'r1'},
            )
        ]

        base, tip = get_base_and_tip_commits(
            base_commit_id=other_commits[0].pk,
            tip_commit_id=other_commits[1].pk,
            diffset=self.diffset)

        self.assertIsNone(base)
        self.assertIsNone(tip)

    def test_get_base_and_tip_commits_only_base(self):
        """Testing get_base_and_tip_commits with only base_commit_id"""
        base, tip = get_base_and_tip_commits(
            base_commit_id=3,
            tip_commit_id=None,
            diffset=self.diffset)

        self.assertEqual(self.commits[3], base)
        self.assertIsNone(tip)

    def test_get_base_and_tip_commits_only_tip(self):
        """Testing get_base_and_tip_commits with only tip_commit_id"""
        base, tip = get_base_and_tip_commits(
            base_commit_id=None,
            tip_commit_id=3,
            diffset=self.diffset)

        self.assertIsNone(base)
        self.assertEqual(self.commits[3], tip)
