# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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 io
import locale
import os
import signal

import pytest

from awscli.compat import (
    compat_open,
    compat_shell_quote,
    ensure_text_type,
    get_popen_kwargs_for_pager_cmd,
    getpreferredencoding,
    ignore_user_entered_signals,
    set_preferred_output_encoding,
    validate_preferred_output_encoding,
)
from awscli.testutils import FileCreator, mock, skip_if_windows, unittest


class TestEnsureText(unittest.TestCase):
    def test_string(self):
        value = 'foo'
        response = ensure_text_type(value)
        self.assertIsInstance(response, str)
        self.assertEqual(response, 'foo')

    def test_binary(self):
        value = b'bar'
        response = ensure_text_type(value)
        self.assertIsInstance(response, str)
        self.assertEqual(response, 'bar')

    def test_unicode(self):
        value = 'baz'
        response = ensure_text_type(value)
        self.assertIsInstance(response, str)
        self.assertEqual(response, 'baz')

    def test_non_ascii(self):
        value = b'\xe2\x9c\x93'
        response = ensure_text_type(value)
        self.assertIsInstance(response, str)
        self.assertEqual(response, '\u2713')

    def test_non_string_or_bytes_raises_error(self):
        value = 500
        with self.assertRaises(ValueError):
            ensure_text_type(value)


@pytest.mark.parametrize(
    "input_string, expected_output",
    (
        ('', '""'),
        ('"', '\\"'),
        ('\\', '\\'),
        ('\\a', '\\a'),
        ('\\\\', '\\\\'),
        ('\\"', '\\\\\\"'),
        ('\\\\"', '\\\\\\\\\\"'),
        ('foo bar', '"foo bar"'),
        ('foo\tbar', '"foo\tbar"'),
    ),
)
def test_compat_shell_quote_windows(input_string, expected_output):
    assert compat_shell_quote(input_string, "win32") == expected_output


@pytest.mark.parametrize(
    "input_string, expected_output",
    (
        ('', '""'),
        ('foo', 'foo'),
        ('foo bar', '"foo bar"'),
        ('foo\tbar', '"foo\tbar"'),
        ('"', '"\\""'),
        ('\\', '\\'),
        ('\\a', '\\a'),
        ('\\\\', '\\\\'),
        ('\\"', '"\\\\\\""'),
        ('foo&bar', '"foo&bar"'),
        ('foo|bar', '"foo|bar"'),
        ('foo>bar', '"foo>bar"'),
        ('foo<bar', '"foo<bar"'),
        ('foo^bar', '"foo^bar"'),
        ('foo(bar)', '"foo(bar)"'),
        ('foo,bar', '"foo,bar"'),
        ('foo;bar', '"foo;bar"'),
        ('foo=bar', '"foo=bar"'),
        ('foo!bar', '"foo!bar"'),
        ('foo%PATH%bar', 'foo%PATH%bar'),
        ('foo\\', 'foo\\'),
        ('foo bar\\', '"foo bar\\\\"'),
    ),
)
def test_compat_shell_quote_windows_for_cmd_exe(input_string, expected_output):
    assert (
        compat_shell_quote(input_string, "win32", shell=True)
        == expected_output
    )


@pytest.mark.parametrize(
    "input_string, expected_output",
    (
        ('', "''"),
        ('*', "'*'"),
        ('foo', 'foo'),
        ('foo bar', "'foo bar'"),
        ('foo\tbar', "'foo\tbar'"),
        ('foo\nbar', "'foo\nbar'"),
        ("foo'bar", '\'foo\'"\'"\'bar\''),
    ),
)
def test_comat_shell_quote_linux(input_string, expected_output):
    assert compat_shell_quote(input_string, "linux2") == expected_output


@pytest.mark.parametrize(
    "input_string, expected_output",
    (
        ('', "''"),
        ('*', "'*'"),
        ('foo', 'foo'),
        ('foo bar', "'foo bar'"),
        ('foo\tbar', "'foo\tbar'"),
        ('foo\nbar', "'foo\nbar'"),
        ("foo'bar", '\'foo\'"\'"\'bar\''),
    ),
)
def test_comat_shell_quote_darwin(input_string, expected_output):
    assert compat_shell_quote(input_string, "darwin") == expected_output


class TestGetPopenPagerCmd(unittest.TestCase):
    @mock.patch('awscli.compat.is_windows', True)
    @mock.patch('awscli.compat.default_pager', 'more')
    def test_windows(self):
        kwargs = get_popen_kwargs_for_pager_cmd()
        self.assertEqual({'args': 'more', 'shell': True}, kwargs)

    @mock.patch('awscli.compat.is_windows', True)
    @mock.patch('awscli.compat.default_pager', 'more')
    def test_windows_with_specific_pager(self):
        kwargs = get_popen_kwargs_for_pager_cmd('less -R')
        self.assertEqual({'args': 'less -R', 'shell': True}, kwargs)

    @mock.patch('awscli.compat.is_windows', False)
    @mock.patch('awscli.compat.default_pager', 'less -R')
    def test_non_windows(self):
        kwargs = get_popen_kwargs_for_pager_cmd()
        self.assertEqual({'args': ['less', '-R']}, kwargs)

    @mock.patch('awscli.compat.is_windows', False)
    @mock.patch('awscli.compat.default_pager', 'less -R')
    def test_non_windows_specific_pager(self):
        kwargs = get_popen_kwargs_for_pager_cmd('more')
        self.assertEqual({'args': ['more']}, kwargs)


class TestIgnoreUserSignals(unittest.TestCase):
    @skip_if_windows("These signals are not supported for windows")
    def test_ignore_signal_sigint(self):
        with ignore_user_entered_signals():
            try:
                os.kill(os.getpid(), signal.SIGINT)
            except KeyboardInterrupt:
                self.fail(
                    'The ignore_user_entered_signals context '
                    'manager should have ignored'
                )

    @skip_if_windows("These signals are not supported for windows")
    def test_ignore_signal_sigquit(self):
        with ignore_user_entered_signals():
            self.assertEqual(signal.getsignal(signal.SIGQUIT), signal.SIG_IGN)
            os.kill(os.getpid(), signal.SIGQUIT)

    @skip_if_windows("These signals are not supported for windows")
    def test_ignore_signal_sigtstp(self):
        with ignore_user_entered_signals():
            self.assertEqual(signal.getsignal(signal.SIGTSTP), signal.SIG_IGN)
            os.kill(os.getpid(), signal.SIGTSTP)


class TestGetPreferredEncoding(unittest.TestCase):
    @mock.patch.dict(os.environ, {'AWS_CLI_FILE_ENCODING': 'cp1252'})
    def test_getpreferredencoding_with_env_var(self):
        encoding = getpreferredencoding()
        self.assertEqual(encoding, 'cp1252')

    @mock.patch('locale.setlocale', side_effect=['POSIX', 'C'])
    def test_getpreferredencoding_wo_env_var_with_ctype_posix(self, *args):
        encoding = getpreferredencoding()
        self.assertEqual(encoding, 'UTF-8')
        encoding = getpreferredencoding()
        self.assertEqual(encoding, 'UTF-8')

    @mock.patch('locale.setlocale', return_value='English_United States.1252')
    @mock.patch('locale.getpreferredencoding')
    def test_runs_locale_getpreferredencoding_wo_env_var_and_posix(
        self, getprefedencoding, *args
    ):
        getpreferredencoding()
        getprefedencoding.assert_called_once_with()


class TestCompatOpenWithAccessPermissions(unittest.TestCase):
    def setUp(self):
        self.files = FileCreator()

    def tearDown(self):
        self.files.remove_all()

    @skip_if_windows('Permissions tests only supported on mac/linux')
    def test_can_create_file_with_acess_permissions(self):
        file_path = os.path.join(self.files.rootdir, "foo_600.txt")
        with compat_open(file_path, access_permissions=0o600, mode='w') as f:
            f.write('bar')
        self.assertEqual(os.stat(file_path).st_mode & 0o777, 0o600)

    def test_not_override_existing_file_access_permissions(self):
        file_path = os.path.join(self.files.rootdir, "foo.txt")
        with open(file_path, mode='w') as f:
            f.write('bar')
        expected_st_mode = os.stat(file_path).st_mode

        with compat_open(file_path, access_permissions=0o600, mode='w') as f:
            f.write('bar')
        self.assertEqual(os.stat(file_path).st_mode, expected_st_mode)


@pytest.mark.parametrize(
    'env_vars, expected_encoding',
    [
        ({}, 'cp1252'),
        ({'AWS_CLI_OUTPUT_ENCODING': 'UTF-8'}, 'UTF-8'),
        ({'PYTHONUTF8': '1'}, 'UTF-8'),
    ],
)
def test_set_preferred_output_encoding(env_vars, expected_encoding):
    stdout_b = io.BytesIO()
    stdout = io.TextIOWrapper(stdout_b, encoding="cp1252")

    with mock.patch.dict(os.environ, env_vars):
        set_preferred_output_encoding(stdout)

    assert stdout.encoding == expected_encoding


def test_validate_preferred_output_encoding():
    with mock.patch.dict(os.environ, {'AWS_CLI_OUTPUT_ENCODING': 'invalid'}):
        with pytest.raises(ValueError):
            validate_preferred_output_encoding()
