#!/usr/bin/env python
from __future__ import with_statement

import sys, os
if __name__ == '__main__':
    sys.path = ([os.path.dirname(os.path.dirname(os.path.dirname(
        os.path.realpath(__file__))))] + sys.path)
    
__requires__='Bazki'
import pkg_resources
pkg_resources.require('Bazki')

import unittest, warnings
import subprocess, shutil
import tempfile

from paste.script import command

from bazbase import custom, testing, db, conversion, structure
import bazsvn

verbose = '-v' in sys.argv

def run(cmd):
    if not verbose:
        cmd.append('-q')
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    if p.returncode != 0:
        raise EnvironmentError(p.returncode, err)

here = os.path.abspath(os.path.dirname(__file__))

class BazsvnWithBazbase(unittest.TestCase):

    def expectRunGetsStatus(self, cmd, status, message):
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        out, err = p.communicate()
        self.assertEquals(status, p.returncode)
        self.assertTrue(message in err, err)

    def setUp(self):
        self.cwd = os.getcwd()

        testing.set_up_for_tests()
        self.old_vcs_hook = custom.version_control_hook
        custom.version_control_hook = bazsvn.hook
        
        # Do this directly to bypass the bazsvn hook.
        from bazsql import model
        model.session.begin()
        model.clear()
        model.session.commit()
        model.session.close()

        conversion.invalidate_cache()

        bazsvn.hook.clear_checkouts()
        
        self.repo = tempfile.mkdtemp(suffix='.testrepo')
        bazsvn.custom.REPOSITORY = self.repo
        subprocess.check_call(['svnadmin', 'create', self.repo])
        os.symlink(os.path.join(here, 'pre-commit'),
                   os.path.join(self.repo, 'hooks', 'pre-commit'))
        
        import_cmd = ['svn', 'import', '--username', 'bazki', '-m',
                      'Bootstrapping...',
                      os.path.join(here, 'db'),
                      'file://' + self.repo]
        run(import_cmd)
        self.checkout = tempfile.mkdtemp(suffix='.testcheckout')
        run(['svn', 'co', "file://%s" % self.repo, self.checkout])
        os.chdir(self.checkout)
        
    def tearDown(self):
        shutil.rmtree(self.checkout)
        shutil.rmtree(self.repo)
        os.chdir(self.cwd)
        custom.version_control_hook = self.old_vcs_hook

    def test_add_child(self):
        path = 'Object/Test/UnitTest.yaml'
        with open(path, 'w') as outf:
            outf.write(u"""comment: |
  This was created\u2014in a unit test!
name: George
""".encode('utf-8'))
        run(['svn', 'add', path])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"UnitTest")
            self.assertEquals(u"George", unicode(e.get_prop(u'name'), 'utf-8'))
            self.assertEquals(u"This was created\u2014in a unit test!\n",
                              unicode(e.get_prop(u'comment'), 'utf-8'))
            self.assertEquals(u"Test", e.get_parent().ename)

    def test_add_propval(self):
        path = 'Object/Test/Test.yaml'
        with open(path, 'a') as outf:
            outf.write("""name: Paul
""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Test")
            self.assertEquals(u"Paul", unicode(e.get_prop(u'name'), 'utf-8'))
            # Shouls also add the prop to its kids
            e2 = structure.get_element(u"Sandbox")
            # Defaults to ename only if parent's was empty or its ename
            self.assertEquals(u"<<parent.this />>",
                              unicode(e2.get_prop(u'name'), 'utf-8'))

    def test_add_propval_default_ename(self):
        path = 'Object/Test/Test.yaml'
        with open(path, 'a') as outf:
            outf.write("""name: Test
""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Test")
            self.assertEquals(u"Test", unicode(e.get_prop(u'name'), 'utf-8'))
            # Shouls also add the prop to its kids
            e2 = structure.get_element(u"Sandbox")
            # Defaults to ename only if parent's was empty or its ename
            self.assertEquals(u"Sandbox",
                              unicode(e2.get_prop(u'name'), 'utf-8'))

    def test_add_propval_default_parent_dot_this(self):
        path = 'Object/Test/Test.yaml'
        with open(path, 'a') as outf:
            outf.write("""product: Paul
""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Test")
            self.assertEquals(u"Paul", unicode(e.get_prop(u'product'), 'utf-8'))
            # Shouls also add the prop to its kids
            e2 = structure.get_element(u"Sandbox")
            self.assertEquals(u"Paul", conversion.render(e2, u'product'))

    def test_remove_propval(self):
        path = 'Object/Test/MergeTest.yaml'
        with open(path, 'w') as outf:
            outf.write("""comment: This test edits the comment and removes the 'test' prop value.
""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"MergeTest")
            self.assertTrue(e.has_propval(u'comment'))
            self.assertEquals(u"This test edits the comment and removes the 'test' prop value.", unicode(e.get_prop(u'comment'), 'utf-8'))
            self.assertFalse(e.has_propval(u'test'))

    def test_add_child_with_leaf_directory_fails(self):
        dirpath = 'Object/Test/UnitTest'
        os.mkdir(dirpath)
        path = dirpath + '/UnitTest.yaml'
        with open(path, 'w') as outf:
            outf.write("""comment: |
  This was created in a unit test.
name: George
""")
        run(['svn', 'add', dirpath])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                      "If UnitTest has no kids, it needs to be in the form UnitTest.yaml, not UnitTest/UnitTest.yaml!")
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"UnitTest"))

    def test_add_non_toplevel_child_with_parent_fails(self):
        dirpath = 'Object/Test'
        path = dirpath + '/UnitTest.yaml'
        with open(path, 'w') as outf:
            outf.write("""parent: Test
comment: |
  This was created in a unit test.
name: George
""")
        run(['svn', 'add', path])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                      "'parent' specified in non-toplevel element 'UnitTest'!")
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"UnitTest"))

    def test_remove_child(self):
        path = 'Object/Test/MergeTest.yaml'
        run(['svn', 'rm', path])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"MergeTest"))

    def test_remove_last_child(self):
        path = 'Object/User/Admin/Daemon.yaml'
        parent_from = 'Object/User/Admin/Admin.yaml'
        parent_to = 'Object/User/Admin.yaml'
        parent_dir = 'Object/User/Admin/'
        run(['svn', 'rm', path])
        run(['svn', 'mv', parent_from, parent_to])
        run(['svn', 'rm', parent_dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals("User", structure.get_element(u"Admin").get_parent().ename)
            self.assertEquals(None, structure.get_element(u"Daemon"))

    def test_remove_last_child_without_removing_leaf_directory_fails(self):
        path = 'Object/User/Admin/Daemon.yaml'
        run(['svn', 'rm', path])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                      "If Admin has no kids, it needs to be in the form Admin.yaml, not Admin/Admin.yaml!")
        # These should still work.
        with db.begin_transaction():
            self.assertEquals(u"Admin",
                              structure.get_element(u"Daemon").get_parent().ename)

    def test_move_subtree(self):
        path_from = 'Object/User/Admin'
        path_to = 'Object/Test/Admin'
        parent_dir = 'Object/User'
        parent_from = 'Object/User/User.yaml'
        parent_to = 'Object/User.yaml'
        run(['svn', 'mv', path_from, path_to])
        run(['svn', 'mv', parent_from, parent_to])
        run(['svn', 'rm', parent_dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals("Test", structure.get_element(u"Admin").get_parent().ename)
            self.assertEquals("Admin",
                              structure.get_element(u"Daemon").get_parent().ename)

    def test_move_to_toplevel(self):
        path_from = 'Object/User/Admin'
        path_to = 'Admin'
        path_to_yaml = 'Admin/Admin.yaml'
        parent_dir = 'Object/User'
        run(['svn', 'mv', path_from, path_to])
        with open(path_to_yaml, 'w') as outf:
            outf.write('parent: User\ncomment: whatever')
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals("User", structure.get_element(u"Admin").get_parent().ename)
            self.assertEquals("Admin",
                              structure.get_element(u"Daemon").get_parent().ename)

    def test_remove_subtree(self):
        path = 'Object/Test'
        run(['svn', 'rm', path])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"Test"))
            self.assertEquals(None, structure.get_element(u"Sandbox"))

    def test_remove_last_subtree(self):
        path = 'Object/User/Admin'
        parent_dir = 'Object/User'
        parent_from = 'Object/User/User.yaml'
        parent_to = 'Object/User.yaml'
        run(['svn', 'rm', path])
        run(['svn', 'mv', parent_from, parent_to])
        run(['svn', 'rm', parent_dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"Admin"))
            self.assertEquals(0,
                              len(structure.get_element(u"User")
                                  .get_children()))

    def test_remove_last_subtree_without_removing_leaf_directory_fails(self):
        path = 'Object/User/Admin'
        run(['svn', 'rm', path])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "If User has no kids, it needs to be in the form User.yaml, not User/User.yaml!")
        with db.begin_transaction():
            # These should still work.
            self.assertEquals(u"User",
                              structure.get_element(u"Admin").get_parent().ename)
            self.assertEquals(u"Admin",
                              structure.get_element(u"Daemon").get_parent().ename)

    def test_add_subtree(self):
        dir = 'Object/Test/UnitTest/'
        os.mkdir(dir)
        path = 'Object/Test/UnitTest/UnitTest.yaml'
        with open(path, 'w') as outf:
            outf.write("""comment: |
  This was created in a unit test.
name: George
""")
        path2 = 'Object/Test/UnitTest/TestTwo.yaml'
        with open(path2, 'w') as outf:
            outf.write("""comment: |
  Also created in a unit test.
name: Ringo
""")
        run(['svn', 'add', dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"UnitTest")
            self.assertEquals(u"George", unicode(e.get_prop(u'name'), 'utf-8'))
            e2 = structure.get_element(u"TestTwo")
            self.assertEquals(u"Ringo", unicode(e2.get_prop(u'name'), 'utf-8'))
            self.assertEquals(u"This was created in a unit test.\n",
                              unicode(e.get_prop(u'comment'), 'utf-8'))
            self.assertEquals(u"Test", e.get_parent().ename)
            self.assertEquals(e, e2.get_parent())
    
    def test_add_include(self):
        path = 'Object/Test/Test.yaml'
        include = 'Object/Test/existent.txt'
        with open(path, 'a') as outf:
            outf.write("""name: !include existent.txt
""")
        with open(include, 'w') as incf:
            incf.write("""Paul""")
        run(['svn', 'add', include])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Test")
            self.assertEquals(u"Paul", unicode(e.get_prop(u'name'), 'utf-8'))

    def test_add_broken_include_fails(self):
        path = 'Object/Test/Test.yaml'
        with open(path, 'a') as outf:
            outf.write("""name: !include nonexistent.txt
""")
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "Couldn't read include 'nonexistent.txt'")

    def test_remove_include(self):
        path = 'Object/Test/IncludeTest.yaml'
        include = 'Object/Test/product.txt'
        with open(path, 'w') as outf:
            outf.write("""comment: No include.""")
        run(['svn', 'rm', include])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertTrue(structure.get_element(u"IncludeTest")
                            .has_propval(u'comment'))
            self.assertFalse(structure.get_element(u"IncludeTest")
                             .has_propval(u'product'))

    def test_remove_include_without_removing_file(self):
        path = 'Object/Test/IncludeTest.yaml'
        include = 'Object/Test/product.txt'
        with open(path, 'w') as outf:
            outf.write("""comment: No include.""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertTrue(structure.get_element(u"IncludeTest")
                            .has_propval(u'comment'))
            self.assertFalse(structure.get_element(u"IncludeTest")
                             .has_propval(u'product'))

    def test_remove_file_without_removing_include_fails(self):
        path = 'Object/Test/IncludeTest.yaml'
        include = 'Object/Test/product.txt'
        run(['svn', 'rm', include])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "Couldn't read include 'product.txt' for "
                                 "IncludeTest.product!")

    def test_change_include(self):
        path = 'Object/Test/IncludeTest.yaml'
        include = 'Object/Test/product.txt'
        with open(include, 'w') as incf:
            incf.write("""42""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"IncludeTest")
            self.assertEquals("42", unicode(e.get_prop(u"product"), 'utf-8'))

    def test_include_to_inline(self):
        path = 'Object/Test/IncludeTest.yaml'
        include = 'Object/Test/product.txt'
        with open(path, 'w') as outf:
            outf.write("""comment: Now inline.
product: 17
""")
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"IncludeTest")
            self.assertEquals("17", unicode(e.get_prop(u"product"), 'utf-8'))

    def test_inline_to_include(self):
        path = 'Object/Test/MergeTest.yaml'
        include = 'Object/Test/existent.txt'
        with open(path, 'w') as outf:
            outf.write("""comment: Now with an include!
test: !include existent.txt
""")
        with open(include, 'w') as incf:
            incf.write("""The banner should be Blue!""")
        run(['svn', 'add', include])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"MergeTest")
            self.assertEquals(u"The banner should be Blue!",
                              unicode(e.get_prop(u'test'), 'utf-8'))

    def test_top_level_lone_element(self):
        dir = 'Fish/'
        os.mkdir(dir)
        path = 'Fish/Fish.yaml'
        with open(path, 'w') as outf:
            outf.write("parent: Object\ncomment: Tree\n")
        run(['svn', 'add', dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Fish")
            self.assertEquals(u'Tree', unicode(e.get_prop(u'comment'), 'utf-8'))
            self.assertEquals(u'Object', e.get_parent().ename)

            # Bazbase change
            e.set_prop(u'comment', 'Wine')
        run(['svn', 'up'])
        self.assertTrue(os.path.exists('Fish/Fish.yaml'))
        self.assertFalse(os.path.exists('Object/Fish.yaml'))
        self.assertFalse(os.path.exists('Object/Fish/Fish.yaml'))
        with open(path) as inf:
            self.assertEquals("parent: Object\ncomment: Wine\n",
                              inf.read())

    def test_top_level_without_parent_fails(self):
        dir = 'Fish/'
        os.mkdir(dir)
        path = 'Fish/Fish.yaml'
        with open(path, 'w') as outf:
            outf.write("comment: Tree\n")
        run(['svn', 'add', dir])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "'parent' not specified at start of toplevel "
                                 "element Fish/Fish.yaml!")

    def test_top_level_with_kid(self):
        dir = 'Fish/'
        os.mkdir(dir)
        path = 'Fish/Fish.yaml'
        with open(path, 'w') as outf:
            outf.write("parent: Object\ncomment: Tree\n")
        path2 = 'Fish/Tree.yaml'
        with open(path2, 'w') as outf:
            outf.write("comment: Pine\n")
        run(['svn', 'add', dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            e = structure.get_element(u"Tree")
            self.assertEquals(u'Pine', unicode(e.get_prop(u'comment'), 'utf-8'))
            self.assertEquals(u'Fish', e.get_parent().ename)
            self.assertEquals(u'Object', e.get_parent().get_parent().ename)

    def test_non_toplevel_yaml_not_under_object(self):
        dir = 'Not.Object/'
        os.mkdir(dir)
        path = 'Not.Object/Whatever.yaml'
        with open(path, 'w') as outf:
            outf.write("It shouldn't matter that this isn't valid yaml.")
        run(['svn', 'add', dir])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"Whatever"))
        
    def test_bare_yaml(self):
        path = 'Whatever.yaml'
        with open(path, 'w') as outf:
            outf.write("It shouldn't matter that this isn't valid yaml.")
        run(['svn', 'add', path])
        run(['svn', 'ci', '-m', 'unit test'])
        with db.begin_transaction():
            self.assertEquals(None, structure.get_element(u"Whatever"))

    def test_overly_nested_element_fails(self):
        dir = 'Object/Test/Dir/'
        os.mkdir(dir)
        path = 'Object/Test/Dir/Something.yaml'
        with open(path, 'w') as outf:
            outf.write("""comment: Whatever.
""")
        run(['svn', 'add', dir])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "Parent 'Dir' for element 'Something' "
                                 "undefined!")

    def test_moving_root_fails(self):
        object_from = 'Object'
        object_to = 'Pizza'
        run(['svn', 'mv', object_from, object_to])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "You can't delete the root element.")

    def test_moving_root_and_renaming_def_fails(self):
        object_from = 'Object'
        object_to = 'Pizza'
        def_from = 'Pizza/Object.yaml'
        def_to = 'Pizza/Pizza.yaml'
        run(['svn', 'mv', object_from, object_to])
        run(['svn', 'mv', def_from, def_to])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "You can't delete the root element.")

    def test_deleting_root_dir_fails(self):
        path = 'Object'
        run(['svn', 'rm', path])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "You can't delete the root element.")
        
    def test_deleting_root_def_fails(self):
        path = 'Object/Object.yaml'
        run(['svn', 'rm', path])
        self.expectRunGetsStatus(['svn', 'ci', '-m', 'unit test'], 1,
                                 "You can't delete the root element.")

    def test_rename_from_bazbase(self):
        with db.begin_transaction():
            sb = structure.get_element(u'Sandbox')
            sb.set_ename(u'Dustbox')
        run(['svn', 'up'])
        self.assertTrue(os.path.exists('Object/Test/Dustbox.yaml'))
        self.assertFalse(os.path.exists('Object/Test/Sandbox.yaml'))

if __name__ == "__main__":
    unittest.main()
