# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from oslo_utils import encodeutils
from tempest.lib import decorators

from zun_tempest_plugin.tests.tempest.api import clients
from zun_tempest_plugin.tests.tempest.api.common import datagen
from zun_tempest_plugin.tests.tempest import base
from zun_tempest_plugin.tests.tempest import utils


class TestContainer(base.BaseZunTest):

    credentials = ['primary', 'admin']
    min_microversion = '1.12'

    @classmethod
    def get_client_manager(cls, credential_type=None, roles=None,
                           force_new=None):
        manager = super(TestContainer, cls).get_client_manager(
            credential_type=credential_type,
            roles=roles,
            force_new=force_new
        )
        return clients.Manager(manager.credentials)

    @classmethod
    def setup_clients(cls):
        super(TestContainer, cls).setup_clients()
        cls.container_client = cls.os_primary.container_client
        cls.docker_client = clients.DockerClient()
        cls.images_client = cls.os_primary.images_client
        cls.ports_client = cls.os_primary.ports_client
        cls.sgs_client = cls.os_primary.sgs_client

    @classmethod
    def resource_setup(cls):
        super(TestContainer, cls).resource_setup()

    def setUp(self):
        super(TestContainer, self).setUp()
        self.containers = []

    def tearDown(self):
        hosts = []
        _, model = self.os_admin.container_client.list_containers(
            params={'all_projects': True})
        for c in model.containers:
            if c['uuid'] in self.containers:
                if c['host'] and c['host'] not in hosts:
                    hosts.append(c['host'])
                # NOTE(kiennt): From version 1.7, Zun disallowed non-admin
                #               users to force delete containers. Therefore,
                #               we have to be admin to do this action.
                self.os_admin.container_client.delete_container(
                    c['uuid'],
                    params={'force': True, 'all_projects': True})
                self.container_client.ensure_container_deleted(c['uuid'])

        # cleanup the network resources
        project_id = self.container_client.tenant_id
        for host in hosts:
            # NOTE(kiennt): Default docker remote url
            #               Remove networks in all hosts
            docker_base_url = self._get_docker_url(host=host)
            networks = self.docker_client.list_networks(project_id,
                                                        docker_base_url)
            for network in networks:
                self.docker_client.remove_network(network['Id'],
                                                  docker_base_url)

        super(TestContainer, self).tearDown()

    @decorators.idempotent_id('b8946b8c-57d5-4fdc-a09a-001d6b552725')
    def test_create_container(self):
        self._create_container()

    @decorators.idempotent_id('b3e307d4-844b-4a57-8c60-8fb3f57aea7c')
    def test_list_containers(self):
        _, container = self._create_container()
        resp, model = self.container_client.list_containers()
        self.assertEqual(200, resp.status)
        self.assertGreater(len(model.containers), 0)
        self.assertIn(
            container.uuid,
            list([c['uuid'] for c in model.containers]))

    @decorators.idempotent_id('0dd13c28-c5ff-4b9e-b73b-61185b410de4')
    def test_get_container(self):
        _, container = self._create_container()
        resp, model = self.container_client.get_container(container.uuid)
        self.assertEqual(200, resp.status)
        self.assertEqual(container.uuid, model.uuid)

    @decorators.idempotent_id('cef53a56-22b7-4808-b01c-06b2b7126115')
    def test_delete_container(self):
        _, container = self._create_container()
        docker_url = self._get_docker_url(container)
        resp, _ = self.container_client.delete_container(container.uuid)
        self.assertEqual(204, resp.status)
        self.container_client.ensure_container_deleted(container.uuid)
        container = self.docker_client.get_container(
            container.uuid, docker_url)
        self.assertIsNone(container)

    @decorators.idempotent_id('ef69c9e7-0ce0-4e14-b7ec-c1dc581a3927')
    def test_run_container(self):
        self._run_container()

    @decorators.idempotent_id('a2152d78-b6a6-4f47-8767-d83d29c6fb19')
    def test_run_container_with_minimal_params(self):
        gen_model = datagen.container_data({'image': 'nginx'})
        self._run_container(gen_model=gen_model)

    @decorators.idempotent_id('c32f93e3-da88-4c13-be38-25d2e662a28e')
    def test_run_container_with_image_driver_glance(self):
        image = None
        try:
            docker_base_url = self._get_docker_url()
            self.docker_client.pull_image(
                'cirros', docker_auth_url=docker_base_url)
            image_data = self.docker_client.get_image(
                'cirros', docker_base_url)
            image = self.images_client.create_image(
                name='cirros', disk_format='raw', container_format='docker')
            self.images_client.store_image_file(image['id'], image_data)
            # delete the local image that was previously pulled down
            self.docker_client.delete_image('cirros', docker_base_url)

            _, model = self._run_container(
                image='cirros', image_driver='glance')
        finally:
            if image:
                try:
                    self.images_client.delete_image(image['id'])
                except Exception:
                    pass

    @decorators.idempotent_id('b70bedbc-5ba2-400c-8f5f-0cf05ca17151')
    def test_run_container_with_environment(self):
        _, model = self._run_container(environment={
            'key1': 'env1', 'key2': 'env2'})

        container = self.docker_client.get_container(
            model.uuid,
            self._get_docker_url(model))
        env = container.get('Config').get('Env')
        self.assertTrue('key1=env1', env)
        self.assertTrue('key2=env2', env)

    @decorators.idempotent_id('0e59d549-58ff-440f-8704-10e223c31cbc')
    def test_run_container_with_labels(self):
        _, model = self._run_container(labels={
            'key1': 'label1', 'key2': 'label2'})

        container = self.docker_client.get_container(
            model.uuid,
            self._get_docker_url(model))
        labels = container.get('Config').get('Labels')
        self.assertTrue('key1=label1', labels)
        self.assertTrue('key2=label2', labels)

    @decorators.idempotent_id('9fc7fec0-e1a9-4f65-a5a6-dba425c1607c')
    def test_run_container_with_restart_policy(self):
        _, model = self._run_container(restart_policy={
            'Name': 'on-failure', 'MaximumRetryCount': 2})

        container = self.docker_client.get_container(
            model.uuid,
            self._get_docker_url(model))
        policy = container.get('HostConfig').get('RestartPolicy')
        self.assertEqual('on-failure', policy['Name'])
        self.assertTrue(2, policy['MaximumRetryCount'])

    @decorators.idempotent_id('58585a4f-cdce-4dbd-9741-4416d1098f94')
    def test_run_container_with_interactive(self):
        _, model = self._run_container(interactive=True)

        container = self.docker_client.get_container(
            model.uuid,
            self._get_docker_url(model))
        tty = container.get('Config').get('Tty')
        stdin_open = container.get('Config').get('OpenStdin')
        self.assertIs(True, tty)
        self.assertIs(True, stdin_open)

    @decorators.idempotent_id('f181eeda-a9d1-4b2e-9746-d6634ca81e2f')
    def test_run_container_without_security_groups(self):
        gen_model = datagen.container_data()
        delattr(gen_model, 'security_groups')
        _, model = self._run_container(gen_model=gen_model)
        sgs = self._get_all_security_groups(model)
        self.assertEqual(1, len(sgs))
        self.assertEqual('default', sgs[0])

    @decorators.idempotent_id('f181eeda-a9d1-4b2e-9746-d6634ca81e2f')
    def test_run_container_with_security_groups(self):
        sg_name = 'test_sg'
        self.sgs_client.create_security_group(name=sg_name)
        _, model = self._run_container(security_groups=[sg_name])
        sgs = self._get_all_security_groups(model)
        self.assertEqual(1, len(sgs))
        self.assertEqual(sg_name, sgs[0])

    @decorators.idempotent_id('c3f02fa0-fdfb-49fc-95e2-6e4dc982f9be')
    def test_commit_container(self):
        """Test container snapshot

        This test does the following:
        1. Create a container
        2. Create and write to a file inside the container
        3. Commit the container and upload the snapshot to Glance
        4. Create another container from the snapshot image
        5. Verify the pre-created file is there
        """
        # This command creates a file inside the container
        command = "/bin/sh -c 'echo hello > testfile;sleep 1000000'"
        _, model = self._run_container(command=command)

        try:
            resp, _ = self.container_client.commit_container(
                model.uuid, params={'repository': 'myrepo'})
            self.assertEqual(202, resp.status)
            self._ensure_image_active('myrepo')

            # This command outputs the content of pre-created file
            command = "/bin/sh -c 'cat testfile;sleep 1000000'"
            _, model = self._run_container(
                image="myrepo", image_driver="glance", command=command)
            resp, body = self.container_client.logs_container(model.uuid)
            self.assertEqual(200, resp.status)
            self.assertTrue('hello' in encodeutils.safe_decode(body))
        finally:
            try:
                response = self.images_client.list_images()
                for image in response['images']:
                    if (image['name'] == 'myrepo' and
                            image['container_format'] == 'docker'):
                        self.images_client.delete_image(image['id'])
            except Exception:
                pass

    def _ensure_image_active(self, image_name):
        def is_image_in_desired_state():
            response = self.images_client.list_images()
            for image in response['images']:
                if (image['name'] == image_name and
                        image['container_format'] == 'docker' and
                        image['status'] == 'active'):
                    return True

            return False

        utils.wait_for_condition(is_image_in_desired_state)

    @decorators.idempotent_id('3fa024ef-aba1-48fe-9682-0d6b7854faa3')
    def test_start_stop_container(self):
        _, model = self._run_container()

        resp, _ = self.container_client.stop_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Stopped')
        self.assertEqual('Stopped',
                         self._get_container_state(model))

        resp, _ = self.container_client.start_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Running')
        self.assertEqual('Running',
                         self._get_container_state(model))

    @decorators.idempotent_id('b5f39756-8898-4e0e-a48b-dda0a06b66b6')
    def test_pause_unpause_container(self):
        _, model = self._run_container()

        resp, _ = self.container_client.pause_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Paused')
        self.assertEqual('Paused',
                         self._get_container_state(model))

        resp, _ = self.container_client.unpause_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Running')
        self.assertEqual('Running',
                         self._get_container_state(model))

    @decorators.idempotent_id('6179a588-3d48-4372-9599-f228411d1449')
    def test_kill_container(self):
        _, model = self._run_container()

        resp, _ = self.container_client.kill_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Stopped')
        self.assertEqual('Stopped',
                         self._get_container_state(model))

    @decorators.idempotent_id('c2e54321-0a70-4331-ba62-9dcaa75ac250')
    def test_reboot_container(self):
        _, model = self._run_container()
        docker_base_url = self._get_docker_url(model)
        container = self.docker_client.get_container(model.uuid,
                                                     docker_base_url)
        pid = container.get('State').get('Pid')

        resp, _ = self.container_client.reboot_container(model.uuid)
        self.assertEqual(202, resp.status)
        self.docker_client.ensure_container_pid_changed(model.uuid, pid,
                                                        docker_base_url)
        self.assertEqual('Running',
                         self._get_container_state(model))
        # assert pid is changed
        container = self.docker_client.get_container(model.uuid,
                                                     docker_base_url)
        self.assertNotEqual(pid, container.get('State').get('Pid'))

    @decorators.idempotent_id('8a591ff8-6793-427f-82a6-e3921d8b4f81')
    def test_exec_container(self):
        _, model = self._run_container()
        resp, body = self.container_client.exec_container(model.uuid,
                                                          command='echo hello')
        self.assertEqual(200, resp.status)
        self.assertTrue('hello' in encodeutils.safe_decode(body))

    @decorators.idempotent_id('a912ca23-14e7-442f-ab15-e05aaa315204')
    def test_logs_container(self):
        _, model = self._run_container(
            command="/bin/sh -c 'echo hello;sleep 1000000'")
        resp, body = self.container_client.logs_container(model.uuid)
        self.assertEqual(200, resp.status)
        self.assertTrue('hello' in encodeutils.safe_decode(body))

    @decorators.idempotent_id('d383f359-3ebd-40ef-9dc5-d36922790230')
    def test_update_container(self):
        _, model = self._run_container(cpu=0.1, memory=100)
        self.assertEqual('100M', model.memory)
        self.assertEqual(0.1, model.cpu)
        docker_base_url = self._get_docker_url(model)
        container = self.docker_client.get_container(model.uuid,
                                                     docker_base_url)
        self._assert_resource_constraints(container, cpu=0.1, memory=100)

        gen_model = datagen.container_patch_data(cpu=0.2, memory=200)
        resp, model = self.container_client.update_container(model.uuid,
                                                             gen_model)
        self.assertEqual(200, resp.status)
        self.assertEqual('200M', model.memory)
        self.assertEqual(0.2, model.cpu)
        container = self.docker_client.get_container(model.uuid,
                                                     docker_base_url)
        self._assert_resource_constraints(container, cpu=0.2, memory=200)

    @decorators.idempotent_id('b218bea7-f19b-499f-9819-c7021ffc59f4')
    def test_rename_container(self):
        _, model = self._run_container(name='container1')
        self.assertEqual('container1', model.name)
        gen_model = datagen.container_rename_data(name='container2')
        resp, model = self.container_client.rename_container(model.uuid,
                                                             gen_model)
        self.assertEqual(200, resp.status)
        self.assertEqual('container2', model.name)

    @decorators.idempotent_id('142b7716-0b21-41ed-b47d-a42fba75636b')
    def test_top_container(self):
        _, model = self._run_container(
            command="/bin/sh -c 'sleep 1000000'")
        resp, body = self.container_client.top_container(model.uuid)
        self.assertEqual(200, resp.status)
        self.assertTrue('sleep 1000000' in encodeutils.safe_decode(body))

    @decorators.idempotent_id('09638306-b501-4803-aafa-7e8025632cef')
    def test_stats_container(self):
        _, model = self._run_container()
        resp, body = self.container_client.stats_container(model.uuid)
        self.assertEqual(200, resp.status)
        self.assertTrue('NET I/O(B)' in encodeutils.safe_decode(body))
        self.assertTrue('CONTAINER' in encodeutils.safe_decode(body))
        self.assertTrue('MEM LIMIT(MiB)' in encodeutils.safe_decode(body))
        self.assertTrue('CPU %' in encodeutils.safe_decode(body))
        self.assertTrue('MEM USAGE(MiB)' in encodeutils.safe_decode(body))
        self.assertTrue('MEM %' in encodeutils.safe_decode(body))
        self.assertTrue('BLOCK I/O(B)' in encodeutils.safe_decode(body))

    @decorators.idempotent_id('b3b9cf17-82ad-4c1b-a4af-8210a778a33e')
    def test_add_sg_to_container(self):
        _, model = self._run_container()
        sgs = self._get_all_security_groups(model)
        self.assertEqual(1, len(sgs))
        self.assertEqual('default', sgs[0])

        sg_name = 'test_add_sg'
        self.sgs_client.create_security_group(name=sg_name)
        gen_model = datagen.container_add_sg_data(name=sg_name)
        resp, body = self.container_client.add_security_group(
            model.uuid, gen_model)
        self.assertEqual(202, resp.status)

        def assert_security_group_is_added():
            sgs = self._get_all_security_groups(model)
            if len(sgs) == 2:
                self.assertTrue('default' in sgs)
                self.assertTrue(sg_name in sgs)
                return True
            else:
                return False

        utils.wait_for_condition(assert_security_group_is_added)

    def _assert_resource_constraints(self, container, cpu=None, memory=None):
        if cpu is not None:
            cpu_quota = container.get('HostConfig').get('CpuQuota')
            self.assertEqual(int(cpu * 100000), cpu_quota)
            cpu_period = container.get('HostConfig').get('CpuPeriod')
            self.assertEqual(100000, cpu_period)
        if memory is not None:
            docker_memory = container.get('HostConfig').get('Memory')
            self.assertEqual(memory * 1024 * 1024, docker_memory)

    def _create_container(self, **kwargs):
        gen_model = datagen.container_data(**kwargs)
        resp, model = self.container_client.post_container(gen_model)
        self.containers.append(model.uuid)
        self.assertEqual(202, resp.status)
        # Wait for container to finish creation
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Created')

        # Assert the container is created
        resp, model = self.container_client.get_container(model.uuid)
        self.assertEqual(200, resp.status)
        self.assertEqual('Created', model.status)
        self.assertEqual('Created', self._get_container_state(model))
        return resp, model

    def _run_container(self, gen_model=None, **kwargs):
        if gen_model is None:
            gen_model = datagen.container_data(**kwargs)
        resp, model = self.container_client.run_container(gen_model)
        self.containers.append(model.uuid)
        self.assertEqual(202, resp.status)
        # Wait for container to started
        self.container_client.ensure_container_in_desired_state(
            model.uuid, 'Running')

        # Assert the container is started
        resp, model = self.container_client.get_container(model.uuid)
        self.assertEqual('Running', model.status)
        self.assertEqual('Running', self._get_container_state(model))
        return resp, model

    def _get_container_state(self, model):
        container = self.docker_client.get_container(
            model.uuid, self._get_docker_url(model))
        status = container.get('State')
        if status.get('Error') is True:
            return 'Error'
        elif status.get('Paused'):
            return 'Paused'
        elif status.get('Running'):
            return 'Running'
        elif status.get('Status') == 'created':
            return 'Created'
        else:
            return 'Stopped'

    def _get_all_security_groups(self, container):
        # find all neutron ports of this container
        port_ids = set()
        for addrs_list in container.addresses.values():
            for addr in addrs_list:
                port_id = addr['port']
                port_ids.add(port_id)

        # find all security groups of this container
        sg_ids = set()
        for port_id in port_ids:
            port = self.ports_client.show_port(port_id)
            for sg in port['port']['security_groups']:
                sg_ids.add(sg)

        sg_names = []
        for sg_id in sg_ids:
            sg = self.sgs_client.show_security_group(sg_id)
            sg_names.append(sg['security_group']['name'])

        return sg_names

    def _get_docker_url(self, container=None, host='localhost'):
        protocol = 'tcp'
        port = '2375'
        if container:
            if not hasattr(container, 'host'):
                _, container = self.os_admin.container_client.get_container(
                    container.uuid, params={'all_projects': True})
            host = container.host
        # NOTE(kiennt): By default, devstack-plugin-container will
        #               set docker_api_url = {
        #                       "unix://$DOCKER_ENGINE_SOCKET_FILE",
        #                       "tcp://0.0.0.0:$DOCKER_ENGINE_PORT"
        #                   }
        base_url = '{}://{}:{}' . format(protocol, host, port)
        return base_url

    @decorators.idempotent_id('dcb0dddb-7f0f-43f6-b82a-0cae13938bd6')
    def test_detach_and_attach_network_to_container(self):
        _, model = self._run_container()
        self.assertEqual(1, len(model.addresses))

        network = list(model.addresses.keys())[0]
        resp, body = self.container_client.network_detach(
            model.uuid, params={'network': network})
        self._ensure_network_detached(model, network)
        resp, body = self.container_client.network_attach(
            model.uuid, params={'network': network})
        self._ensure_network_attached(model, network)

    def _ensure_network_detached(self, container, network):
        def is_network_detached():
            _, model = self.container_client.get_container(container.uuid)
            if network not in model.addresses:
                return True
            else:
                return False

        utils.wait_for_condition(is_network_detached)

    def _ensure_network_attached(self, container, network):
        def is_network_attached():
            _, model = self.container_client.get_container(container.uuid)
            if network in model.addresses:
                return True
            else:
                return False

        utils.wait_for_condition(is_network_attached)
