# Copyright (C) 2005-2012, 2016 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 of bound branches (binding, unbinding, commit, etc) command."""

from breezy import branch, controldir, errors, tests
from breezy.tests import script


class TestBoundBranches(tests.TestCaseWithTransport):

    def create_branches(self):
        base_tree = self.make_branch_and_tree('base')
        base_tree.lock_write()
        self.build_tree(['base/a', 'base/b'])
        base_tree.add(['a', 'b'])
        base_tree.commit('init')
        base_tree.unlock()

        child_tree = base_tree.branch.create_checkout('child')

        self.check_revno(1, 'child')
        d = controldir.ControlDir.open('child')
        self.assertNotEqual(None, d.open_branch().get_master_branch())

        return base_tree, child_tree

    def check_revno(self, val, loc='.'):
        self.assertEqual(
            val, controldir.ControlDir.open(loc).open_branch().last_revision_info()[0])

    def test_simple_binding(self):
        tree = self.make_branch_and_tree('base')
        self.build_tree(['base/a', 'base/b'])
        tree.add('a', ids=b'b')
        tree.commit(message='init')

        tree.controldir.sprout('child')

        self.run_bzr('bind ../base', working_dir='child')

        d = controldir.ControlDir.open('child')
        self.assertNotEqual(None, d.open_branch().get_master_branch())

        self.run_bzr('unbind', working_dir='child')
        self.assertEqual(None, d.open_branch().get_master_branch())

        self.run_bzr('unbind', retcode=3, working_dir='child')

    def test_bind_branch6(self):
        branch1 = self.make_branch('branch1', format='dirstate-tags')
        error = self.run_bzr('bind', retcode=3, working_dir='branch1')[1]
        self.assertEndsWith(
            error, 'No location supplied and no previous location known\n')

    def setup_rebind(self, format):
        branch1 = self.make_branch('branch1')
        branch2 = self.make_branch('branch2', format=format)
        branch2.bind(branch1)
        branch2.unbind()

    def test_rebind_branch6(self):
        self.setup_rebind('dirstate-tags')
        self.run_bzr('bind', working_dir='branch2')
        b = branch.Branch.open('branch2')
        self.assertEndsWith(b.get_bound_location(), '/branch1/')

    def test_rebind_branch5(self):
        self.setup_rebind('knit')
        error = self.run_bzr('bind', retcode=3, working_dir='branch2')[1]
        self.assertEndsWith(
            error, 'No location supplied.  This format does not remember'
            ' old locations.\n')

    def test_bound_commit(self):
        child_tree = self.create_branches()[1]

        self.build_tree_contents([('child/a', b'new contents')])
        child_tree.commit(message='child')

        self.check_revno(2, 'child')

        # Make sure it committed on the parent
        self.check_revno(2, 'base')

    def test_bound_fail(self):
        # Make sure commit fails if out of date.
        base_tree, child_tree = self.create_branches()

        self.build_tree_contents([
            ('base/a', b'new base contents\n'),
            ('child/b', b'new b child contents\n')])
        base_tree.commit(message='base')
        self.check_revno(2, 'base')

        self.check_revno(1, 'child')
        self.assertRaises(errors.BoundBranchOutOfDate, child_tree.commit,
                          message='child')
        self.check_revno(1, 'child')

        child_tree.update()
        self.check_revno(2, 'child')

        child_tree.commit(message='child')
        self.check_revno(3, 'child')
        self.check_revno(3, 'base')

    def test_double_binding(self):
        child_tree = self.create_branches()[1]
        child_tree.controldir.sprout('child2')

        # Double binding succeeds, but committing to child2 should fail
        self.run_bzr('bind ../child', working_dir='child2')

        # Refresh the child tree object as 'unbind' modified it
        child2_tree = controldir.ControlDir.open('child2').open_workingtree()
        self.assertRaises(errors.CommitToDoubleBoundBranch,
                          child2_tree.commit, message='child2', allow_pointless=True)

    def test_unbinding(self):
        base_tree, child_tree = self.create_branches()

        self.build_tree_contents([
            ('base/a', b'new base contents\n'),
            ('child/b', b'new b child contents\n')])

        base_tree.commit(message='base')
        self.check_revno(2, 'base')

        self.check_revno(1, 'child')
        self.run_bzr("commit -m child", retcode=3, working_dir='child')
        self.check_revno(1, 'child')
        self.run_bzr('unbind', working_dir='child')
        # Refresh the child tree/branch objects as 'unbind' modified them
        child_tree = child_tree.controldir.open_workingtree()
        child_tree.commit(message='child')
        self.check_revno(2, 'child')

    def test_commit_remote_bound(self):
        # It is not possible to commit to a branch
        # which is bound to a branch which is bound
        base_tree, child_tree = self.create_branches()
        base_tree.controldir.sprout('newbase')

        # There is no way to know that B has already
        # been bound by someone else, otherwise it
        # might be nice if this would fail
        self.run_bzr('bind ../newbase', working_dir='base')

        self.run_bzr('commit -m failure --unchanged', retcode=3,
                     working_dir='child')

    def test_pull_updates_both(self):
        base_tree = self.create_branches()[0]
        newchild_tree = base_tree.controldir.sprout(
            'newchild').open_workingtree()
        self.build_tree_contents([('newchild/b', b'newchild b contents\n')])
        newchild_tree.commit(message='newchild')
        self.check_revno(2, 'newchild')

        # The pull should succeed, and update
        # the bound parent branch
        self.run_bzr('pull ../newchild', working_dir='child')
        self.check_revno(2, 'child')
        self.check_revno(2, 'base')

    def test_pull_local_updates_local(self):
        base_tree = self.create_branches()[0]
        newchild_tree = base_tree.controldir.sprout(
            'newchild').open_workingtree()
        self.build_tree_contents([('newchild/b', b'newchild b contents\n')])
        newchild_tree.commit(message='newchild')
        self.check_revno(2, 'newchild')

        # The pull should succeed, and update
        # the bound parent branch
        self.run_bzr('pull ../newchild --local', working_dir='child')
        self.check_revno(2, 'child')
        self.check_revno(1, 'base')

    def test_bind_diverged(self):
        base_tree, child_tree = self.create_branches()
        base_branch = base_tree.branch
        child_branch = child_tree.branch

        self.run_bzr('unbind', working_dir='child')

        # Refresh the child tree/branch objects as 'unbind' modified them
        child_tree = child_tree.controldir.open_workingtree()
        child_tree.commit(message='child', allow_pointless=True)
        self.check_revno(2, 'child')

        self.check_revno(1, 'base')
        base_tree.commit(message='base', allow_pointless=True)
        self.check_revno(2, 'base')

        # These branches have diverged, but bind should succeed anyway
        self.run_bzr('bind ../base', working_dir='child')

        # Refresh the child tree/branch objects as 'bind' modified them
        child_tree = child_tree.controldir.open_workingtree()
        # This should turn the local commit into a merge
        child_tree.update()
        child_tree.commit(message='merged')
        self.check_revno(3, 'child')
        self.assertEqual(child_tree.branch.last_revision(),
                         base_tree.branch.last_revision())

    def test_bind_parent_ahead(self):
        base_tree = self.create_branches()[0]

        self.run_bzr('unbind', working_dir='child')

        base_tree.commit(message='base', allow_pointless=True)

        self.check_revno(1, 'child')
        self.run_bzr('bind ../base', working_dir='child')

        # binding does not pull data:
        self.check_revno(1, 'child')
        self.run_bzr('unbind', working_dir='child')

        # Check and make sure it also works if parent is ahead multiple
        base_tree.commit(message='base 3', allow_pointless=True)
        base_tree.commit(message='base 4', allow_pointless=True)
        base_tree.commit(message='base 5', allow_pointless=True)
        self.check_revno(5, 'base')

        self.check_revno(1, 'child')
        self.run_bzr('bind ../base', working_dir='child')
        self.check_revno(1, 'child')

    def test_bind_child_ahead(self):
        # test binding when the master branches history is a prefix of the
        # childs - it should bind ok but the revision histories should not
        # be altered
        child_tree = self.create_branches()[1]

        self.run_bzr('unbind', working_dir='child')
        # Refresh the child tree/branch objects as 'bind' modified them
        child_tree = child_tree.controldir.open_workingtree()
        child_tree.commit(message='child', allow_pointless=True)
        self.check_revno(2, 'child')
        self.check_revno(1, 'base')

        self.run_bzr('bind ../base', working_dir='child')
        self.check_revno(1, 'base')

        # Check and make sure it also works if child is ahead multiple
        self.run_bzr('unbind', working_dir='child')
        child_tree.commit(message='child 3', allow_pointless=True)
        child_tree.commit(message='child 4', allow_pointless=True)
        child_tree.commit(message='child 5', allow_pointless=True)
        self.check_revno(5, 'child')

        self.check_revno(1, 'base')
        self.run_bzr('bind ../base', working_dir='child')
        self.check_revno(1, 'base')

    def test_bind_fail_if_missing(self):
        """We should not be able to bind to a missing branch."""
        tree = self.make_branch_and_tree('tree_1')
        tree.commit('dummy commit')
        self.run_bzr_error(['Not a branch.*no-such-branch/'],
                           ['bind', '../no-such-branch'],
                           working_dir='tree_1')
        self.assertIs(None, tree.branch.get_bound_location())

    def test_commit_after_merge(self):
        base_tree, child_tree = self.create_branches()

        # We want merge to be able to be a local only
        # operation, because it can be without violating
        # the binding invariants.
        # But we can't fail afterwards
        other_tree = child_tree.controldir.sprout('other').open_workingtree()
        other_branch = other_tree.branch

        self.build_tree_contents([('other/c', b'file c\n')])
        other_tree.add('c')
        other_tree.commit(message='adding c')
        new_rev_id = other_branch.last_revision()

        child_tree.merge_from_branch(other_branch)

        self.assertPathExists('child/c')
        self.assertEqual([new_rev_id], child_tree.get_parent_ids()[1:])

        # Make sure the local branch has the installed revision
        self.assertTrue(child_tree.branch.repository.has_revision(new_rev_id))

        # And make sure that the base tree does not
        self.assertFalse(base_tree.branch.repository.has_revision(new_rev_id))

        # Commit should succeed, and cause merged revisions to
        # be pulled into base
        self.run_bzr(['commit', '-m', 'merge other'], working_dir='child')
        self.check_revno(2, 'child')
        self.check_revno(2, 'base')
        self.assertTrue(base_tree.branch.repository.has_revision(new_rev_id))

    def test_pull_overwrite(self):
        # XXX: This test should be moved to branch-implemenations/test_pull
        child_tree = self.create_branches()[1]

        other_tree = child_tree.controldir.sprout('other').open_workingtree()

        self.build_tree_contents([('other/a', b'new contents\n')])
        other_tree.commit(message='changed a')
        self.check_revno(2, 'other')
        self.build_tree_contents([
            ('other/a', b'new contents\nand then some\n')])
        other_tree.commit(message='another a')
        self.check_revno(3, 'other')
        self.build_tree_contents([
            ('other/a', b'new contents\nand then some\nand some more\n')])
        other_tree.commit('yet another a')
        self.check_revno(4, 'other')

        self.build_tree_contents([('child/a', b'also changed a\n')])
        child_tree.commit(message='child modified a')

        self.check_revno(2, 'child')
        self.check_revno(2, 'base')

        self.run_bzr('pull --overwrite ../other', working_dir='child')

        # both the local and master should have been updated.
        self.check_revno(4, 'child')
        self.check_revno(4, 'base')

    def test_bind_directory(self):
        """Test --directory option"""
        tree = self.make_branch_and_tree('base')
        self.build_tree(['base/a', 'base/b'])
        tree.add('a', ids=b'b')
        tree.commit(message='init')
        branch = tree.branch
        tree.controldir.sprout('child')
        self.run_bzr('bind --directory=child base')
        d = controldir.ControlDir.open('child')
        self.assertNotEqual(None, d.open_branch().get_master_branch())
        self.run_bzr('unbind -d child')
        self.assertEqual(None, d.open_branch().get_master_branch())
        self.run_bzr('unbind --directory child', retcode=3)


class TestBind(script.TestCaseWithTransportAndScript):

    def test_bind_when_bound(self):
        self.run_script("""
$ brz init trunk
...
$ brz init copy
...
$ cd copy
$ brz bind ../trunk
$ brz bind
2>brz: ERROR: Branch is already bound
""")

    def test_bind_before_bound(self):
        self.run_script("""
$ brz init trunk
...
$ cd trunk
$ brz bind
2>brz: ERROR: No location supplied and no previous location known
""")
