# Copyright 2015, Ansible, Inc.
# Luke Sneeringer <lsneeringer@ansible.com>
#
# 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 json
import os.path
import stat
import warnings

import click
from click.testing import CliRunner
from fauxquests.response import Resp
from fauxquests.utils import URL
import requests
import six.moves.urllib.parse as urlparse

import tower_cli
from tower_cli.api import client, Client
from tower_cli.cli.misc import config, version, login, _echo_setting
from tower_cli.conf import settings
from tower_cli.constants import CUR_API_VERSION

from tests.compat import unittest, mock


class VersionTests(unittest.TestCase):
    """A set of tests to ensure that the version command runs in the way
    that we expect.
    """
    def setUp(self):
        self.runner = CliRunner()

    def test_version_command(self):
        """Establish that the version command returns the output we
        expect.
        """
        # Set up output from the /config/ endpoint in Tower and
        # invoke the command.
        with client.test_mode as t:
            t.register_json('/config/', {
                'license_info': {
                    'license_type': 'open'
                },
                'version': '4.21',
                'ansible_version': '2.7'
            })
            result = self.runner.invoke(version)

            # Verify that we got the output we expected.
            self.assertEqual(result.exit_code, 0)
            self.assertEqual(
                result.output.strip(),
                'Tower CLI %s\n'
                'API %s\n'
                'AWX 4.21\n'
                'Ansible 2.7' % (
                    tower_cli.__version__,
                    CUR_API_VERSION
                ),
            )

    def test_cannot_connect(self):
        """Establish that the version command gives a nice error in cases
        where it cannot connect to Tower.
        """
        with mock.patch.object(client, 'get') as get:
            get.side_effect = requests.exceptions.RequestException
            result = self.runner.invoke(version)
            self.assertEqual(result.exit_code, 1)
            self.assertIn('Could not connect to Ansible Tower.', result.output)


class ConfigTests(unittest.TestCase):
    """Establish that the `tower-cli config` command works in the way
    that we expect.
    """
    def setUp(self):
        self.runner = CliRunner()

    def test_no_arguments(self):
        """Establish that if `tower-cli config` is called with no arguments,
        that we print out the current configuration.
        """
        # Invoke the command.
        with settings.runtime_values(username='meagan', verbose=False,
                                     password='This is the best wine.'):
            result = self.runner.invoke(config)

        # Ensure that we got a 0 exit status
        self.assertEqual(result.exit_code, 0)

        # Ensure that the output looks correct.
        self.assertIn('username: meagan', result.output)
        self.assertIn('password: This is the best wine.', result.output)
        self.assertIn('verbose: False', result.output)

    def test_key_and_no_value(self):
        """Establish that if we are given a key and no value, that the
        setting's value is printed.
        """
        with settings.runtime_values(password='This is the best wine.'):
            result = self.runner.invoke(config, ['password'])
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'password: This is the best wine.')

    def test_write_setting(self):
        """Establish that if we attempt to write a valid setting, that
        the parser's write method is run.
        """
        # Invoke the command, but trap the file-write at the end
        # so we don't plow over real things.
        mock_open = mock.mock_open()
        filename = os.path.expanduser('~/.tower_cli.cfg')
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod') as chmod:
                result = self.runner.invoke(config, ['username', 'luke'])
                chmod.assert_called_once_with(filename, int('0600', 8))

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'Configuration updated successfully.')

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call(os.path.expanduser('~/.tower_cli.cfg'), 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('username = luke\n'),
                      mock_open.mock_calls)

    def test_permissions_warning(self):
        """Warn user if configuration file permissions can not be set
        """
        # Try to set permissions on file that does not exist, expecting warning
        mock_open = mock.mock_open()
        filename = '.tower_cli.cfg'
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod') as chmod:
                chmod.side_effect = OSError
                with mock.patch.object(warnings, 'warn') as warn:
                    result = self.runner.invoke(
                        config, ['username', 'luke', '--scope=local'])
                    warn.assert_called_once_with(mock.ANY, UserWarning)
                    chmod.assert_called_once_with(
                        filename, stat.S_IRUSR | stat.S_IWUSR)

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'Configuration updated successfully.')

    def test_write_global_setting(self):
        """Establish that if we attempt to write a valid setting, that
        the parser's write method is run.
        """
        # Invoke the command, but trap the file-write at the end
        # so we don't plow over real things.
        filename = '/etc/tower/tower_cli.cfg'
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os.path, 'isdir') as isdir:
                with mock.patch.object(os, 'chmod') as chmod:
                    isdir.return_value = True
                    result = self.runner.invoke(
                        config, ['username', 'luke', '--scope=global'],
                    )
                    isdir.assert_called_once_with('/etc/tower/')
                    chmod.assert_called_once_with(filename, int('0600', 8))

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'Configuration updated successfully.')

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call('/etc/tower/tower_cli.cfg', 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('username = luke\n'),
                      mock_open.mock_calls)

    def test_write_local_setting(self):
        """Establish that if we attempt to write a valid setting locally, that
        the correct parser's write method is run.
        """
        # Invoke the command, but trap the file-write at the end
        # so we don't plow over real things.
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod') as chmod:
                result = self.runner.invoke(
                    config, ['username', 'meagan', '--scope=local'],
                )
                filename = ".tower_cli.cfg"
                chmod.assert_called_once_with(filename, int('0600', 8))

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'Configuration updated successfully.')

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call('.tower_cli.cfg', 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('username = meagan\n'),
                      mock_open.mock_calls)

    def test_unset(self):
        """Establish that calling `tower-cli config --unset` works in the
        way that we expect.
        """
        # Invoke the command, but trap the file-write at the end
        # so we don't plow over real things.
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod'):
                result = self.runner.invoke(config, ['username', '--unset'])

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual(result.output.strip(),
                         'Configuration updated successfully.')

        # Ensure that the output seems to be correct.
        self.assertNotIn(mock.call().write('username = luke\n'),
                         mock_open.mock_calls)

    def test_error_invalid_key(self):
        """Establish that if `tower-cli config` is sent an invalid key,
        that we raise an exception.
        """
        result = self.runner.invoke(config, ['bogus'])
        self.assertEqual(result.exit_code, 1)
        self.assertEqual(result.output.strip(),
                         'Error: Invalid configuration option "bogus".')

    def test_error_value_and_unset(self):
        """Establish that if `tower-cli config` is called with both a value
        and the --unset flag, that we raise an exception.
        """
        result = self.runner.invoke(config, ['host', '127.0.0.1', '--unset'])
        self.assertEqual(result.exit_code, 2)
        self.assertEqual(result.output.strip(),
                         'Error: Cannot provide both a value and --unset.')

    def test_error_no_global_config_file(self):
        """Establish that if no global config file exists, that tower-cli
        does not attempt to create it.
        """
        with mock.patch.object(os.path, 'isdir') as isdir:
            isdir.return_value = False
            result = self.runner.invoke(config,
                                        ['host', 'foo', '--scope=global'])
            isdir.assert_called_once_with('/etc/tower/')
        self.assertEqual(result.exit_code, 1)
        self.assertEqual(result.output.strip(),
                         'Error: /etc/tower/ does not exist, and this '
                         'command cowardly declines to create it.')


class LoginTests(unittest.TestCase):
    """Establish that the `tower-cli login` command works in the way
    that we expect.
    """
    def setUp(self):
        self.runner = CliRunner()

    def test_no_arguments(self):
        """
        Establish that if `tower-cli login` is called with no arguments, it
        complains
        """
        # Invoke the command.
        result = self.runner.invoke(login)

        # Ensure that we got a non-zero exit status
        self.assertNotEqual(result.exit_code, 0)
        self.assertIn('Missing argument "username"', result.output)

    def test_oauth_unsupported(self):
        """Establish that if `tower-cli login` is used on a Tower that
        doesn't support OAuth2, it shows a meaningful error
        """
        with client.test_mode as t:
            # You have to modify this internal private registry to
            # register a URL endpoint that _doesn't_ have the version
            # prefix
            prefix = Client().get_prefix(include_version=False)
            t._registry[URL(prefix + 'o/', method='HEAD')] = Resp(
                ''.encode('utf-8'), 404, {}
            )
            result = self.runner.invoke(
                login, ['bob', '--password', 'secret']
            )

        # Ensure that we got a non-zero exit status
        self.assertNotEqual(result.exit_code, 0)
        self.assertIn(
            'Error: This version of Tower does not support OAuth2.0',
            result.output
        )

    def test_personal_access_token(self):
        """Establish that if `tower-cli login` is called with a username and
        password, we obtain and write an oauth token to the config file
        """
        # Invoke the command.
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod'):
                with client.test_mode as t:
                    # You have to modify this internal private registry to
                    # register a URL endpoint that _doesn't_ have the version
                    # prefix
                    prefix = Client().get_prefix(include_version=False)
                    t._registry[URL(prefix + 'o/', method='HEAD')] = Resp(
                        ''.encode('utf-8'), 200, {}
                    )
                    t.register('/users/bob/personal_tokens/', json.dumps({
                        'token': 'abc123'
                    }), status_code=201, method='POST')
                    result = self.runner.invoke(
                        login, ['bob', '--password', 'secret', '--scope', 'read']
                    )

        # Ensure that we got a zero exit status
        self.assertEqual(result.exit_code, 0)
        assert json.loads(t.requests[-1].body)['scope'] == 'read'

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call(os.path.expanduser('~/.tower_cli.cfg'), 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('oauth_token = abc123\n'),
                      mock_open.mock_calls)

    def test_personal_access_invalid(self):
        """Establish that if `tower-cli login` is called with an invalid
        username and password, it shows a meaningful error
        """
        with client.test_mode as t:
            # You have to modify this internal private registry to
            # register a URL endpoint that _doesn't_ have the version
            # prefix
            prefix = Client().get_prefix(include_version=False)
            t._registry[URL(prefix + 'o/', method='HEAD')] = Resp(
                ''.encode('utf-8'), 200, {}
            )
            t.register('/users/bob/personal_tokens/', json.dumps({}),
                       status_code=401, method='POST')
            result = self.runner.invoke(
                login, ['bob', '--password', 'secret']
            )

        # Ensure that we got a non-zero exit status
        self.assertNotEqual(result.exit_code, 0)
        self.assertIn(
            'Error: Invalid Tower authentication credentials', result.output
        )

    def test_application_scoped_token(self):
        """Establish that if `tower-cli login` is called with a username,
        password, and OAuth2 client ID and secret, we obtain and write an oauth
        token to the config file
        """
        # Invoke the command.
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod'):
                with client.test_mode as t:
                    # You have to modify this internal private registry to
                    # register a URL endpoint that _doesn't_ have the version
                    # prefix
                    prefix = Client().get_prefix(include_version=False)
                    t._registry[URL(prefix + 'o/', method='HEAD')] = Resp(
                        ''.encode('utf-8'), 200, {}
                    )
                    t._registry[URL(prefix + 'o/token/', method='POST')] = Resp(
                        json.dumps({'access_token': 'abc123'}).encode('utf-8'),
                        201, {}
                    )
                    result = self.runner.invoke(
                        login, ['bob', '--password', 'secret', '--client-id',
                                'abc123', '--client-secret', 'some-secret']
                    )

        # Ensure that we got a zero exit status
        self.assertEqual(result.exit_code, 0)
        data = urlparse.parse_qs(t.requests[-1].body)
        assert data['scope'] == ['write']
        assert data['grant_type'] == ['password']
        assert data['password'] == ['secret']
        assert data['username'] == ['bob']

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call(os.path.expanduser('~/.tower_cli.cfg'), 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('oauth_token = abc123\n'),
                      mock_open.mock_calls)

    def test_public_application_scoped_token(self):
        """Establish that if `tower-cli login` is called with a username,
        password, and public OAuth2 client ID, we obtain and write an oauth
        token to the config file
        """
        # Invoke the command.
        mock_open = mock.mock_open()
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os, 'chmod'):
                with client.test_mode as t:
                    # You have to modify this internal private registry to
                    # register a URL endpoint that _doesn't_ have the version
                    # prefix
                    prefix = Client().get_prefix(include_version=False)
                    t._registry[URL(prefix + 'o/', method='HEAD')] = Resp(
                        ''.encode('utf-8'), 200, {}
                    )
                    t._registry[URL(prefix + 'o/token/', method='POST')] = Resp(
                        json.dumps({'access_token': 'abc123'}).encode('utf-8'),
                        201, {}
                    )
                    result = self.runner.invoke(
                        login, ['bob', '--password', 'secret', '--client-id',
                                'abc123']
                    )

        # Ensure that we got a zero exit status
        self.assertEqual(result.exit_code, 0)
        data = urlparse.parse_qs(t.requests[-1].body)
        assert data['scope'] == ['write']
        assert data['grant_type'] == ['password']
        assert data['password'] == ['secret']
        assert data['username'] == ['bob']
        assert data['client_id'] == ['abc123']

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call(os.path.expanduser('~/.tower_cli.cfg'), 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('oauth_token = abc123\n'),
                      mock_open.mock_calls)


class SupportTests(unittest.TestCase):
    """Establish that support functions in this module work in the way
    that we expect.
    """
    def test_echo_setting(self):
        """Establish that the `echo_setting` method works in the way
        that we expect.
        """
        with settings.runtime_values(host='20.12.4.21'):
            with mock.patch.object(click, 'secho') as secho:
                _echo_setting('host')
                self.assertEqual(secho.mock_calls, [
                    mock.call('host: ', fg='magenta', bold=True, nl=False),
                    mock.call('20.12.4.21', fg='white', bold=True),
                ])


class DeprecationTests(unittest.TestCase):
    """Establish any deprecation notices are sent with a command if they
    are expected.
    """
    def setUp(self):
        self.runner = CliRunner()

    def test_write_global_setting_deprecated(self):
        """Establish that if we attempt to write a valid setting, that
        the parser's write method is run.
        """
        # Invoke the command, but trap the file-write at the end
        # so we don't plow over real things.
        mock_open = mock.mock_open()
        warning_text = 'The `--global` option is deprecated and will be '\
                       'removed. Use `--scope=global` to get the same effect.'
        with mock.patch('tower_cli.cli.misc.open', mock_open,
                        create=True):
            with mock.patch.object(os.path, 'isdir') as isdir:
                with mock.patch.object(os, 'chmod'):
                    with mock.patch.object(warnings, 'warn') as warn:
                        isdir.return_value = True
                        result = self.runner.invoke(
                            config, ['username', 'meagan', '--global'],
                        )
                        warn.assert_called_once_with(warning_text,
                                                     DeprecationWarning)
                        self.assertEqual(warn.mock_calls[0][1][1],
                                         DeprecationWarning)
                        isdir.assert_called_once_with('/etc/tower/')

        # Ensure that the command completed successfully.
        self.assertEqual(result.exit_code, 0)
        self.assertEqual('Configuration updated successfully.',
                         result.output.strip())

        # Ensure that the output seems to be correct.
        self.assertIn(mock.call('/etc/tower/tower_cli.cfg', 'w'),
                      mock_open.mock_calls)
        self.assertIn(mock.call().write('username = meagan\n'),
                      mock_open.mock_calls)
