# Copyright (C) 2009, 2010 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Tests for Branch.iter_merge_sorted_revisions()"""

from breezy import errors, revision, tests
from breezy.tests import per_branch


class TestIterMergeSortedRevisionsSimpleGraph(per_branch.TestCaseWithBranch):

    def setUp(self):
        super().setUp()
        self.revids = {}
        builder = self.make_builder_with_merges('.')
        self.branch = builder.get_branch()
        self.branch.lock_read()
        self.addCleanup(self.branch.unlock)

    def make_snapshot(self, builder, parents, revid_name):
        self.assertNotIn(revid_name, self.revids)
        if parents is None:
            files = [('add', ('', None, 'directory', '')), ]
        else:
            parents = [self.revids[name] for name in parents]
            files = []
        self.revids[revid_name] = builder.build_snapshot(
            parents, files, message="Revision %s" % revid_name)

    def make_builder_with_merges(self, relpath):
        try:
            builder = self.make_branch_builder(relpath)
        except (errors.TransportNotPossible, errors.UninitializableFormat):
            raise tests.TestNotApplicable('format not directly constructable')
        builder.start_series()
        # 1
        # |\
        # 2 |
        # | |
        # | 1.1.1
        # |/
        # 3
        self.make_snapshot(builder, None, '1')
        self.make_snapshot(builder, ['1'], '1.1.1')
        self.make_snapshot(builder, ['1'], '2')
        self.make_snapshot(builder, ['2', '1.1.1'], '3')
        builder.finish_series()
        return builder

    def assertIterRevids(self, expected, *args, **kwargs):
        if kwargs.get('stop_revision_id') is not None:
            kwargs['stop_revision_id'] = self.revids[kwargs['stop_revision_id']]
        if kwargs.get('start_revision_id') is not None:
            kwargs['start_revision_id'] = self.revids[kwargs['start_revision_id']]
        # We don't care about depths and revnos here, only about returning the
        # right revids.
        revids = [revid for (revid, depth, revno, eom) in
                  self.branch.iter_merge_sorted_revisions(*args, **kwargs)]
        self.assertEqual([self.revids[short] for short in expected], revids)

    def test_merge_sorted(self):
        self.assertIterRevids(['3', '1.1.1', '2', '1'])

    def test_merge_sorted_range(self):
        self.assertIterRevids(['1.1.1'],
                              start_revision_id='1.1.1', stop_revision_id='1')

    def test_merge_sorted_range_start_only(self):
        self.assertIterRevids(['1.1.1', '1'],
                              start_revision_id='1.1.1')

    def test_merge_sorted_range_stop_exclude(self):
        self.assertIterRevids(['3', '1.1.1', '2'], stop_revision_id='1')

    def test_merge_sorted_range_stop_include(self):
        self.assertIterRevids(['3', '1.1.1', '2'],
                              stop_revision_id='2', stop_rule='include')

    def test_merge_sorted_range_stop_with_merges(self):
        self.assertIterRevids(['3', '1.1.1'],
                              stop_revision_id='3', stop_rule='with-merges')

    def test_merge_sorted_range_stop_with_merges_can_show_non_parents(self):
        # 1.1.1 gets logged before the end revision is reached.
        # so it is returned even though 1.1.1 is not a parent of 2.
        self.assertIterRevids(['3', '1.1.1', '2'],
                              stop_revision_id='2', stop_rule='with-merges')

    def test_merge_sorted_range_stop_with_merges_ignore_non_parents(self):
        # 2 is not a parent of 1.1.1 so it must not be returned
        self.assertIterRevids(['3', '1.1.1'],
                              stop_revision_id='1.1.1', stop_rule='with-merges')

    def test_merge_sorted_single_stop_exclude(self):
        # from X..X exclusive is an empty result
        self.assertIterRevids([], start_revision_id='3', stop_revision_id='3')

    def test_merge_sorted_single_stop_include(self):
        # from X..X inclusive is [X]
        self.assertIterRevids(['3'],
                              start_revision_id='3', stop_revision_id='3',
                              stop_rule='include')

    def test_merge_sorted_single_stop_with_merges(self):
        self.assertIterRevids(['3', '1.1.1'],
                              start_revision_id='3', stop_revision_id='3',
                              stop_rule='with-merges')

    def test_merge_sorted_forward(self):
        self.assertIterRevids(['1', '2', '1.1.1', '3'], direction='forward')

    def test_merge_sorted_range_forward(self):
        self.assertIterRevids(['1.1.1'],
                              start_revision_id='1.1.1', stop_revision_id='1',
                              direction='forward')

    def test_merge_sorted_range_start_only_forward(self):
        self.assertIterRevids(['1', '1.1.1'],
                              start_revision_id='1.1.1', direction='forward')

    def test_merge_sorted_range_stop_exclude_forward(self):
        self.assertIterRevids(['2', '1.1.1', '3'],
                              stop_revision_id='1', direction='forward')

    def test_merge_sorted_range_stop_include_forward(self):
        self.assertIterRevids(['2', '1.1.1', '3'],
                              stop_revision_id='2', stop_rule='include',
                              direction='forward')

    def test_merge_sorted_range_stop_with_merges_forward(self):
        self.assertIterRevids(['1.1.1', '3'],
                              stop_revision_id='3', stop_rule='with-merges',
                              direction='forward')


class TestIterMergeSortedRevisionsBushyGraph(per_branch.TestCaseWithBranch):

    def setUp(self):
        super().setUp()
        self.revids = {}

    def make_branch_builder(self, relpath):
        try:
            builder = super().make_branch_builder(relpath)
        except (errors.TransportNotPossible, errors.UninitializableFormat):
            raise tests.TestNotApplicable('format not directly constructable')
        return builder

    def make_snapshot(self, builder, parents, revid_name):
        self.assertNotIn(revid_name, self.revids)
        if parents is None:
            files = [('add', ('', None, 'directory', '')), ]
        else:
            parents = [self.revids[name] for name in parents]
            files = []
        self.revids[revid_name] = builder.build_snapshot(
            parents, files, message="Revision %s" % revid_name)

    def make_branch_with_embedded_merges(self, relpath='.'):
        builder = self.make_branch_builder(relpath)

        # 1
        # |\
        # | 1.1.1
        # | /
        # 2
        # | \
        # 3 |
        # | |
        # | 2.1.1
        # | |    \
        # | 2.1.2 |
        # | |     |
        # | |   2.2.1
        # | |  /
        # | 2.1.3
        # |/
        # 4
        builder.start_series()
        self.make_snapshot(builder, None, '1')
        self.make_snapshot(builder, ['1'], '1.1.1')
        self.make_snapshot(builder, ['1', '1.1.1'], '2')
        self.make_snapshot(builder, ['2'], '2.1.1')
        self.make_snapshot(builder, ['2.1.1'], '2.1.2')
        self.make_snapshot(builder, ['2.1.1'], '2.2.1')
        self.make_snapshot(builder, ['2.1.2', '2.2.1'], '2.1.3')
        self.make_snapshot(builder, ['2'], '3')
        self.make_snapshot(builder, ['3', '2.1.3'], '4')
        builder.finish_series()
        br = builder.get_branch()
        br.lock_read()
        self.addCleanup(br.unlock)
        return br

    def make_branch_with_different_depths_merges(self, relpath='.'):
        builder = self.make_branch_builder(relpath)
        # 1
        # |\
        # | 1.1.1
        # | /
        # 2
        # | \
        # 3 |
        # | |
        # | 2.1.1
        # | |    \
        # | 2.1.2 |
        # | |     |
        # | |     2.2.1
        # | |    /
        # | 2.1.3
        # |/
        # 4
        builder.start_series()
        self.make_snapshot(builder, None, '1')
        self.make_snapshot(builder, ['1'], '2')
        self.make_snapshot(builder, ['1'], '1.1.1')
        self.make_snapshot(builder, ['1.1.1'], '1.1.2')
        self.make_snapshot(builder, ['1.1.1'], '1.2.1')
        self.make_snapshot(builder, ['1.2.1'], '1.2.2')
        self.make_snapshot(builder, ['1.2.1'], '1.3.1')
        self.make_snapshot(builder, ['1.3.1'], '1.3.2')
        self.make_snapshot(builder, ['1.3.1'], '1.4.1')
        self.make_snapshot(builder, ['1.3.2'], '1.3.3')
        self.make_snapshot(builder, ['1.2.2', '1.3.3'], '1.2.3')
        self.make_snapshot(builder, ['2'], '2.1.1')
        self.make_snapshot(builder, ['2.1.1'], '2.1.2')
        self.make_snapshot(builder, ['2.1.1'], '2.2.1')
        self.make_snapshot(builder, ['2.1.2', '2.2.1'], '2.1.3')
        self.make_snapshot(builder, ['2', '1.2.3'], '3')
        # .. to bring them all and ... bind them
        self.make_snapshot(builder, ['3', '2.1.3'], '4')
        builder.finish_series()
        br = builder.get_branch()
        br.lock_read()
        self.addCleanup(br.unlock)
        return br

    def make_branch_with_alternate_ancestries(self, relpath='.'):
        # See test_merge_sorted_exclude_ancestry below for the difference with
        # bt.test_log.TestLogExcludeAncestry.
        # make_branch_with_alternate_ancestries and
        # test_merge_sorted_exclude_ancestry
        # See the FIXME in assertLogRevnos there too.
        builder = self.make_branch_builder(relpath)
        # 1
        # |\
        # | 1.1.1
        # | /| \
        # 2  |  |
        # |  |  1.2.1
        # |  | /
        # |  1.1.2
        # | /
        # 3
        builder.start_series()
        self.make_snapshot(builder, None, '1')
        self.make_snapshot(builder, ['1'], '1.1.1')
        self.make_snapshot(builder, ['1', '1.1.1'], '2')
        self.make_snapshot(builder, ['1.1.1'], '1.2.1')
        self.make_snapshot(builder, ['1.1.1', '1.2.1'], '1.1.2')
        self.make_snapshot(builder, ['2', '1.1.2'], '3')
        builder.finish_series()
        br = builder.get_branch()
        br.lock_read()
        self.addCleanup(br.unlock)
        return br

    def assertIterRevids(self, expected, branch, *args, **kwargs):
        if kwargs.get('stop_revision_id') is not None:
            kwargs['stop_revision_id'] = self.revids[kwargs['stop_revision_id']]
        if kwargs.get('start_revision_id') is not None:
            kwargs['start_revision_id'] = self.revids[kwargs['start_revision_id']]
        # We don't care about depths and revnos here, only about returning the
        # right revids.
        revids = [revid for (revid, depth, revno, eom) in
                  branch.iter_merge_sorted_revisions(*args, **kwargs)]
        self.assertEqual([self.revids[short] for short in expected], revids)

    def test_merge_sorted_starting_at_embedded_merge(self):
        branch = self.make_branch_with_embedded_merges()
        self.assertIterRevids(['4', '2.1.3', '2.2.1', '2.1.2', '2.1.1',
                               '3', '2', '1.1.1', '1'],
                              branch)
        # 3 and 2.1.2 are not part of 2.2.1 ancestry and should not appear
        self.assertIterRevids(['2.2.1', '2.1.1', '2', '1.1.1', '1'],
                              branch, start_revision_id='2.2.1',
                              stop_rule='with-merges')

    def test_merge_sorted_with_different_depths_merge(self):
        branch = self.make_branch_with_different_depths_merges()
        self.assertIterRevids(['4', '2.1.3', '2.2.1', '2.1.2', '2.1.1',
                               '3',
                               '1.2.3', '1.3.3', '1.3.2', '1.3.1',
                               '1.2.2', '1.2.1', '1.1.1',
                               '2', '1'],
                              branch)
        # 3 (and its descendants) and 2.1.2 are not part of 2.2.1 ancestry and
        # should not appear
        self.assertIterRevids(['2.2.1', '2.1.1', '2', '1'],
                              branch, start_revision_id='2.2.1',
                              stop_rule='with-merges')

    def test_merge_sorted_exclude_ancestry(self):
        branch = self.make_branch_with_alternate_ancestries()
        self.assertIterRevids(['3', '1.1.2', '1.2.1', '2', '1.1.1', '1'],
                              branch)
        # '2' is not part of the ancestry even if merge_sort order will make it
        # appear before 1.1.1
        self.assertIterRevids(['1.1.2', '1.2.1'],
                              branch,
                              stop_rule='with-merges-without-common-ancestry',
                              start_revision_id='1.1.2',
                              stop_revision_id='1.1.1')
