from __future__ import absolute_import
import mock
import six
import os
import unittest

import koji

from koji_cli.commands import handle_image_build, _build_image_oz
from . import utils

if six.PY2:
    ConfigParser = six.moves.configparser.SafeConfigParser
else:
    ConfigParser = six.moves.configparser.ConfigParser

TASK_OPTIONS = {
    "background": None,
    "disk_size": "20",
    "distro": "Fedora-26",
    "factory_parameter": [
        (
            "factory_test_ver",
            "1.0"
        )
    ],
    "format": [
        "qcow2",
        "rhevm-ova",
        "vsphere-ova"
    ],
    "kickstart": "fedora-26-server-docker.ks",
    "ksurl": "git://git.fedorahosted.org/git/spin-kickstarts.git?fedora26#68c40eb7",
    "ksversion": "DEVEL",
    "noprogress": None,
    "noverifyssl": None,
    "optional_arches": [
        "ppc",
        "arm64"
    ],
    "ova_option": [
        "vsphere_product_version=26",
        "rhevm_description=Fedora Cloud 26",
        "vsphere_product_vendor_name=Fedora Project",
        "ovf_memory_mb=6144",
        "rhevm_default_display_type=1",
        "vsphere_product_name=Fedora Cloud 26",
        "ovf_cpu_count=4",
        "rhevm_os_descriptor=Fedora-26"
    ],
    "release": None,
    "repo": [
        "https://alt.fedoraproject.org/pub/alt/releases/26/Cloud/$arch/os/"
    ],
    "scratch": None,
    "skip_tag": None,
    "specfile": "git://git.fedorahosted.org/git/spin-kickstarts.git?spec_templates/fedora26#68c40eb7",
    "wait": None,
}


def mock_open():
    """Return the right patch decorator for open"""
    if six.PY2:
        return mock.patch('__builtin__.open')
    else:
        return mock.patch('builtins.open')


class Options(object):
    def __init__(self, init_dict):
        for k, v in init_dict.items():
            setattr(self, k, v)


class TestBuildImageOz(utils.CliTestCase):
    def setUp(self):
        self.maxDiff = None
        self.task_options = Options(TASK_OPTIONS)
        self.session = mock.MagicMock()
        self.options = mock.MagicMock()
        self.options.quiet = False
        self.options.poll_interval = 100
        self.options.weburl = 'https://test.org'
        self.args = [
            'fedora-server-docker',
            '26',
            'f26-candidate',
            'https://alt.fedoraproject.org/pub/alt/releases/26/Cloud/$arch/os/',
            'x86_64',
            'ppc',
            'arm64'
        ]

        self.target_info = {
            'dest_tag_name': 'f26-candidate',
            'dest_tag': 'f26-candidate'
        }

        self.tag_info = {
            'maven_support': False,
            'locked': False,
            'name': 'f26-candidate',
            'extra': {},
            'perm': None,
            'id': 2,
            'arches': 'x86_64',
            'maven_include_all': False,
            'perm_id': None
        }

        # mocks
        self.activate_session = mock.patch('koji_cli.commands.activate_session').start()
        self.watch_tasks = mock.patch('koji_cli.commands.watch_tasks').start()
        self.unique_path = mock.patch('koji_cli.commands.unique_path').start()
        self.unique_path.return_value = '/path/to/cli-image'
        self.running_in_bg = mock.patch('koji_cli.commands._running_in_bg').start()
        self.running_in_bg.return_value = False

    def tearDown(self):
        mock.patch.stopall()

    def test_build_image_oz(self):
        task_id = 101
        self.session.getBuildTarget.return_value = self.target_info
        self.session.getTag.return_value = self.tag_info
        self.session.buildImageOz.return_value = task_id
        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
            _build_image_oz(
                self.options, self.task_options, self.session, self.args)
        expected = "Created task: %d" % task_id + "\n"
        expected += "Task info: %s/taskinfo?taskID=%s" % \
                    (self.options.weburl, task_id) + "\n"
        self.assert_console_message(stdout, expected)
        self.watch_tasks.assert_called_with(
            self.session, [task_id],
            quiet=self.options.quiet,
            poll_interval=self.options.poll_interval, topurl=self.options.topurl)
        self.session.buildImageOz.assert_called_once()

    def test_build_image_oz_background(self):
        task_id = 105
        self.session.getBuildTarget.return_value = self.target_info
        self.session.getTag.return_value = self.tag_info
        self.session.buildImageOz.return_value = task_id
        self.task_options.background = True
        self.running_in_bg.return_value = True
        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
            _build_image_oz(
                self.options, self.task_options, self.session, self.args)
        expected = "Created task: %d" % task_id + "\n"
        expected += "Task info: %s/taskinfo?taskID=%s" % \
                    (self.options.weburl, task_id) + "\n"
        self.assert_console_message(stdout, expected)
        self.watch_tasks.assert_not_called()
        self.session.buildImageOz.assert_called_once()

    def test_build_image_oz_scratch(self):
        task_id = 106
        # self.task_options.kickstart will be
        # changed in _build_image_oz()
        ksfile = self.task_options.kickstart
        self.task_options.ksurl = None
        self.task_options.scratch = True

        self.session.getBuildTarget.return_value = self.target_info
        self.session.getTag.return_value = self.tag_info
        self.session.buildImageOz.return_value = task_id

        self.task_options.background = True
        self.running_in_bg.return_value = True
        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
            _build_image_oz(
                self.options, self.task_options, self.session, self.args)
        expected = '' + '\n'
        expected += "Created task: %d" % task_id + "\n"
        expected += "Task info: %s/taskinfo?taskID=%s" % \
                    (self.options.weburl, task_id) + "\n"
        self.assert_console_message(stdout, expected)
        self.watch_tasks.assert_not_called()
        self.session.buildImageOz.assert_called_once()
        self.unique_path.assert_called_with('cli-image')
        self.session.uploadWrapper.assert_called_with(
            ksfile,
            '/path/to/cli-image',
            callback=None)

    def test_build_image_oz_exception(self):
        self.session.getBuildTarget.return_value = {}
        with self.assertRaises(koji.GenericError) as cm:
            _build_image_oz(self.options, self.task_options, self.session, self.args)
        self.assertEqual(str(cm.exception), 'No such build target: %s' % self.args[2])

        self.session.getBuildTarget.return_value = self.target_info
        self.session.getTag.return_value = {}
        with self.assertRaises(koji.GenericError) as cm:
            _build_image_oz(self.options, self.task_options, self.session, self.args)
        self.assertEqual(str(cm.exception),
                         'No such destination tag: %s' % self.target_info['dest_tag_name'])

        self.session.getTag.return_value = self.tag_info
        with self.assertRaises(koji.GenericError) as cm:
            self.task_options.ksurl = None
            self.task_options.scratch = False
            _build_image_oz(self.options, self.task_options, self.session, self.args)
        self.assertEqual(str(cm.exception), 'Non-scratch builds must provide ksurl')


class TestImageBuild(utils.CliTestCase):
    def setUp(self):
        self.maxDiff = None
        self.options = mock.MagicMock()
        self.session = mock.MagicMock()
        self.configparser = mock.patch('six.moves.configparser.ConfigParser').start()

        self.error_format = """Usage: %s image-build [options] <name> <version> <target> <install-tree-url> <arch> [<arch> ...]
       %s image-build --config <FILE>

(Specify the --help global option for a list of other help options)

%s: error: {message}
""" % (self.progname, self.progname, self.progname)

    def tearDown(self):
        mock.patch.stopall()

    def test_handle_image_build_with_config_error(self):
        """Test handle_image_build argument with --config, config error"""
        with self.assertRaises(koji.ConfigurationError) as cm:
            handle_image_build(self.options,
                               self.session,
                               ['--config', '/nonexistent-file-755684354'])
        self.assertEqual(cm.exception.args[0],
                         "Config file /nonexistent-file-755684354 can't be opened.")
        self.session.activate_session_mock.assert_not_called()

    def test_handle_image_build_with_config_no_image_section(self):
        """Test handle_image_build argument with --config, no image section"""
        expected = "single section called [%s] is required" % "image-build"

        self.configparser.return_value = ConfigParser()

        self.assert_system_exit(
            handle_image_build,
            self.options, self.session, ['--config',
                                         os.path.join(os.path.dirname(__file__),
                                                      'data/image-build-config-empty.conf')],
            stderr=self.format_error_message(expected),
            activate_session=None,
            exit_code=2)
        self.session.activate_session_mock.assert_not_called()

    @mock.patch('koji_cli.commands._build_image_oz')
    def test_handle_image_build_with_config_valid_with_config(self, build_image_oz_mock):
        """Test handle_image_build."""
        config_file = os.path.join(os.path.dirname(__file__), 'data/image-build-config.conf')
        self.configparser.return_value = ConfigParser()
        handle_image_build(
            self.options,
            self.session,
            ['--config', config_file])

        args, kwargs = build_image_oz_mock.call_args
        TASK_OPTIONS['config'] = config_file
        self.assertDictEqual(TASK_OPTIONS, args[1].__dict__)
        self.session.activate_session_mock.assert_not_called()

    def test_handle_image_build_argument_error_without_config_arg_error(self):
        """Test handle_image_build argument errors, no --config, arguments error"""
        expected = "At least five arguments are required: a name, " + \
                   "a version, a build target, a URL to an " + \
                   "install tree, and 1 or more architectures."

        self.assert_system_exit(
            handle_image_build,
            self.options,
            self.session,
            [],
            stderr=self.format_error_message(expected),
            activate_session=None,
            exit_code=2)
        self.session.activate_session_mock.assert_not_called()

    def test_handle_image_build_argument_error_without_config_without_kickstart_option(self):
        """Test handle_image_build argument errors, no --config cases,
        without kickstart option (--ksurl, kickstart)"""
        expected = "You must specify --kickstart"

        self.assert_system_exit(
            handle_image_build,
            self.options,
            self.session,
            ['name', 'version', 'target', 'install-tree-url', 'arch'],
            stderr=self.format_error_message(expected),
            activate_session=None,
            exit_code=2)
        self.session.activate_session_mock.assert_not_called()

    def test_handle_image_build_argument_error_without_config_without_distro_option(self):
        """Test handle_image_build argument errors, no --config cases, without distro option"""
        expected = "You must specify --distro. Examples: Fedora-16, RHEL-6.4, " + \
                   "SL-6.4 or CentOS-6.4"

        self.assert_system_exit(
            handle_image_build,
            self.options,
            self.session,
            ['name', 'version', 'target', 'install-tree-url', 'arch',
             '--kickstart', 'kickstart.ks'],
            stderr=self.format_error_message(expected),
            activate_session=None,
            exit_code=2)

    def test_handle_image_build_help(self):
        """Test handle_image_build help message"""
        self.assert_help(
            handle_image_build,
            """Usage: %s image-build [options] <name> <version> <target> <install-tree-url> <arch> [<arch> ...]
       %s image-build --config <FILE>

(Specify the --help global option for a list of other help options)

Options:
  -h, --help            show this help message and exit
  --background          Run the image creation task at a lower priority
  --config=CONFIG       Use a configuration file to define image-build options
                        instead of command line options (they will be
                        ignored).
  --disk-size=DISK_SIZE
                        Set the disk device size in gigabytes
  --distro=DISTRO       specify the RPM based distribution the image will be
                        based on with the format RHEL-X.Y, CentOS-X.Y, SL-X.Y,
                        or Fedora-NN. The packages for the Distro you choose
                        must have been built in this system.
  --format=FORMAT       Convert results to one or more formats (vmdk, qcow,
                        qcow2, vdi, vpc, rhevm-ova, vsphere-ova, vagrant-
                        virtualbox, vagrant-libvirt, vagrant-vmware-fusion,
                        vagrant-hyperv, docker, raw-xz, liveimg-squashfs, tar-
                        gz), this option may be used multiple times. By
                        default, specifying this option will omit the raw disk
                        image (which is 10G in size) from the build results.
                        If you really want it included with converted images,
                        pass in 'raw' as an option.
  --kickstart=KICKSTART
                        Path to a local kickstart file
  --ksurl=SCMURL        The URL to the SCM containing the kickstart file
  --ksversion=VERSION   The syntax version used in the kickstart file
  --noprogress          Do not display progress of the upload
  --noverifyssl         Use the noverifyssl option for the install tree and
                        all repos. This option is only allowed if enabled on
                        the builder.
  --nowait              Don't wait on image creation
  --ova-option=OVA_OPTION
                        Override a value in the OVA description XML. Provide a
                        value in a name=value format, such as
                        'ovf_memory_mb=6144'
  --factory-parameter=FACTORY_PARAMETER
                        Pass a parameter to Image Factory. The results are
                        highly specific to the image format being created.
                        This is a two argument parameter that can be specified
                        an arbitrary number of times. For example: --factory-
                        parameter docker_cmd '[ "/bin/echo Hello World" ]'
  --release=RELEASE     Forcibly set the release field
  --repo=REPO           Specify a repo that will override the repo used to
                        install RPMs in the image. May be used multiple times.
                        The build tag repo associated with the target is the
                        default.
  --scratch             Create a scratch image
  --skip-tag            Do not attempt to tag package
  --can-fail=ARCH1,ARCH2,...
                        List of archs which are not blocking for build
                        (separated by commas.
  --specfile=URL        SCM URL to spec file fragment to use to generate
                        wrapper RPMs
  --wait                Wait on the image creation, even if running in the
                        background
""" % (self.progname, self.progname))


if __name__ == '__main__':
    unittest.main()
