# -*- coding: utf-8 -*-

# 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.

import mock
from testtools import matchers

from paunch import runner
from paunch.tests import base


class TestBaseRunner(base.TestCase):
    def setUp(self):
        super(TestBaseRunner, self).setUp()
        self.runner = runner.DockerRunner('tester')
        self.podman_runner = runner.PodmanRunner('tester')

    def mock_execute(self, popen, stdout, stderr, returncode):
        subproc = mock.Mock()
        subproc.returncode = returncode
        subproc.communicate.return_value = (stdout.encode('utf-8'),
                                            stderr.encode('utf-8'))
        popen.return_value = subproc

    def assert_execute(self, popen, cmd):
        popen.assert_called_with(cmd, stderr=-1, stdout=-1)

    @mock.patch('subprocess.Popen')
    def test_execute(self, popen):
        self.mock_execute(popen, 'The stdout', 'The stderr', 0)

        self.assertEqual(
            ('The stdout', 'The stderr', 0),
            self.runner.execute(['ls', '-l'])
        )
        self.assert_execute(popen, ['ls', '-l'])

    @mock.patch('subprocess.Popen')
    def test_current_config_ids_docker(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)

        self.assertEqual(
            set(('one', 'two', 'three')),
            self.runner.current_config_ids()
        )
        self.assert_execute(
            popen, ['docker', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Label "config_id"}}']
        )

    @mock.patch('subprocess.Popen')
    def test_current_config_ids_podman(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
        self.assertEqual(
            set(('one', 'two', 'three')),
            self.podman_runner.current_config_ids()
        )
        self.assert_execute(
            popen, ['podman', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Labels.config_id}}']
        )

    @mock.patch('subprocess.Popen')
    def test_containers_in_config(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
        self.runner.remove_container = mock.Mock()

        result = self.runner.containers_in_config('foo')

        self.assert_execute(
            popen, ['docker', 'ps', '-q', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--filter', 'label=config_id=foo']
        )
        self.assertEqual(['one', 'two', 'three'], result)

    @mock.patch('subprocess.Popen')
    def test_inspect(self, popen):
        self.mock_execute(popen, '[{"foo": "bar"}]', '', 0)

        self.assertEqual(
            {"foo": "bar"},
            self.runner.inspect('one')
        )
        self.assert_execute(
            popen, ['docker', 'inspect', '--type', 'container', 'one']
        )

    @mock.patch('subprocess.Popen')
    def test_inspect_format(self, popen):
        self.mock_execute(popen, 'bar', '', 0)

        self.assertEqual(
            "bar",
            self.runner.inspect('one', output_format='{{foo}}')
        )
        self.assert_execute(
            popen, ['docker', 'inspect', '--type', 'container',
                    '--format', '{{foo}}', 'one']
        )

    def test_unique_container_name(self):
        self.runner.inspect = mock.Mock()
        self.runner.inspect.return_value = None
        self.assertEqual('foo', self.runner.unique_container_name('foo'))

        self.runner.inspect.side_effect = ['exists', 'exists', None]
        name = self.runner.unique_container_name('foo')
        name_pattern = '^foo-[a-z0-9]{8}$'
        self.assertThat(name, matchers.MatchesRegex(name_pattern))

    @mock.patch('subprocess.Popen')
    def test_discover_container_name(self, popen):
        self.mock_execute(popen, 'one-12345678', '', 0)

        self.assertEqual(
            'one-12345678',
            self.runner.discover_container_name('one', 'foo')
        )

        self.assert_execute(
            popen, ['docker', 'ps', '-a',
                    '--filter', 'label=container_name=one',
                    '--filter', 'label=config_id=foo',
                    '--format', '{{.Names}}']
        )

    @mock.patch('subprocess.Popen')
    def test_discover_container_name_empty(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.assertEqual(
            None,
            self.runner.discover_container_name('one', 'foo')
        )

    @mock.patch('subprocess.Popen')
    def test_discover_container_name_error(self, popen):
        self.mock_execute(popen, '', 'ouch', 1)

        self.assertEqual(
            None,
            self.runner.discover_container_name('one', 'foo')
        )

    @mock.patch('subprocess.Popen')
    def test_delete_missing_configs_docker(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree\nfour', '', 0)
        self.runner.remove_containers = mock.Mock()

        self.runner.delete_missing_configs(['two', 'three'])
        self.assert_execute(
            popen, ['docker', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Label "config_id"}}']
        )

        # containers one and four will be deleted
        self.runner.remove_containers.assert_has_calls([
            mock.call('one'), mock.call('four')
        ], any_order=True)

    @mock.patch('subprocess.Popen')
    def test_delete_missing_configs_podman(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree\nfour', '', 0)
        self.podman_runner.remove_containers = mock.Mock()

        self.podman_runner.delete_missing_configs(['two', 'three'])
        self.assert_execute(
            popen, ['podman', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Labels.config_id}}']
        )

        # containers one and four will be deleted
        self.podman_runner.remove_containers.assert_has_calls([
            mock.call('one'), mock.call('four')
        ], any_order=True)

    @mock.patch('subprocess.Popen')
    def test_list_configs_docker(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
        self.runner.inspect = mock.Mock(
            return_value={'e': 'f'})
        self.runner.containers_in_config = mock.Mock(
            return_value=['a', 'b', 'c'])

        result = self.runner.list_configs()

        self.assert_execute(
            popen, ['docker', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Label "config_id"}}']
        )
        self.runner.containers_in_config.assert_has_calls([
            mock.call('one'), mock.call('two'), mock.call('three')
        ], any_order=True)
        self.runner.inspect.assert_has_calls([
            mock.call('a'), mock.call('b'), mock.call('c'),
            mock.call('a'), mock.call('b'), mock.call('c'),
            mock.call('a'), mock.call('b'), mock.call('c')
        ])
        self.assertEqual({
            'one': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}],
            'two': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}],
            'three': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}]
        }, result)

    @mock.patch('subprocess.Popen')
    def test_list_configs_podman(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
        self.podman_runner.inspect = mock.Mock(
            return_value={'e': 'f'})
        self.podman_runner.containers_in_config = mock.Mock(
            return_value=['a', 'b', 'c'])

        result = self.podman_runner.list_configs()

        self.assert_execute(
            popen, ['podman', 'ps', '-a', '--filter',
                    'label=managed_by=tester',
                    '--format', '{{.Labels.config_id}}']
        )
        self.podman_runner.containers_in_config.assert_has_calls([
            mock.call('one'), mock.call('two'), mock.call('three')
        ], any_order=True)
        self.podman_runner.inspect.assert_has_calls([
            mock.call('a'), mock.call('b'), mock.call('c'),
            mock.call('a'), mock.call('b'), mock.call('c'),
            mock.call('a'), mock.call('b'), mock.call('c')
        ])
        self.assertEqual({
            'one': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}],
            'two': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}],
            'three': [{'e': 'f'}, {'e': 'f'}, {'e': 'f'}]
        }, result)

    @mock.patch('subprocess.Popen')
    def test_remove_containers(self, popen):
        self.mock_execute(popen, 'one\ntwo\nthree', '', 0)
        self.runner.remove_container = mock.Mock()

        self.runner.remove_containers('foo')

        self.assert_execute(
            popen, ['docker', 'ps', '-q', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--filter', 'label=config_id=foo']
        )
        self.runner.remove_container.assert_has_calls([
            mock.call('one'), mock.call('two'), mock.call('three')
        ])

    @mock.patch('subprocess.Popen')
    def test_remove_container(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.runner.remove_container('one')
        self.assert_execute(
            popen, ['docker', 'rm', 'one']
        )

    @mock.patch('subprocess.Popen')
    def test_stop_container(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.runner.stop_container('one')
        self.assert_execute(
            popen, ['docker', 'stop', 'one']
        )

    @mock.patch('subprocess.Popen')
    def test_stop_container_override(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.runner.stop_container('one', 'podman')
        self.assert_execute(
            popen, ['podman', 'stop', 'one']
        )

    @mock.patch('subprocess.Popen')
    def test_container_names_docker(self, popen):
        ps_result = '''one one
two-12345678 two
two two
three-12345678 three
four-12345678 four
'''

        self.mock_execute(popen, ps_result, '', 0)

        names = list(self.runner.container_names())

        self.assert_execute(
            popen, ['docker', 'ps', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--format', '{{.Names}} {{.Label "container_name"}}']
        )
        self.assertEqual([
            ['one', 'one'],
            ['two-12345678', 'two'],
            ['two', 'two'],
            ['three-12345678', 'three'],
            ['four-12345678', 'four']
        ], names)

    @mock.patch('subprocess.Popen')
    def test_container_names_podman(self, popen):
        ps_result = '''one one
two-12345678 two
two two
three-12345678 three
four-12345678 four
'''

        self.mock_execute(popen, ps_result, '', 0)

        names = list(self.podman_runner.container_names())

        self.assert_execute(
            popen, ['podman', 'ps', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--format', '{{.Names}} {{.Labels.container_name}}']
        )
        self.assertEqual([
            ['one', 'one'],
            ['two-12345678', 'two'],
            ['two', 'two'],
            ['three-12345678', 'three'],
            ['four-12345678', 'four']
        ], names)

    @mock.patch('subprocess.Popen')
    def test_container_names_by_conf_id_docker(self, popen):
        ps_result = '''one one
two-12345678 two
'''

        self.mock_execute(popen, ps_result, '', 0)

        names = list(self.runner.container_names('abc'))

        self.assert_execute(
            popen, ['docker', 'ps', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--filter', 'label=config_id=abc',
                    '--format', '{{.Names}} {{.Label "container_name"}}']
        )
        self.assertEqual([
            ['one', 'one'],
            ['two-12345678', 'two']
        ], names)

    @mock.patch('subprocess.Popen')
    def test_container_names_by_conf_id_podman(self, popen):
        ps_result = '''one one
two-12345678 two
'''

        self.mock_execute(popen, ps_result, '', 0)

        names = list(self.podman_runner.container_names('abc'))

        self.assert_execute(
            popen, ['podman', 'ps', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--filter', 'label=config_id=abc',
                    '--format', '{{.Names}} {{.Labels.container_name}}']
        )
        self.assertEqual([
            ['one', 'one'],
            ['two-12345678', 'two']
        ], names)

    @mock.patch('os.path.exists', return_value=True)
    def test_validate_volume_source_file(self, exists_mock):
        self.assertTrue(self.podman_runner.validate_volume_source('/tmp'))

    @mock.patch('os.path.exists', return_value=False)
    def test_validate_volume_source_file_fail(self, exists_mock):
        self.assertFalse(self.podman_runner.validate_volume_source('/nope'))

    @mock.patch('os.path.exists', return_value=False)
    @mock.patch('subprocess.Popen')
    def test_validate_volume_source_container(self, popen, exists_mock):
        ps_result = '''foo
foobar
'''
        self.mock_execute(popen, ps_result, '', 0)
        self.assertTrue(self.podman_runner.validate_volume_source('foo'))

    @mock.patch('os.path.exists', return_value=False)
    @mock.patch('subprocess.Popen')
    def test_validate_volume_source_container_fail(self, popen, exists_mock):
        ps_result = ''
        self.mock_execute(popen, ps_result, '', 0)
        self.assertFalse(self.podman_runner.validate_volume_source('foo'))


class TestDockerRunner(TestBaseRunner):

    @mock.patch('subprocess.Popen')
    def test_rename_containers(self, popen):
        ps_result = '''one one
two-12345678 two
two two
three-12345678 three
four-12345678 four
'''

        self.mock_execute(popen, ps_result, '', 0)
        self.runner.rename_container = mock.Mock()

        self.runner.rename_containers()

        self.assert_execute(
            popen, ['docker', 'ps', '-a',
                    '--filter', 'label=managed_by=tester',
                    '--format', '{{.Names}} {{.Label "container_name"}}']
        )
        # only containers three-12345678 and four-12345678 four will be renamed
        self.runner.rename_container.assert_has_calls([
            mock.call('three-12345678', 'three'),
            mock.call('four-12345678', 'four')
        ], any_order=True)


class PodmanRunner(TestBaseRunner):

    @mock.patch('subprocess.Popen')
    def test_image_exist(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.runner = runner.PodmanRunner('tester')
        self.runner.image_exist('one')
        self.assert_execute(
            popen, ['podman', 'image', 'exists', 'one']
        )

    @mock.patch('subprocess.Popen')
    def test_container_exist(self, popen):
        self.mock_execute(popen, '', '', 0)

        self.runner = runner.PodmanRunner('tester')
        self.runner.container_exist('one')
        self.assert_execute(
            popen, ['podman', 'container', 'exists', 'one']
        )

    @mock.patch('paunch.runner.BaseRunner.stop_container')
    def test_stop_container(self, mock_stop):
        mock_running = mock.MagicMock()
        mock_running.return_value = True
        self.runner = runner.PodmanRunner('tester')
        self.runner.container_running = mock_running
        self.runner.stop_container('foo')
        mock_running.assert_called_once_with('foo')
        mock_stop.assert_called_once_with('foo', None, False)

    @mock.patch('paunch.runner.BaseRunner.stop_container')
    def test_stop_container_missing(self, mock_stop):
        mock_running = mock.MagicMock()
        mock_running.return_value = False
        self.runner = runner.PodmanRunner('tester')
        self.runner.container_running = mock_running
        self.runner.stop_container('foo')
        mock_running.assert_called_once_with('foo')
        mock_stop.assert_not_called()

    @mock.patch('paunch.utils.systemd.service_delete')
    @mock.patch('paunch.runner.BaseRunner.remove_container')
    def test_remove_container(self, mock_remove, mock_systemd):
        mock_exists = mock.MagicMock()
        mock_exists.return_value = True
        self.runner = runner.PodmanRunner('tester')
        self.runner.container_exist = mock_exists
        self.runner.remove_container('foo')
        mock_exists.assert_called_once_with('foo')
        mock_systemd.assert_called_once_with(container='foo',
                                             log=self.runner.log)
        mock_remove.assert_called_once_with('foo')

    @mock.patch('paunch.utils.systemd.service_delete')
    @mock.patch('paunch.runner.BaseRunner.remove_container')
    def test_remove_container_missing(self, mock_remove, mock_systemd):
        mock_exists = mock.MagicMock()
        mock_exists.return_value = False
        self.runner = runner.PodmanRunner('tester')
        self.runner.container_exist = mock_exists
        self.runner.remove_container('foo')
        mock_exists.assert_called_once_with('foo')
        mock_systemd.assert_not_called()
        mock_remove.assert_not_called()
