import pytest
import mock
import os

from django.utils.timezone import now, timedelta

from awx.main.tasks import (
    RunProjectUpdate, RunInventoryUpdate,
    awx_isolated_heartbeat,
    isolated_manager
)
from awx.main.models import (
    ProjectUpdate, InventoryUpdate, InventorySource,
    Instance, InstanceGroup
)


@pytest.fixture
def scm_revision_file(tmpdir_factory):
    # Returns path to temporary testing revision file
    revision_file = tmpdir_factory.mktemp('revisions').join('revision.txt')
    with open(str(revision_file), 'w') as f:
        f.write('1234567890123456789012345678901234567890')
    return os.path.join(revision_file.dirname, 'revision.txt')


@pytest.mark.django_db
class TestDependentInventoryUpdate:

    def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file):
        task = RunProjectUpdate()
        task.revision_path = scm_revision_file
        proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
        with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
            task.post_run_hook(proj_update, 'successful')
            inv_update_mck.assert_called_once_with(proj_update, mock.ANY)

    def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file):
        task = RunProjectUpdate()
        task.revision_path = scm_revision_file
        proj_update = ProjectUpdate.objects.create(project=project)
        with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck:
            task.post_run_hook(proj_update, 'successful')
            assert not inv_update_mck.called

    def test_dependent_inventory_updates(self, scm_inventory_source):
        task = RunProjectUpdate()
        scm_inventory_source.scm_last_revision = ''
        proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project)
        with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
            task._update_dependent_inventories(proj_update, [scm_inventory_source])
            assert InventoryUpdate.objects.count() == 1
            inv_update = InventoryUpdate.objects.first()
            iu_run_mock.assert_called_once_with(inv_update.id)
            assert inv_update.source_project_update_id == proj_update.pk

    def test_dependent_inventory_project_cancel(self, project, inventory):
        '''
        Test that dependent inventory updates exhibit good behavior on cancel
        of the source project update
        '''
        task = RunProjectUpdate()
        proj_update = ProjectUpdate.objects.create(project=project)

        kwargs = dict(
            source_project=project,
            source='scm',
            source_path='inventory_file',
            update_on_project_update=True,
            inventory=inventory
        )

        is1 = InventorySource.objects.create(name="test-scm-inv", **kwargs)
        is2 = InventorySource.objects.create(name="test-scm-inv2", **kwargs)

        def user_cancels_project(pk):
            ProjectUpdate.objects.all().update(cancel_flag=True)

        with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock:
            iu_run_mock.side_effect = user_cancels_project
            task._update_dependent_inventories(proj_update, [is1, is2])
            # Verify that it bails after 1st update, detecting a cancel
            assert is2.inventory_updates.count() == 0
            iu_run_mock.assert_called_once()



class MockSettings:
    AWX_ISOLATED_PERIODIC_CHECK = 60
    CLUSTER_HOST_ID = 'tower_1'


@pytest.mark.django_db
class TestIsolatedManagementTask:

    @pytest.fixture
    def control_group(self):
        return InstanceGroup.objects.create(name='alpha')

    @pytest.fixture
    def control_instance(self, control_group):
        return control_group.instances.create(hostname='tower_1')

    @pytest.fixture
    def needs_updating(self, control_group):
        ig = InstanceGroup.objects.create(name='thepentagon', controller=control_group)
        inst = ig.instances.create(hostname='isolated', capacity=103)
        inst.last_isolated_check=now() - timedelta(seconds=MockSettings.AWX_ISOLATED_PERIODIC_CHECK)
        inst.save()
        return ig

    @pytest.fixture
    def just_updated(self, control_group):
        ig = InstanceGroup.objects.create(name='thepentagon', controller=control_group)
        inst = ig.instances.create(hostname='isolated', capacity=103)
        inst.last_isolated_check=now()
        inst.save()
        return inst

    @pytest.fixture
    def old_version(self, control_group):
        ig = InstanceGroup.objects.create(name='thepentagon', controller=control_group)
        inst = ig.instances.create(hostname='isolated-old', capacity=103)
        inst.save()
        return inst

    def test_old_version(self, control_instance, old_version):
        update_capacity = isolated_manager.IsolatedManager.update_capacity

        assert old_version.capacity == 103
        with mock.patch('awx.main.tasks.settings', MockSettings()):
            # Isolated node is reporting an older version than the cluster
            # instance that issued the health check, set capacity to zero.
            update_capacity(old_version, {'version': '1.0.0'}, '3.0.0')
            assert old_version.capacity == 0

            # Upgrade was completed, health check playbook now reports matching
            # version, make sure capacity is set.
            update_capacity(old_version, {'version': '5.0.0-things',
                                          'capacity_cpu':103, 'capacity_mem':103}, '5.0.0-stuff')
            assert old_version.capacity == 103

    def test_takes_action(self, control_instance, needs_updating):
        original_isolated_instance = needs_updating.instances.all().first()
        with mock.patch('awx.main.tasks.settings', MockSettings()):
            with mock.patch.object(isolated_manager.IsolatedManager, 'health_check') as check_mock:
                awx_isolated_heartbeat()
        iso_instance = Instance.objects.get(hostname='isolated')
        call_args, _ = check_mock.call_args
        assert call_args[0][0] == iso_instance
        assert iso_instance.last_isolated_check > original_isolated_instance.last_isolated_check
        assert iso_instance.modified == original_isolated_instance.modified

    def test_does_not_take_action(self, control_instance, just_updated):
        with mock.patch('awx.main.tasks.settings', MockSettings()):
            with mock.patch.object(isolated_manager.IsolatedManager, 'health_check') as check_mock:
                awx_isolated_heartbeat()
        iso_instance = Instance.objects.get(hostname='isolated')
        check_mock.assert_not_called()
        assert iso_instance.capacity == 103
