# Copyright (c) 2013 New Dream Network, LLC (DreamHost)
#
# 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.
#
# Copyright (C) 2013 Association of Universities for Research in Astronomy
#                    (AURA)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#
#     2. Redistributions in binary form must reproduce the above
#        copyright notice, this list of conditions and the following
#        disclaimer in the documentation and/or other materials provided
#        with the distribution.
#
#     3. The name of AURA and its representatives may not be used to
#        endorse or promote products derived from this software without
#        specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS

from __future__ import absolute_import

import os
import re
import subprocess
import textwrap

import fixtures
from testtools import content
import virtualenv

from pbr.tests import util

PBR_ROOT = os.path.abspath(os.path.join(__file__, '..', '..', '..'))


class Chdir(fixtures.Fixture):
    """Dive into given directory and return back on cleanup.

    :ivar path: The target directory.
    """

    def __init__(self, path):
        self.path = path

    def setUp(self):
        super(Chdir, self).setUp()
        self.addCleanup(os.chdir, os.getcwd())
        os.chdir(self.path)


class CapturedSubprocess(fixtures.Fixture):
    """Run a process and capture its output.

    :attr stdout: The output (a string). Only set if the process fails.
    :attr stderr: The standard error (a string). Only set if the process fails.
    :attr returncode: The return code of the process.

    Note that stdout and stderr are decoded from the bytestrings subprocess
    returns using error=replace
    """

    def __init__(self, label, *args, **kwargs):
        """Create a CapturedSubprocess.

        :param label: A label for the subprocess in the test log. E.g. 'foo'.
        :param *args: The *args to pass to Popen.
        :param **kwargs: The **kwargs to pass to Popen.
        """
        super(CapturedSubprocess, self).__init__()
        self.label = label
        self.args = args
        self.kwargs = kwargs
        self.kwargs['stderr'] = subprocess.PIPE
        self.kwargs['stdin'] = subprocess.PIPE
        self.kwargs['stdout'] = subprocess.PIPE

    def setUp(self):
        super(CapturedSubprocess, self).setUp()
        # setuptools can be very shouty
        env = os.environ.copy()
        env['PYTHONWARNINGS'] = 'ignore'
        self.kwargs['env'] = env
        proc = subprocess.Popen(*self.args, **self.kwargs)
        out, err = proc.communicate()
        self.out = out.decode('utf-8', 'replace')
        self.err = err.decode('utf-8', 'replace')
        self.addDetail(self.label + '-stdout', content.text_content(self.out))
        self.addDetail(self.label + '-stderr', content.text_content(self.err))
        self.returncode = proc.returncode
        if proc.returncode:
            raise AssertionError(
                'Failed process args=%r, kwargs=%r, returncode=%s'
                % (self.args, self.kwargs, proc.returncode)
            )
        self.addCleanup(delattr, self, 'out')
        self.addCleanup(delattr, self, 'err')
        self.addCleanup(delattr, self, 'returncode')


class GitRepo(fixtures.Fixture):
    """A git repo for testing with.

    Use of TempHomeDir with this fixture is strongly recommended as due to the
    lack of config --local in older gits, it will write to the users global
    configuration without TempHomeDir.
    """

    def __init__(self, basedir):
        super(GitRepo, self).__init__()
        self._basedir = basedir

    def setUp(self):
        super(GitRepo, self).setUp()
        util.run_cmd(['git', 'init', '.'], self._basedir)
        util.config_git()
        util.run_cmd(['git', 'add', '.'], self._basedir)

    def commit(self, message_content='test commit'):
        files = len(os.listdir(self._basedir))
        path = self._basedir + '/%d' % files
        open(path, 'wt').close()
        util.run_cmd(['git', 'add', path], self._basedir)
        util.run_cmd(['git', 'commit', '-m', message_content], self._basedir)

    def uncommit(self):
        util.run_cmd(['git', 'reset', '--hard', 'HEAD^'], self._basedir)

    def tag(self, version):
        util.run_cmd(['git', 'tag', '-sm', 'test tag', version], self._basedir)


class GPGKey(fixtures.Fixture):
    """Creates a GPG key for testing.

    It's recommended that this be used in concert with a unique home
    directory.
    """

    def setUp(self):
        super(GPGKey, self).setUp()
        # If a temporary home dir is in use (and it should be), ensure gpg is
        # aware of it. This seems to be necessary on Fedora.
        self.useFixture(
            fixtures.EnvironmentVariable('GNUPGHOME', os.getenv('HOME'))
        )
        tempdir = self.useFixture(fixtures.TempDir())
        gnupg_version_re = re.compile(r'^gpg\s.*\s([\d+])\.([\d+])\.([\d+])')
        gnupg_version = util.run_cmd(['gpg', '--version'], tempdir.path)
        for line in gnupg_version[0].split('\n'):
            gnupg_version = gnupg_version_re.match(line)
            if gnupg_version:
                gnupg_version = (
                    int(gnupg_version.group(1)),
                    int(gnupg_version.group(2)),
                    int(gnupg_version.group(3)),
                )
                break
        else:
            if gnupg_version is None:
                gnupg_version = (0, 0, 0)

        config_file = os.path.join(tempdir.path, 'key-config')
        with open(config_file, 'wt') as f:
            if gnupg_version[0] == 2 and gnupg_version[1] >= 1:
                f.write(
                    """
                %no-protection
                %transient-key
                """
                )
            f.write(
                """
            %no-ask-passphrase
            Key-Type: RSA
            Name-Real: Example Key
            Name-Comment: N/A
            Name-Email: example@example.com
            Expire-Date: 2d
            %commit
            """
            )

        # Note that --quick-random (--debug-quick-random in GnuPG 2.x)
        # does not have a corresponding preferences file setting and
        # must be passed explicitly on the command line instead
        if gnupg_version[0] == 1:
            gnupg_random = '--quick-random'
        elif gnupg_version[0] >= 2:
            gnupg_random = '--debug-quick-random'
        else:
            gnupg_random = ''

        _, _, retcode = util.run_cmd(
            ['gpg', '--gen-key', '--batch', gnupg_random, config_file],
            tempdir.path,
        )
        assert retcode == 0, 'gpg key generation failed!'


class Venv(fixtures.Fixture):
    """Create a virtual environment for testing with.

    :attr path: The path to the environment root.
    :attr python: The path to the python binary in the environment.
    """

    def __init__(self, reason, modules=(), pip_cmd=None):
        """Create a Venv fixture.

        :param reason: A human readable string to bake into the venv
            file path to aid diagnostics in the case of failures.
        :param modules: A list of modules to install, defaults to latest
            pip, wheel, and the working copy of PBR.
        :attr pip_cmd: A list to override the default pip_cmd passed to
            python for installing base packages.
        """
        self._reason = reason
        if modules == ():
            modules = ['pip', 'wheel', 'build', 'setuptools', PBR_ROOT]
        self.modules = modules
        if pip_cmd is None:
            self.pip_cmd = ['-m', 'pip', '-v', 'install']
        else:
            self.pip_cmd = pip_cmd

    def _setUp(self):
        path = self.useFixture(fixtures.TempDir()).path
        virtualenv.cli_run([path])

        python = os.path.join(path, 'bin', 'python')
        command = [python] + self.pip_cmd + ['-U']
        if self.modules and len(self.modules) > 0:
            command.extend(self.modules)
            self.useFixture(
                CapturedSubprocess('mkvenv-' + self._reason, command)
            )
        self.addCleanup(delattr, self, 'path')
        self.addCleanup(delattr, self, 'python')
        self.path = path
        self.python = python
        return path, python


class Packages(fixtures.Fixture):
    """Creates packages from dict with defaults

    :param package_dirs: A dict of package name to directory strings
    {'pkg_a': '/tmp/path/to/tmp/pkg_a', 'pkg_b': '/tmp/path/to/tmp/pkg_b'}
    """

    defaults = {
        'setup.py': textwrap.dedent(
            u"""\
            #!/usr/bin/env python
            import setuptools
            setuptools.setup(
                setup_requires=['pbr'],
                pbr=True,
            )
            """
        ),
        'setup.cfg': textwrap.dedent(
            u"""\
            [metadata]
            name = {pkg_name}
            """
        ),
    }

    def __init__(self, packages):
        """Creates packages from dict with defaults

        :param packages: a dict where the keys are the package name and a
            value that is a second dict that may be empty, containing keys of
            filenames and a string value of the contents. ::

                {'package-a': {'requirements.txt': 'string', 'setup.cfg': 'string'}
        """
        self.packages = packages

    def _writeFile(self, directory, file_name, contents):
        path = os.path.abspath(os.path.join(directory, file_name))
        path_dir = os.path.dirname(path)
        if not os.path.exists(path_dir):
            if path_dir.startswith(directory):
                os.makedirs(path_dir)
            else:
                raise ValueError
        with open(path, 'wt') as f:
            f.write(contents)

    def _setUp(self):
        tmpdir = self.useFixture(fixtures.TempDir()).path
        package_dirs = {}
        for pkg_name in self.packages:
            pkg_path = os.path.join(tmpdir, pkg_name)
            package_dirs[pkg_name] = pkg_path
            os.mkdir(pkg_path)
            for cf in ['setup.py', 'setup.cfg']:
                if cf in self.packages[pkg_name]:
                    contents = self.packages[pkg_name].pop(cf)
                else:
                    contents = self.defaults[cf].format(pkg_name=pkg_name)
                self._writeFile(pkg_path, cf, contents)

            for cf in self.packages[pkg_name]:
                self._writeFile(pkg_path, cf, self.packages[pkg_name][cf])
            self.useFixture(GitRepo(pkg_path)).commit()
        self.addCleanup(delattr, self, 'package_dirs')
        self.package_dirs = package_dirs
        return package_dirs
