# (c) cavaliba.com - test_iam_group.py

import yaml
from django.test import TestCase, override_settings

import app_home.cache as cache
from app_data.group import Group
from app_data.loader import load_broker
from app_data.models import DataInstance
from app_data.user import User

AAA = {"username": "unittest"}


class GroupFromTest(TestCase):
    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: group
              keyname: group01
              displayname: Group One
            """)
        aaa = {"perms": ["p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_from_keyname(self):
        group = Group.from_keyname(keyname="group01")
        self.assertIsNotNone(group)
        self.assertIsInstance(group, Group)
        self.assertEqual(group.keyname, "group01")

    def test_from_keyname_missing(self):
        group = Group.from_keyname(keyname="does_not_exist")
        self.assertIsNone(group)

    def test_from_id(self):
        ref = Group.from_keyname(keyname="group01")
        group = Group.from_id(id=ref.id)
        self.assertIsNotNone(group)
        self.assertIsInstance(group, Group)
        self.assertEqual(group.keyname, "group01")

    def test_from_id_missing(self):
        group = Group.from_id(id=999999)
        self.assertIsNone(group)

    def test_from_iobj(self):
        iobj = DataInstance.objects.filter(classname="group", keyname="group01").first()
        self.assertIsNotNone(iobj)
        group = Group.from_iobj(iobj=iobj)
        self.assertIsNotNone(group)
        self.assertIsInstance(group, Group)
        self.assertEqual(group.keyname, "group01")

    def test_displayname(self):
        group = Group.from_keyname(keyname="group01")
        self.assertEqual(group.displayname, "Group One")


class GroupCRUDTest(TestCase):
    def setUp(self):
        cache.clear()

    def test_create(self):
        group = Group(keyname="group_create")
        group.displayname = "Create Test"
        result = group.save(aaa=AAA, action="create")
        self.assertTrue(result)
        self.assertTrue(group.is_bound)
        found = Group.from_keyname(keyname="group_create")
        self.assertIsNotNone(found)
        self.assertEqual(found.displayname, "Create Test")

    def test_update(self):
        group = Group(keyname="group_update")
        group.save(aaa=AAA, action="create")
        group.displayname = "Updated Name"
        result = group.save(aaa=AAA, action="update")
        self.assertTrue(result)
        found = Group.from_keyname(keyname="group_update")
        self.assertEqual(found.displayname, "Updated Name")

    def test_delete(self):
        group = Group(keyname="group_delete")
        group.save(aaa=AAA, action="create")
        self.assertTrue(group.is_bound)
        result = group.delete()
        self.assertTrue(result)
        self.assertFalse(group.is_bound)
        found = Group.from_keyname(keyname="group_delete")
        self.assertIsNone(found)

    def test_enable(self):
        group = Group(keyname="group_enable")
        group.is_enabled = False
        group.save(aaa=AAA, action="create")
        result = group.enable(aaa=AAA)
        self.assertTrue(result)
        self.assertTrue(group.is_enabled)
        found = Group.from_keyname(keyname="group_enable")
        self.assertTrue(found.is_enabled)

    def test_disable(self):
        group = Group(keyname="group_disable")
        group.save(aaa=AAA, action="create")
        self.assertTrue(group.is_enabled)
        result = group.disable(aaa=AAA)
        self.assertTrue(result)
        self.assertFalse(group.is_enabled)
        found = Group.from_keyname(keyname="group_disable")
        self.assertFalse(found.is_enabled)


class GroupGetSubgroupsTest(TestCase):
    """
    Hierarchy:
        parent  subgroups: [child1, child2]
        child1  subgroups: [grandchild]
        child2  (leaf)
        grandchild  (leaf)
        unrelated  (no relation to parent)
    """

    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: group
              keyname: grandchild
              displayname: Grandchild

            - classname: group
              keyname: child1
              displayname: Child One
              subgroups:
                - grandchild

            - classname: group
              keyname: child2
              displayname: Child Two

            - classname: group
              keyname: parent
              displayname: Parent
              subgroups:
                - child1
                - child2

            - classname: group
              keyname: unrelated
              displayname: Unrelated
            """)
        aaa = {"perms": ["p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_returns_list(self):
        g = Group.from_keyname(keyname="parent")
        self.assertIsInstance(g.get_subgroups(), list)

    def test_direct_subgroups_included(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {s.keyname for s in g.get_subgroups()}
        self.assertIn("child1", keynames)
        self.assertIn("child2", keynames)

    def test_transitive_subgroup_included(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {s.keyname for s in g.get_subgroups()}
        self.assertIn("grandchild", keynames)

    def test_unrelated_excluded(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {s.keyname for s in g.get_subgroups()}
        self.assertNotIn("unrelated", keynames)

    def test_self_not_in_subgroups(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {s.keyname for s in g.get_subgroups()}
        self.assertNotIn("parent", keynames)

    def test_no_duplicates(self):
        g = Group.from_keyname(keyname="parent")
        keynames = [s.keyname for s in g.get_subgroups()]
        self.assertEqual(len(keynames), len(set(keynames)))

    def test_leaf_group_returns_empty(self):
        g = Group.from_keyname(keyname="child2")
        self.assertEqual(g.get_subgroups(), [])

    def test_returns_group_instances(self):
        g = Group.from_keyname(keyname="parent")
        for s in g.get_subgroups():
            self.assertIsInstance(s, Group)
            self.assertTrue(s.is_bound)

    def test_sets_class_variable(self):
        g = Group.from_keyname(keyname="parent")
        self.assertIsNone(g.subgroups)
        g.get_subgroups()
        self.assertIsNotNone(g.subgroups)


class GroupGetDirectUsersTest(TestCase):
    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: user01
              displayname: User One

            - classname: user
              keyname: user02
              displayname: User Two

            - classname: group
              keyname: grp_with_users
              displayname: Group With Users
              users:
                - user01
                - user02

            - classname: group
              keyname: grp_empty
              displayname: Group Empty
            """)
        aaa = {"perms": ["p_user_create", "p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_returns_list(self):
        g = Group.from_keyname(keyname="grp_with_users")
        self.assertIsInstance(g.get_direct_users(), list)

    def test_finds_direct_users(self):
        g = Group.from_keyname(keyname="grp_with_users")
        keynames = {u.keyname for u in g.get_direct_users()}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_empty_group_returns_empty(self):
        g = Group.from_keyname(keyname="grp_empty")
        self.assertEqual(g.get_direct_users(), [])

    def test_no_duplicates(self):
        g = Group.from_keyname(keyname="grp_with_users")
        keynames = [u.keyname for u in g.get_direct_users()]
        self.assertEqual(len(keynames), len(set(keynames)))

    def test_returns_user_instances(self):
        g = Group.from_keyname(keyname="grp_with_users")
        for u in g.get_direct_users():
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    def test_sets_class_variable(self):
        g = Group.from_keyname(keyname="grp_with_users")
        self.assertIsNone(g.direct_users)
        g.get_direct_users()
        self.assertIsNotNone(g.direct_users)


class GroupGetAutoUsersTest(TestCase):
    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: user01
              displayname: User One

            - classname: user
              keyname: user02
              displayname: User Two

            - classname: group
              keyname: grp_auto
              displayname: Group Auto
              autogroup_users:
                - user01
                - user02

            - classname: group
              keyname: grp_empty
              displayname: Group Empty
            """)
        aaa = {"perms": ["p_user_create", "p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_returns_list(self):
        g = Group.from_keyname(keyname="grp_auto")
        self.assertIsInstance(g.get_auto_users(), list)

    def test_finds_auto_users(self):
        g = Group.from_keyname(keyname="grp_auto")
        keynames = {u.keyname for u in g.get_auto_users()}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_empty_group_returns_empty(self):
        g = Group.from_keyname(keyname="grp_empty")
        self.assertEqual(g.get_auto_users(), [])

    def test_no_duplicates(self):
        g = Group.from_keyname(keyname="grp_auto")
        keynames = [u.keyname for u in g.get_auto_users()]
        self.assertEqual(len(keynames), len(set(keynames)))

    def test_returns_user_instances(self):
        g = Group.from_keyname(keyname="grp_auto")
        for u in g.get_auto_users():
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    def test_sets_class_variable(self):
        g = Group.from_keyname(keyname="grp_auto")
        self.assertIsNone(g.auto_users)
        g.get_auto_users()
        self.assertIsNotNone(g.auto_users)


class GroupGetUsersTest(TestCase):
    """
    grp_mixed  users: [user01, user02]  autogroup_users: [user02, user03]
    user02 in both — dedup check.
    """

    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: user01
              displayname: User One

            - classname: user
              keyname: user02
              displayname: User Two

            - classname: user
              keyname: user03
              displayname: User Three

            - classname: group
              keyname: grp_mixed
              displayname: Group Mixed
              users:
                - user01
                - user02
              autogroup_users:
                - user02
                - user03

            - classname: group
              keyname: grp_empty
              displayname: Group Empty
            """)
        aaa = {"perms": ["p_user_create", "p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_returns_list(self):
        g = Group.from_keyname(keyname="grp_mixed")
        self.assertIsInstance(g.get_users(), list)

    def test_includes_direct_users(self):
        g = Group.from_keyname(keyname="grp_mixed")
        keynames = {u.keyname for u in g.get_users()}
        self.assertIn("user01", keynames)

    def test_includes_auto_users(self):
        g = Group.from_keyname(keyname="grp_mixed")
        keynames = {u.keyname for u in g.get_users()}
        self.assertIn("user03", keynames)

    def test_no_duplicates_across_sources(self):
        g = Group.from_keyname(keyname="grp_mixed")
        keynames = [u.keyname for u in g.get_users()]
        self.assertEqual(len(keynames), len(set(keynames)))
        self.assertEqual(keynames.count("user02"), 1)

    def test_empty_group_returns_empty(self):
        g = Group.from_keyname(keyname="grp_empty")
        self.assertEqual(g.get_users(), [])

    def test_returns_user_instances(self):
        g = Group.from_keyname(keyname="grp_mixed")
        for u in g.get_users():
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    def test_sets_class_variable(self):
        g = Group.from_keyname(keyname="grp_mixed")
        self.assertIsNone(g.users)
        g.get_users()
        self.assertIsNotNone(g.users)


class GroupGetUsersAllTest(TestCase):
    """
    Hierarchy:
        parent    users: [user01]   subgroups: [child]
          child   users: [user02]   autogroup_users: [user01, user03]
                  subgroups: [grandchild]
            grandchild  users: [user04]
        unrelated users: [user05]
    user01 in both parent.users and child.autogroup_users — cross-group dedup check.
    """

    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: user01
              displayname: User One

            - classname: user
              keyname: user02
              displayname: User Two

            - classname: user
              keyname: user03
              displayname: User Three

            - classname: user
              keyname: user04
              displayname: User Four

            - classname: user
              keyname: user05
              displayname: User Five

            - classname: group
              keyname: grandchild
              displayname: Grandchild
              users:
                - user04

            - classname: group
              keyname: child
              displayname: Child
              users:
                - user02
              autogroup_users:
                - user01
                - user03
              subgroups:
                - grandchild

            - classname: group
              keyname: parent
              displayname: Parent
              users:
                - user01
              subgroups:
                - child

            - classname: group
              keyname: unrelated
              displayname: Unrelated
              users:
                - user05
            """)
        aaa = {"perms": ["p_user_create", "p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    def test_returns_list(self):
        g = Group.from_keyname(keyname="parent")
        self.assertIsInstance(g.get_users_all(), list)

    def test_includes_self_users(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertIn("user01", keynames)

    def test_includes_subgroup_direct_users(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertIn("user02", keynames)

    def test_includes_subgroup_auto_users(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertIn("user03", keynames)

    def test_includes_transitive_subgroup_users(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertIn("user04", keynames)

    def test_excludes_unrelated_group_users(self):
        g = Group.from_keyname(keyname="parent")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertNotIn("user05", keynames)

    def test_no_duplicates_across_groups(self):
        g = Group.from_keyname(keyname="parent")
        keynames = [u.keyname for u in g.get_users_all()]
        self.assertEqual(len(keynames), len(set(keynames)))
        self.assertEqual(keynames.count("user01"), 1)

    def test_leaf_group_returns_own_users_only(self):
        g = Group.from_keyname(keyname="grandchild")
        keynames = {u.keyname for u in g.get_users_all()}
        self.assertEqual(keynames, {"user04"})

    def test_returns_user_instances(self):
        g = Group.from_keyname(keyname="parent")
        for u in g.get_users_all():
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    def test_sets_class_variable(self):
        g = Group.from_keyname(keyname="parent")
        self.assertIsNone(g.users_all)
        g.get_users_all()
        self.assertIsNotNone(g.users_all)


class GroupMaxComputedUserTest(TestCase):
    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: userA
              displayname: User A

            - classname: user
              keyname: userB
              displayname: User B

            - classname: user
              keyname: userC
              displayname: User C

            - classname: group
              keyname: grp_limit
              displayname: Group Limit
              users:
                - userA
                - userB
                - userC
            """)
        aaa = {"perms": ["p_user_create", "p_group_create"]}
        load_broker(datalist=datalist, aaa=aaa)

    @override_settings(CAVALIBA_MAX_COMPUTED_USER=2)
    def test_direct_users_capped_at_max(self):
        g = Group.from_keyname(keyname="grp_limit")
        result = g.get_direct_users()
        self.assertLessEqual(len(result), 2)

    @override_settings(CAVALIBA_MAX_COMPUTED_USER=2)
    def test_users_capped_at_max(self):
        g = Group.from_keyname(keyname="grp_limit")
        result = g.get_users()
        self.assertLessEqual(len(result), 2)


class GroupGetUsersComputedTest(TestCase):
    """
    Schema layout:
        project.owner  -> user   (G1 target: project:*:owner)
            proj1.owner = [user01]
            proj2.owner = [user02]
            proj3.owner = [user01]   <- duplicate of proj1, tests deduplication

        team.leader    -> user   (G2 target: team:team1:*)
        team.deputy    -> user
        team.codename  -> string  <- non-user field, must be ignored by G2
            team1.leader = [user01]
            team1.deputy = [user02]
            team1.codename = alpha

        grp_g1   autogroup: 'project:*:owner'
        grp_g2   autogroup: 'team:team1:*'
        grp_none autogroup: (empty)
        grp_bad  autogroup: 'bad_grammar'
    """

    def setUp(self):
        cache.clear()
        datalist = yaml.safe_load("""
            - classname: user
              keyname: user01
              displayname: User One

            - classname: user
              keyname: user02
              displayname: User Two

            - classname: user
              keyname: user03
              displayname: User Three

            - classname: _schema
              keyname: project
              displayname: Project
              owner:
                displayname: Owner
                dataformat: schema
                dataformat_ext: user
                cardinal_max: 0

            - classname: project
              keyname: proj1
              owner:
                - user01

            - classname: project
              keyname: proj2
              owner:
                - user02

            - classname: project
              keyname: proj3
              owner:
                - user01

            - classname: _schema
              keyname: team
              displayname: Team
              leader:
                displayname: Leader
                dataformat: schema
                dataformat_ext: user
              deputy:
                displayname: Deputy
                dataformat: schema
                dataformat_ext: user
              codename:
                displayname: Codename
                dataformat: string

            - classname: team
              keyname: team1
              leader: user01
              deputy: user02
              codename: alpha

            - classname: group
              keyname: grp_g1
              displayname: Group G1
              autogroup: 'project:*:owner'

            - classname: group
              keyname: grp_g2
              displayname: Group G2
              autogroup: 'team:team1:*'

            - classname: group
              keyname: grp_none
              displayname: Group No Autogroup

            - classname: group
              keyname: grp_bad
              displayname: Group Bad Grammar
              autogroup: 'bad_grammar'
            """)
        aaa = {"perms": ["p_user_create", "p_group_create", "p_schema_write", "p_data_admin"]}
        load_broker(datalist=datalist, aaa=aaa)

    # -----------------------------------
    # get_users_computed_g1
    # -----------------------------------

    def test_g1_returns_list(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "owner")
        self.assertIsInstance(result, list)

    def test_g1_finds_users_from_all_instances(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "owner")
        keynames = {u.keyname for u in result}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_g1_deduplicates_users(self):
        # user01 appears in proj1 and proj3 — must appear once
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "owner")
        keynames = [u.keyname for u in result]
        self.assertEqual(keynames.count("user01"), 1)

    def test_g1_excludes_non_member_users(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "owner")
        keynames = {u.keyname for u in result}
        self.assertNotIn("user03", keynames)

    def test_g1_unknown_schema_returns_empty(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("no_such_schema", "owner")
        self.assertEqual(result, [])

    def test_g1_unknown_fieldname_returns_empty(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "no_such_field")
        self.assertEqual(result, [])

    def test_g1_returns_user_instances(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.get_users_computed_g1("project", "owner")
        for u in result:
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    # -----------------------------------
    # get_users_computed_g2
    # -----------------------------------

    def test_g2_returns_list(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("team", "team1")
        self.assertIsInstance(result, list)

    def test_g2_finds_users_from_all_user_fields(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("team", "team1")
        keynames = {u.keyname for u in result}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_g2_ignores_non_user_fields(self):
        # codename is a string field — its value must not appear as a user
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("team", "team1")
        keynames = {u.keyname for u in result}
        self.assertNotIn("alpha", keynames)

    def test_g2_unknown_instance_returns_empty(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("team", "no_such_instance")
        self.assertEqual(result, [])

    def test_g2_unknown_schema_returns_empty(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("no_such_schema", "team1")
        self.assertEqual(result, [])

    def test_g2_returns_user_instances(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.get_users_computed_g2("team", "team1")
        for u in result:
            self.assertIsInstance(u, User)
            self.assertTrue(u.is_bound)

    # -----------------------------------
    # autogroup_recompute (dispatcher)
    # -----------------------------------

    def test_computed_dispatches_to_g1(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.autogroup_recompute()
        keynames = {u.keyname for u in result}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_computed_dispatches_to_g2(self):
        g = Group.from_keyname(keyname="grp_g2")
        result = g.autogroup_recompute()
        keynames = {u.keyname for u in result}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_computed_empty_autogroup_returns_empty(self):
        g = Group.from_keyname(keyname="grp_none")
        result = g.autogroup_recompute()
        self.assertEqual(result, [])

    def test_computed_invalid_grammar_returns_empty(self):
        g = Group.from_keyname(keyname="grp_bad")
        result = g.autogroup_recompute()
        self.assertEqual(result, [])

    def test_computed_returns_list(self):
        g = Group.from_keyname(keyname="grp_g1")
        result = g.autogroup_recompute()
        self.assertIsInstance(result, list)

    # -----------------------------------
    # autogroup_update
    # -----------------------------------

    def test_autogroup_update_returns_count_of_groups_with_users(self):
        # grp_g1 and grp_g2 have autogroup rules that yield users; grp_none and grp_bad yield none
        count = Group.autogroup_update()
        self.assertEqual(count, 2)

    def test_autogroup_update_persists_autogroup_users(self):
        Group.autogroup_update()
        g = Group.from_keyname(keyname="grp_g1")
        stored = g.get_attribute("autogroup_users")
        # get_attribute returns DataInstance objects; compare by keyname
        keynames = {obj.keyname for obj in stored if obj}
        self.assertIn("user01", keynames)
        self.assertIn("user02", keynames)

    def test_autogroup_update_with_specific_group(self):
        g = Group.from_keyname(keyname="grp_g1")
        count = Group.autogroup_update(groups=[g])
        self.assertEqual(count, 1)

    def test_autogroup_update_empty_list_updates_all(self):
        count = Group.autogroup_update(groups=[])
        self.assertEqual(count, 2)

    def test_autogroup_update_group_without_rule_stores_empty(self):
        Group.autogroup_update()
        g = Group.from_keyname(keyname="grp_none")
        stored = g.get_attribute("autogroup_users")
        self.assertEqual(stored, [])
