import os
import sys
import copy
import lxml.etree
import subprocess
from mock import Mock, MagicMock, patch
import Bcfg2.Client.Tools
from Bcfg2.Client.Tools.POSIXUsers import *
from Bcfg2.Utils import PackedDigitRange

# add all parent testsuite directories to sys.path to allow (most)
# relative imports in python 2.4
path = os.path.dirname(__file__)
while path != "/":
    if os.path.basename(path).lower().startswith("test"):
        sys.path.append(path)
    if os.path.basename(path) == "testsuite":
        break
    path = os.path.dirname(path)
from common import *
from TestTools.Test_init import TestTool


class TestPOSIXUsers(TestTool):
    test_obj = POSIXUsers

    def get_obj(self, logger=None, setup=None, config=None):
        if setup is None:
            setup = MagicMock()
            def getitem(key):
                if key == 'encoding':
                    return 'UTF-8'
                else:
                    return []

            setup.__getitem__.side_effect = getitem

        return TestTool.get_obj(self, logger, setup, config)

    @patch("pwd.getpwall")
    @patch("grp.getgrall")
    def test_existing(self, mock_getgrall, mock_getpwall):
        users = self.get_obj()
        mock_getgrall.return_value = MagicMock()
        mock_getpwall.return_value = MagicMock()

        def reset():
            mock_getgrall.reset_mock()
            mock_getpwall.reset_mock()

        # make sure we start clean
        self.assertIsNone(users._existing)
        self.assertIsInstance(users.existing, dict)
        self.assertIn("POSIXUser", users.existing)
        self.assertIn("POSIXGroup", users.existing)
        mock_getgrall.assert_called_with()
        mock_getpwall.assert_called_with()

        reset()
        self.assertIsInstance(users._existing, dict)
        self.assertIsInstance(users.existing, dict)
        self.assertEqual(users.existing, users._existing)
        self.assertIn("POSIXUser", users.existing)
        self.assertIn("POSIXGroup", users.existing)
        self.assertFalse(mock_getgrall.called)
        self.assertFalse(mock_getpwall.called)

        reset()
        users._existing = None
        self.assertIsInstance(users.existing, dict)
        self.assertIn("POSIXUser", users.existing)
        self.assertIn("POSIXGroup", users.existing)
        mock_getgrall.assert_called_with()
        mock_getpwall.assert_called_with()

    def test__in_managed_range(self):
        users = self.get_obj()
        users._whitelist = dict(POSIXGroup=PackedDigitRange("1-10"))
        users._blacklist = dict(POSIXGroup=PackedDigitRange("8-100"))
        self.assertTrue(users._in_managed_range("POSIXGroup", "9"))

        users._whitelist = dict(POSIXGroup=None)
        users._blacklist = dict(POSIXGroup=PackedDigitRange("8-100"))
        self.assertFalse(users._in_managed_range("POSIXGroup", "9"))

        users._whitelist = dict(POSIXGroup=None)
        users._blacklist = dict(POSIXGroup=PackedDigitRange("100-"))
        self.assertTrue(users._in_managed_range("POSIXGroup", "9"))

        users._whitelist = dict(POSIXGroup=PackedDigitRange("1-10"))
        users._blacklist = dict(POSIXGroup=None)
        self.assertFalse(users._in_managed_range("POSIXGroup", "25"))

    @patch("Bcfg2.Client.Tools.Tool.canInstall")
    def test_canInstall(self, mock_canInstall):
        users = self.get_obj()
        users._in_managed_range = Mock()
        users._in_managed_range.return_value = False
        mock_canInstall.return_value = False

        def reset():
            users._in_managed_range.reset()
            mock_canInstall.reset()

        # test failure of inherited method
        entry = lxml.etree.Element("POSIXUser", name="test")
        self.assertFalse(users.canInstall(entry))
        mock_canInstall.assertCalledWith(users, entry)

        # test with no uid specified
        reset()
        mock_canInstall.return_value = True
        self.assertTrue(users.canInstall(entry))
        mock_canInstall.assertCalledWith(users, entry)

        # test with uid specified, not in managed range
        reset()
        entry.set("uid", "1000")
        self.assertFalse(users.canInstall(entry))
        mock_canInstall.assertCalledWith(users, entry)
        users._in_managed_range.assert_called_with(entry.tag, "1000")

        # test with uid specified, in managed range
        reset()
        users._in_managed_range.return_value = True
        self.assertTrue(users.canInstall(entry))
        mock_canInstall.assertCalledWith(users, entry)
        users._in_managed_range.assert_called_with(entry.tag, "1000")

    @patch("Bcfg2.Client.Tools.Tool.Inventory")
    def test_Inventory(self, mock_Inventory):
        config = lxml.etree.Element("Configuration")
        bundle = lxml.etree.SubElement(config, "Bundle", name="test")
        lxml.etree.SubElement(bundle, "POSIXUser", name="test", group="test")
        lxml.etree.SubElement(bundle, "POSIXUser", name="test2", group="test2")
        lxml.etree.SubElement(bundle, "POSIXGroup", name="test2")

        orig_bundle = copy.deepcopy(bundle)

        users = self.get_obj(config=config)
        users.set_defaults['POSIXUser'] = Mock()
        users.set_defaults['POSIXUser'].side_effect = lambda e: e

        states = dict()
        self.assertEqual(users.Inventory(states),
                         mock_Inventory.return_value)
        mock_Inventory.assert_called_with(users, states, config.getchildren())
        lxml.etree.SubElement(orig_bundle, "POSIXGroup", name="test")
        self.assertXMLEqual(orig_bundle, bundle)

    def test_FindExtra(self):
        users = self.get_obj()
        users._in_managed_range = Mock()
        users._in_managed_range.side_effect = lambda t, i: i < 100

        def getSupportedEntries():
            return [lxml.etree.Element("POSIXUser", name="test1"),
                    lxml.etree.Element("POSIXGroup", name="test1")]

        users.getSupportedEntries = Mock()
        users.getSupportedEntries.side_effect = getSupportedEntries

        users._existing = dict(POSIXUser=dict(test1=("test1", "x", 15),
                                              test2=("test2", "x", 25),
                                              test3=("test3", "x", 115)),
                               POSIXGroup=dict(test2=("test2", "x", 25)))
        extra = users.FindExtra()
        self.assertEqual(len(extra), 2)
        self.assertItemsEqual([e.tag for e in extra],
                              ["POSIXUser", "POSIXGroup"])
        self.assertItemsEqual([e.get("name") for e in extra],
                              ["test2", "test2"])
        self.assertItemsEqual(users._in_managed_range.call_args_list,
                              [call("POSIXUser", 25),
                               call("POSIXUser", 115),
                               call("POSIXGroup", 25)])

    def test_populate_user_entry(self):
        users = self.get_obj()
        users._existing = dict(POSIXUser=dict(),
                               POSIXGroup=dict(root=('root', 'x', 0, [])))

        cases = [(lxml.etree.Element("POSIXUser", name="test"),
                  lxml.etree.Element("POSIXUser", name="test", group="test",
                                     gecos="test", shell="/bin/bash",
                                     home="/home/test")),
                 (lxml.etree.Element("POSIXUser", name="root", gecos="Root",
                                     shell="/bin/zsh"),
                  lxml.etree.Element("POSIXUser", name="root", group='root',
                                     gid='0', gecos="Root", shell="/bin/zsh",
                                     home='/root')),
                 (lxml.etree.Element("POSIXUser", name="test2", gecos="",
                                     shell="/bin/zsh"),
                  lxml.etree.Element("POSIXUser", name="test2", group='test2',
                                     gecos="", shell="/bin/zsh",
                                     home='/home/test2'))]

        for initial, expected in cases:
            actual = users.populate_user_entry(initial)
            self.assertXMLEqual(actual, expected)

    def test_user_supplementary_groups(self):
        users = self.get_obj()
        users._existing = \
            dict(POSIXUser=dict(),
                 POSIXGroup=dict(root=('root', 'x', 0, []),
                                 wheel=('wheel', 'x', 10, ['test']),
                                 users=('users', 'x', 100, ['test'])))
        entry = lxml.etree.Element("POSIXUser", name="test")
        self.assertItemsEqual(users.user_supplementary_groups(entry),
                              [users.existing['POSIXGroup']['wheel'],
                               users.existing['POSIXGroup']['users']])
        entry.set('name', 'test2')
        self.assertItemsEqual(users.user_supplementary_groups(entry), [])

    def test_VerifyPOSIXUser(self):
        users = self.get_obj()
        users._verify = Mock()
        users._verify.return_value = True
        users.populate_user_entry = Mock()
        users.user_supplementary_groups = Mock()
        users.user_supplementary_groups.return_value = \
            [('wheel', 'x', 10, ['test']), ('users', 'x', 100, ['test'])]

        def reset():
            users._verify.reset_mock()
            users.populate_user_entry.reset_mock()
            users.user_supplementary_groups.reset_mock()

        entry = lxml.etree.Element("POSIXUser", name="test")
        self.assertFalse(users.VerifyPOSIXUser(entry, []))
        users.populate_user_entry.assert_called_with(entry)
        users._verify.assert_called_with(users.populate_user_entry.return_value)
        users.user_supplementary_groups.assert_called_with(entry)

        reset()
        m1 = lxml.etree.SubElement(entry, "MemberOf", group="wheel")
        m2 = lxml.etree.SubElement(entry, "MemberOf")
        m2.text = "users"
        self.assertTrue(users.VerifyPOSIXUser(entry, []))
        users.populate_user_entry.assert_called_with(entry)
        users._verify.assert_called_with(users.populate_user_entry.return_value)
        users.user_supplementary_groups.assert_called_with(entry)

        reset()
        m3 = lxml.etree.SubElement(entry, "MemberOf", group="extra")
        self.assertFalse(users.VerifyPOSIXUser(entry, []))
        users.populate_user_entry.assert_called_with(entry)
        users._verify.assert_called_with(users.populate_user_entry.return_value)
        users.user_supplementary_groups.assert_called_with(entry)

        reset()
        def _verify(entry):
            entry.set("current_exists", "false")
            return False

        users._verify.side_effect = _verify
        self.assertFalse(users.VerifyPOSIXUser(entry, []))
        users.populate_user_entry.assert_called_with(entry)
        users._verify.assert_called_with(users.populate_user_entry.return_value)

    def test_VerifyPOSIXGroup(self):
        users = self.get_obj()
        users._verify = Mock()
        entry = lxml.etree.Element("POSIXGroup", name="test")
        self.assertEqual(users._verify.return_value,
                         users.VerifyPOSIXGroup(entry, []))

    def test__verify(self):
        users = self.get_obj()
        users._existing = \
            dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest',
                                      '/home/test', '/bin/zsh')),
                 POSIXGroup=dict(test=('test', 'x', 1000, [])))

        entry = lxml.etree.Element("POSIXUser", name="nonexistent")
        self.assertFalse(users._verify(entry))
        self.assertEqual(entry.get("current_exists"), "false")

        entry = lxml.etree.Element("POSIXUser", name="test", group="test",
                                   gecos="Bogus", shell="/bin/bash",
                                   home="/home/test")
        self.assertFalse(users._verify(entry))

        entry = lxml.etree.Element("POSIXUser", name="test", group="test",
                                   gecos="Test McTest", shell="/bin/zsh",
                                   home="/home/test")
        self.assertTrue(users._verify(entry))

        entry = lxml.etree.Element("POSIXUser", name="test", group="test",
                                   gecos="Test McTest", shell="/bin/zsh",
                                   home="/home/test", uid="1000", gid="1000")
        self.assertTrue(users._verify(entry))

        entry = lxml.etree.Element("POSIXUser", name="test", group="test",
                                   gecos="Test McTest", shell="/bin/zsh",
                                   home="/home/test", uid="1001")
        self.assertFalse(users._verify(entry))

    def test_Install(self):
        users = self.get_obj()
        users._install = Mock()
        users._existing = MagicMock()


        entries = [lxml.etree.Element("POSIXUser", name="test"),
                   lxml.etree.Element("POSIXGroup", name="test"),
                   lxml.etree.Element("POSIXUser", name="test2")]
        states = dict()

        users.Install(entries, states)
        self.assertItemsEqual(entries, states.keys())
        for state in states.values():
            self.assertEqual(state, users._install.return_value)
        # need to verify two things about _install calls:
        # 1) _install was called for each entry;
        # 2) _install was called for all groups before any users
        self.assertItemsEqual(users._install.call_args_list,
                              [call(e) for e in entries])
        users_started = False
        for args in users._install.call_args_list:
            if args[0][0].tag == "POSIXUser":
                users_started = True
            elif users_started:
                assert False, "_install() called on POSIXGroup after installing one or more POSIXUsers"

    def test__install(self):
        users = self.get_obj()
        users._get_cmd = Mock()
        users.cmd = Mock()
        users.set_defaults = dict(POSIXUser=Mock(), POSIXGroup=Mock())
        users._existing = \
            dict(POSIXUser=dict(test=('test', 'x', 1000, 1000, 'Test McTest',
                                      '/home/test', '/bin/zsh')),
                 POSIXGroup=dict(test=('test', 'x', 1000, [])))

        def reset():
            users._get_cmd.reset_mock()
            users.cmd.reset_mock()
            for setter in users.set_defaults.values():
                setter.reset_mock()
            users.modified = []

        cmd_rv = Mock()
        cmd_rv.success = True
        users.cmd.run.return_value = cmd_rv

        reset()
        entry = lxml.etree.Element("POSIXUser", name="test2")
        self.assertTrue(users._install(entry))
        users.set_defaults[entry.tag].assert_called_with(entry)
        users._get_cmd.assert_called_with("add",
                                          users.set_defaults[entry.tag].return_value)
        users.cmd.run.assert_called_with(users._get_cmd.return_value)
        self.assertIn(entry, users.modified)

        reset()
        entry = lxml.etree.Element("POSIXUser", name="test")
        self.assertTrue(users._install(entry))
        users.set_defaults[entry.tag].assert_called_with(entry)
        users._get_cmd.assert_called_with("mod",
                                          users.set_defaults[entry.tag].return_value)
        users.cmd.run.assert_called_with(users._get_cmd.return_value)
        self.assertIn(entry, users.modified)

        reset()
        cmd_rv.success = False
        self.assertFalse(users._install(entry))
        users.set_defaults[entry.tag].assert_called_with(entry)
        users._get_cmd.assert_called_with("mod",
                                          users.set_defaults[entry.tag].return_value)
        users.cmd.run.assert_called_with(users._get_cmd.return_value)
        self.assertNotIn(entry, users.modified)

    def test__get_cmd(self):
        users = self.get_obj()

        entry = lxml.etree.Element("POSIXUser", name="test", group="test",
                                   home="/home/test", shell="/bin/zsh",
                                   gecos="Test McTest")
        m1 = lxml.etree.SubElement(entry, "MemberOf", group="wheel")
        m2 = lxml.etree.SubElement(entry, "MemberOf")
        m2.text = "users"

        cases = [(lxml.etree.Element("POSIXGroup", name="test"), []),
                 (lxml.etree.Element("POSIXGroup", name="test", gid="1001"),
                  ["-g", "1001"]),
                 (lxml.etree.Element("POSIXUser", name="test", group="test",
                                     home="/home/test", shell="/bin/zsh",
                                     gecos="Test McTest"),
                  ["-g", "test", "-d", "/home/test", "-s", "/bin/zsh",
                   "-c", "Test McTest"]),
                 (lxml.etree.Element("POSIXUser", name="test", group="test",
                                     home="/home/test", shell="/bin/zsh",
                                     gecos="Test McTest", uid="1001"),
                  ["-u", "1001", "-g", "test", "-d", "/home/test",
                   "-s", "/bin/zsh", "-c", "Test McTest"]),
                 (entry,
                  ["-g", "test", "-G", "wheel,users", "-d", "/home/test",
                   "-s", "/bin/zsh", "-c", "Test McTest"])]
        for entry, expected in cases:
            for action in ["add", "mod", "del"]:
                actual = users._get_cmd(action, entry)
                if entry.tag == "POSIXGroup":
                    etype = "group"
                else:
                    etype = "user"
                self.assertEqual(actual[0], "/usr/sbin/%s%s" % (etype, action))
                self.assertEqual(actual[-1], entry.get("name"))
                if action != "del":
                    self.assertItemsEqual(actual[1:-1], expected)

    @patch("grp.getgrnam")
    def test_Remove(self, mock_getgrnam):
        users = self.get_obj()
        users._remove = Mock()
        users.FindExtra = Mock()
        users._existing = MagicMock()
        users.extra = MagicMock()

        def reset():
            users._remove.reset_mock()
            users.FindExtra.reset_mock()
            users._existing = MagicMock()
            users.extra = MagicMock()
            mock_getgrnam.reset_mock()

        entries = [lxml.etree.Element("POSIXUser", name="test"),
                   lxml.etree.Element("POSIXGroup", name="test"),
                   lxml.etree.Element("POSIXUser", name="test2")]

        users.Remove(entries)
        self.assertIsNone(users._existing)
        users.FindExtra.assert_called_with()
        self.assertEqual(users.extra, users.FindExtra.return_value)
        mock_getgrnam.assert_called_with("test")
        # need to verify two things about _remove calls:
        # 1) _remove was called for each entry;
        # 2) _remove was called for all users before any groups
        self.assertItemsEqual(users._remove.call_args_list,
                              [call(e) for e in entries])
        groups_started = False
        for args in users._remove.call_args_list:
            if args[0][0].tag == "POSIXGroup":
                groups_started = True
            elif groups_started:
                assert False, "_remove() called on POSIXUser after removing one or more POSIXGroups"

        reset()
        mock_getgrnam.side_effect = KeyError
        users.Remove(entries)
        self.assertIsNone(users._existing)
        users.FindExtra.assert_called_with()
        self.assertEqual(users.extra, users.FindExtra.return_value)
        mock_getgrnam.assert_called_with("test")
        self.assertItemsEqual(users._remove.call_args_list,
                              [call(e) for e in entries
                               if e.tag == "POSIXUser"])

    def test__remove(self):
        users = self.get_obj()
        users._get_cmd = Mock()
        users.cmd = Mock()
        cmd_rv = Mock()
        cmd_rv.success = True
        users.cmd.run.return_value = cmd_rv

        def reset():
            users._get_cmd.reset_mock()
            users.cmd.reset_mock()

        entry = lxml.etree.Element("POSIXUser", name="test2")
        self.assertTrue(users._remove(entry))
        users._get_cmd.assert_called_with("del", entry)
        users.cmd.run.assert_called_with(users._get_cmd.return_value)

        reset()
        cmd_rv.success = False
        self.assertFalse(users._remove(entry))
        users._get_cmd.assert_called_with("del", entry)
        users.cmd.run.assert_called_with(users._get_cmd.return_value)
