import pytest
from unittest import mock

from awx.main.models import AdHocCommand, InventoryUpdate, Job, JobTemplate, ProjectUpdate
from awx.main.models.ha import Instance, InstanceGroup
from awx.main.tasks import apply_cluster_membership_policies
from awx.api.versioning import reverse

from django.utils.timezone import now


@pytest.mark.django_db
def test_default_tower_instance_group(default_instance_group, job_factory):
    assert default_instance_group in job_factory().preferred_instance_groups


@pytest.mark.django_db
class TestPolicyTaskScheduling:
    """Tests make assertions about when the policy task gets scheduled"""

    @pytest.mark.parametrize('field, value, expect', [
        ('name', 'foo-bar-foo-bar', False),
        ('policy_instance_percentage', 35, True),
        ('policy_instance_minimum', 3, True),
        ('policy_instance_list', ['bar?'], True),
        ('modified', now(), False)
    ])
    def test_policy_task_ran_for_ig_when_needed(self, instance_group_factory, field, value, expect):
        # always run on instance group creation
        with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy:
            ig = InstanceGroup.objects.create(name='foo')
        mock_policy.assert_called_once()
        # selectively run on instance group modification
        with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy:
            setattr(ig, field, value)
            ig.save()
        if expect:
            mock_policy.assert_called_once()
        else:
            mock_policy.assert_not_called()

    @pytest.mark.parametrize('field, value, expect', [
        ('hostname', 'foo-bar-foo-bar', True),
        ('managed_by_policy', False, True),
        ('enabled', False, False),
        ('capacity_adjustment', 0.42, True),
        ('capacity', 42, False)
    ])
    def test_policy_task_ran_for_instance_when_needed(self, instance_group_factory, field, value, expect):
        # always run on instance group creation
        with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy:
            inst = Instance.objects.create(hostname='foo')
        mock_policy.assert_called_once()
        # selectively run on instance group modification
        with mock.patch('awx.main.models.ha.schedule_policy_task') as mock_policy:
            setattr(inst, field, value)
            inst.save()
        if expect:
            mock_policy.assert_called_once()
        else:
            mock_policy.assert_not_called()


@pytest.mark.django_db
def test_instance_dup(org_admin, organization, project, instance_factory, instance_group_factory, get, system_auditor):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    ig_all = instance_group_factory("all", instances=[i1, i2, i3])
    ig_dup = instance_group_factory("duplicates", instances=[i1])
    project.organization.instance_groups.add(ig_all, ig_dup)
    actual_num_instances = Instance.objects.active_count()
    list_response = get(reverse('api:instance_list'), user=system_auditor)
    api_num_instances_auditor = list(list_response.data.items())[0][1]

    list_response2 = get(reverse('api:instance_list'), user=org_admin)
    api_num_instances_oa = list(list_response2.data.items())[0][1]

    assert actual_num_instances == api_num_instances_auditor
    # Note: The org_admin will not see the default 'tower' node because it is not in it's group, as expected
    assert api_num_instances_oa == (actual_num_instances - 1)


@pytest.mark.django_db
def test_policy_instance_few_instances(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    ig_1 = instance_group_factory("ig1", percentage=25)
    ig_2 = instance_group_factory("ig2", percentage=25)
    ig_3 = instance_group_factory("ig3", percentage=25)
    ig_4 = instance_group_factory("ig4", percentage=25)
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 1
    assert i1 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i1 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i1 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i1 in ig_4.instances.all()
    i2 = instance_factory("i2")
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 1
    assert i1 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i2 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i1 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i2 in ig_4.instances.all()


@pytest.mark.django_db
def test_policy_instance_distribution_round_up(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    i4 = instance_factory("i4")
    i5 = instance_factory("i5")
    ig_1 = instance_group_factory("ig1", percentage=79)
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 4
    assert set([i1, i2, i3, i4]) == set(ig_1.instances.all())
    assert i5 not in ig_1.instances.all()


@pytest.mark.django_db
def test_policy_instance_distribution_uneven(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    ig_1 = instance_group_factory("ig1", percentage=25)
    ig_2 = instance_group_factory("ig2", percentage=25)
    ig_3 = instance_group_factory("ig3", percentage=25)
    ig_4 = instance_group_factory("ig4", percentage=25)
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 1
    assert i1 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i2 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i3 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i1 in ig_4.instances.all()


@pytest.mark.django_db
def test_policy_instance_distribution_even(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    i4 = instance_factory("i4")
    ig_1 = instance_group_factory("ig1", percentage=25)
    ig_2 = instance_group_factory("ig2", percentage=25)
    ig_3 = instance_group_factory("ig3", percentage=25)
    ig_4 = instance_group_factory("ig4", percentage=25)
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 1
    assert i1 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i2 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i3 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i4 in ig_4.instances.all()
    ig_1.policy_instance_minimum = 2
    ig_1.save()
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 2
    assert i1 in ig_1.instances.all()
    assert i2 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i3 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i4 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i1 in ig_4.instances.all()


@pytest.mark.django_db
def test_policy_instance_distribution_simultaneous(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    i4 = instance_factory("i4")
    ig_1 = instance_group_factory("ig1", percentage=25, minimum=2)
    ig_2 = instance_group_factory("ig2", percentage=25)
    ig_3 = instance_group_factory("ig3", percentage=25)
    ig_4 = instance_group_factory("ig4", percentage=25)
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 2
    assert i1 in ig_1.instances.all()
    assert i2 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i3 in ig_2.instances.all()
    assert len(ig_3.instances.all()) == 1
    assert i4 in ig_3.instances.all()
    assert len(ig_4.instances.all()) == 1
    assert i1 in ig_4.instances.all()


@pytest.mark.django_db
def test_policy_instance_list_manually_assigned(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    ig_1 = instance_group_factory("ig1", percentage=100, minimum=2)
    ig_2 = instance_group_factory("ig2")
    ig_2.policy_instance_list = [i2.hostname]
    ig_2.save()
    apply_cluster_membership_policies()
    assert len(ig_1.instances.all()) == 2
    assert i1 in ig_1.instances.all()
    assert i2 in ig_1.instances.all()
    assert len(ig_2.instances.all()) == 1
    assert i2 in ig_2.instances.all()


@pytest.mark.django_db
def test_policy_instance_list_explicitly_pinned(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    ig_1 = instance_group_factory("ig1", percentage=100, minimum=2)
    ig_2 = instance_group_factory("ig2")
    ig_2.policy_instance_list = [i2.hostname]
    ig_2.save()

    # without being marked as manual, i2 will be picked up by ig_1
    apply_cluster_membership_policies()
    assert set(ig_1.instances.all()) == set([i1, i2])
    assert set(ig_2.instances.all()) == set([i2])

    i2.managed_by_policy = False
    i2.save()

    # after marking as manual, i2 no longer available for ig_1
    apply_cluster_membership_policies()
    assert set(ig_1.instances.all()) == set([i1])
    assert set(ig_2.instances.all()) == set([i2])


@pytest.mark.django_db
def test_basic_instance_group_membership(instance_group_factory, default_instance_group, job_factory):
    j = job_factory()
    ig = instance_group_factory("basicA", [default_instance_group.instances.first()])
    j.job_template.instance_groups.add(ig)
    assert ig in j.preferred_instance_groups
    assert default_instance_group not in j.preferred_instance_groups


@pytest.mark.django_db
def test_inherited_instance_group_membership(instance_group_factory, default_instance_group, job_factory, project, inventory):
    j = job_factory()
    j.project = project
    j.inventory = inventory
    ig_org = instance_group_factory("basicA", [default_instance_group.instances.first()])
    ig_inv = instance_group_factory("basicB", [default_instance_group.instances.first()])
    j.project.organization.instance_groups.add(ig_org)
    j.inventory.instance_groups.add(ig_inv)
    assert ig_org in j.preferred_instance_groups
    assert ig_inv in j.preferred_instance_groups
    assert default_instance_group not in j.preferred_instance_groups


@pytest.mark.django_db
def test_mixed_group_membership(instance_factory, instance_group_factory):
    for i in range(5):
        instance_factory("i{}".format(i))
    ig_1 = instance_group_factory("ig1", percentage=60)
    ig_2 = instance_group_factory("ig2", minimum=3)
    ig_3 = instance_group_factory("ig3", minimum=1, percentage=60)
    apply_cluster_membership_policies()
    for group in (ig_1, ig_2, ig_3):
        assert len(group.instances.all()) == 3


@pytest.mark.django_db
def test_instance_group_capacity(instance_factory, instance_group_factory):
    i1 = instance_factory("i1")
    i2 = instance_factory("i2")
    i3 = instance_factory("i3")
    ig_all = instance_group_factory("all", instances=[i1, i2, i3])
    assert ig_all.capacity == 300
    ig_single = instance_group_factory("single", instances=[i1])
    assert ig_single.capacity == 100


@pytest.mark.django_db
class TestInstanceGroupOrdering:

    def test_ad_hoc_instance_groups(self, instance_group_factory, inventory, default_instance_group):
        ad_hoc = AdHocCommand.objects.create(inventory=inventory)
        assert ad_hoc.preferred_instance_groups == [default_instance_group]
        ig_org = instance_group_factory("OrgIstGrp", [default_instance_group.instances.first()])
        ig_inv = instance_group_factory("InvIstGrp", [default_instance_group.instances.first()])
        inventory.organization.instance_groups.add(ig_org)
        assert ad_hoc.preferred_instance_groups == [ig_org]
        inventory.instance_groups.add(ig_inv)
        assert ad_hoc.preferred_instance_groups == [ig_inv, ig_org]

    def test_inventory_update_instance_groups(self, instance_group_factory, inventory_source, default_instance_group):
        iu = InventoryUpdate.objects.create(inventory_source=inventory_source)
        assert iu.preferred_instance_groups == [default_instance_group]
        ig_org = instance_group_factory("OrgIstGrp", [default_instance_group.instances.first()])
        ig_inv = instance_group_factory("InvIstGrp", [default_instance_group.instances.first()])
        ig_tmp = instance_group_factory("TmpIstGrp", [default_instance_group.instances.first()])
        inventory_source.inventory.organization.instance_groups.add(ig_org)
        inventory_source.inventory.instance_groups.add(ig_inv)
        assert iu.preferred_instance_groups == [ig_inv, ig_org]
        inventory_source.instance_groups.add(ig_tmp)
        # API does not allow setting IGs on inventory source, so ignore those
        assert iu.preferred_instance_groups == [ig_inv, ig_org]

    def test_project_update_instance_groups(self, instance_group_factory, project, default_instance_group):
        pu = ProjectUpdate.objects.create(project=project)
        assert pu.preferred_instance_groups == [default_instance_group]
        ig_org = instance_group_factory("OrgIstGrp", [default_instance_group.instances.first()])
        ig_tmp = instance_group_factory("TmpIstGrp", [default_instance_group.instances.first()])
        project.organization.instance_groups.add(ig_org)
        assert pu.preferred_instance_groups == [ig_org]
        project.instance_groups.add(ig_tmp)
        assert pu.preferred_instance_groups == [ig_tmp, ig_org]

    def test_job_instance_groups(self, instance_group_factory, inventory, project, default_instance_group):
        jt = JobTemplate.objects.create(inventory=inventory, project=project)
        job = Job.objects.create(inventory=inventory, job_template=jt, project=project)
        assert job.preferred_instance_groups == [default_instance_group]
        ig_org = instance_group_factory("OrgIstGrp", [default_instance_group.instances.first()])
        ig_inv = instance_group_factory("InvIstGrp", [default_instance_group.instances.first()])
        ig_tmp = instance_group_factory("TmpIstGrp", [default_instance_group.instances.first()])
        project.organization.instance_groups.add(ig_org)
        inventory.instance_groups.add(ig_inv)
        assert job.preferred_instance_groups == [ig_inv, ig_org]
        job.job_template.instance_groups.add(ig_tmp)
        assert job.preferred_instance_groups == [ig_tmp, ig_inv, ig_org]
