#  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 argparse
import os
import textwrap

from io import StringIO
from unittest import mock

from cliff.formatters import table
from cliff.tests import base
from cliff.tests import test_columns


def _generate_namespace(
    max_width: int = 0, print_empty: bool = False, fit_width: bool = False
) -> argparse.Namespace:
    if max_width == 0:
        # Envvar is only taken into account if CLI parameter not given
        max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0))

    return argparse.Namespace(
        max_width=max_width,
        print_empty=print_empty,
        fit_width=fit_width,
    )


def _table_tester_helper(tags, data, extra_args=None):
    """Get table output as a string, formatted according to
    CLI arguments, environment variables and terminal size

    :param tags: tuple of strings for data tags (column headers or fields)
    :param data: tuple of strings for single data row or list of tuples of
        strings for multiple rows of data
    :param extra_args: an instance of argparse.Namespace, a list of strings for
        CLI arguments, or None
    """
    sf = table.TableFormatter()

    if extra_args is None:
        # Default to no CLI arguments
        parsed_args = _generate_namespace()
    elif isinstance(extra_args, argparse.Namespace):
        # Use the given CLI arguments
        parsed_args = extra_args
    else:
        # Parse arguments as if passed on the command-line
        parser = argparse.ArgumentParser(description='Testing...')
        sf.add_argument_group(parser)
        parsed_args = parser.parse_args(extra_args)

    output = StringIO()
    emitter = sf.emit_list if type(data) is list else sf.emit_one
    emitter(tags, data, output, parsed_args)
    return output.getvalue()


class TestTableFormatter(base.TestBase):
    @mock.patch('cliff.utils.terminal_width')
    def test(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'test\rcarriage\r\nreturn')
        expected = textwrap.dedent(
            '''\
        +-------+---------------+
        | Field | Value         |
        +-------+---------------+
        | a     | A             |
        | b     | B             |
        | c     | C             |
        | d     | test carriage |
        |       | return        |
        +-------+---------------+
        '''
        )
        self.assertEqual(expected, _table_tester_helper(c, d))


class TestTerminalWidth(base.TestBase):
    # Multi-line output when width is restricted to 42 columns
    expected_ml_val = textwrap.dedent(
        '''\
    +-------+--------------------------------+
    | Field | Value                          |
    +-------+--------------------------------+
    | a     | A                              |
    | b     | B                              |
    | c     | C                              |
    | d     | dddddddddddddddddddddddddddddd |
    |       | dddddddddddddddddddddddddddddd |
    |       | ddddddddddddddddd              |
    +-------+--------------------------------+
    '''
    )

    # Multi-line output when width is restricted to 80 columns
    expected_ml_80_val = textwrap.dedent(
        '''\
    +-------+----------------------------------------------------------------------+
    | Field | Value                                                                |
    +-------+----------------------------------------------------------------------+
    | a     | A                                                                    |
    | b     | B                                                                    |
    | c     | C                                                                    |
    | d     | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
    |       | ddddddddd                                                            |
    +-------+----------------------------------------------------------------------+
    '''  # noqa: E501
    )

    # Single-line output, for when no line length restriction apply
    expected_sl_val = textwrap.dedent(
        '''\
    +-------+-------------------------------------------------------------------------------+
    | Field | Value                                                                         |
    +-------+-------------------------------------------------------------------------------+
    | a     | A                                                                             |
    | b     | B                                                                             |
    | c     | C                                                                             |
    | d     | ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
    +-------+-------------------------------------------------------------------------------+
    '''  # noqa: E501
    )

    @mock.patch('cliff.utils.terminal_width')
    def test_table_formatter_no_cli_param(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        self.assertEqual(
            self.expected_ml_80_val,
            _table_tester_helper(
                c, d, extra_args=_generate_namespace(fit_width=True)
            ),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_table_formatter_cli_param(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        self.assertEqual(
            self.expected_ml_val,
            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_table_formatter_no_cli_param_unlimited_tw(self, tw):
        tw.return_value = 0
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        # output should not be wrapped to multiple lines
        self.assertEqual(
            self.expected_sl_val,
            _table_tester_helper(c, d, extra_args=_generate_namespace()),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_table_formatter_cli_param_unlimited_tw(self, tw):
        tw.return_value = 0
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        self.assertEqual(
            self.expected_ml_val,
            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
        )

    @mock.patch('cliff.utils.terminal_width')
    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
    def test_table_formatter_cli_param_envvar_big(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        self.assertEqual(
            self.expected_ml_val,
            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
        )

    @mock.patch('cliff.utils.terminal_width')
    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '23'})
    def test_table_formatter_cli_param_envvar_tiny(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', 'd' * 77)
        self.assertEqual(
            self.expected_ml_val,
            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
        )


class TestMaxWidth(base.TestBase):
    expected_80 = textwrap.dedent(
        '''\
    +--------------------------+---------------------------------------------+
    | Field                    | Value                                       |
    +--------------------------+---------------------------------------------+
    | field_name               | the value                                   |
    | a_really_long_field_name | a value significantly longer than the field |
    +--------------------------+---------------------------------------------+
    '''
    )

    @mock.patch('cliff.utils.terminal_width')
    def test_80(self, tw):
        tw.return_value = 80
        c = ('field_name', 'a_really_long_field_name')
        d = ('the value', 'a value significantly longer than the field')
        self.assertEqual(self.expected_80, _table_tester_helper(c, d))

    @mock.patch('cliff.utils.terminal_width')
    def test_70(self, tw):
        # resize value column
        tw.return_value = 70
        c = ('field_name', 'a_really_long_field_name')
        d = ('the value', 'a value significantly longer than the field')
        expected = textwrap.dedent(
            '''\
        +--------------------------+-----------------------------------------+
        | Field                    | Value                                   |
        +--------------------------+-----------------------------------------+
        | field_name               | the value                               |
        | a_really_long_field_name | a value significantly longer than the   |
        |                          | field                                   |
        +--------------------------+-----------------------------------------+
        '''
        )
        self.assertEqual(
            expected,
            _table_tester_helper(c, d, extra_args=['--fit-width']),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_50(self, tw):
        # resize both columns
        tw.return_value = 50
        c = ('field_name', 'a_really_long_field_name')
        d = ('the value', 'a value significantly longer than the field')
        expected = textwrap.dedent(
            '''\
        +-----------------------+------------------------+
        | Field                 | Value                  |
        +-----------------------+------------------------+
        | field_name            | the value              |
        | a_really_long_field_n | a value significantly  |
        | ame                   | longer than the field  |
        +-----------------------+------------------------+
        '''
        )
        self.assertEqual(
            expected,
            _table_tester_helper(c, d, extra_args=['--fit-width']),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_10(self, tw):
        # resize all columns limited by min_width=16
        tw.return_value = 10
        c = ('field_name', 'a_really_long_field_name')
        d = ('the value', 'a value significantly longer than the field')
        expected = textwrap.dedent(
            '''\
        +------------------+------------------+
        | Field            | Value            |
        +------------------+------------------+
        | field_name       | the value        |
        | a_really_long_fi | a value          |
        | eld_name         | significantly    |
        |                  | longer than the  |
        |                  | field            |
        +------------------+------------------+
        '''
        )
        self.assertEqual(
            expected,
            _table_tester_helper(c, d, extra_args=['--fit-width']),
        )


class TestListFormatter(base.TestBase):
    _col_names = ('one', 'two', 'three')
    _col_data = [('one one one one one', 'two two two two', 'three three')]

    _expected_mv = {
        80: textwrap.dedent(
            '''\
        +---------------------+-----------------+-------------+
        | one                 | two             | three       |
        +---------------------+-----------------+-------------+
        | one one one one one | two two two two | three three |
        +---------------------+-----------------+-------------+
        '''
        ),
        50: textwrap.dedent(
            '''\
        +----------------+-----------------+-------------+
        | one            | two             | three       |
        +----------------+-----------------+-------------+
        | one one one    | two two two two | three three |
        | one one        |                 |             |
        +----------------+-----------------+-------------+
        '''
        ),
        47: textwrap.dedent(
            '''\
        +---------------+---------------+-------------+
        | one           | two           | three       |
        +---------------+---------------+-------------+
        | one one one   | two two two   | three three |
        | one one       | two           |             |
        +---------------+---------------+-------------+
        '''
        ),
        45: textwrap.dedent(
            '''\
        +--------------+--------------+-------------+
        | one          | two          | three       |
        +--------------+--------------+-------------+
        | one one one  | two two two  | three three |
        | one one      | two          |             |
        +--------------+--------------+-------------+
        '''
        ),
        40: textwrap.dedent(
            '''\
        +------------+------------+------------+
        | one        | two        | three      |
        +------------+------------+------------+
        | one one    | two two    | three      |
        | one one    | two two    | three      |
        | one        |            |            |
        +------------+------------+------------+
        '''
        ),
        10: textwrap.dedent(
            '''\
        +----------+----------+----------+
        | one      | two      | three    |
        +----------+----------+----------+
        | one one  | two two  | three    |
        | one one  | two two  | three    |
        | one      |          |          |
        +----------+----------+----------+
        '''
        ),
    }

    @mock.patch('cliff.utils.terminal_width')
    def test_table_list_formatter(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c')
        d1 = ('A', 'B', 'C')
        d2 = ('D', 'E', 'test\rcarriage\r\nreturn')
        data = [d1, d2]
        expected = textwrap.dedent(
            '''\
        +---+---+---------------+
        | a | b | c             |
        +---+---+---------------+
        | A | B | C             |
        | D | E | test carriage |
        |   |   | return        |
        +---+---+---------------+
        '''
        )
        self.assertEqual(expected, _table_tester_helper(c, data))

    @mock.patch('cliff.utils.terminal_width')
    def test_table_formatter_formattable_column(self, tw):
        tw.return_value = 0
        c = ('a', 'b', 'c', 'd')
        d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
        expected = textwrap.dedent(
            '''\
        +-------+---------------------------------------------+
        | Field | Value                                       |
        +-------+---------------------------------------------+
        | a     | A                                           |
        | b     | B                                           |
        | c     | C                                           |
        | d     | I made this string myself: ['the', 'value'] |
        +-------+---------------------------------------------+
        '''
        )
        self.assertEqual(expected, _table_tester_helper(c, d))

    @mock.patch('cliff.utils.terminal_width')
    def test_formattable_column(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c')
        d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
        data = [d1]
        expected = textwrap.dedent(
            '''\
        +---+---+---------------------------------------------+
        | a | b | c                                           |
        +---+---+---------------------------------------------+
        | A | B | I made this string myself: ['the', 'value'] |
        +---+---+---------------------------------------------+
        '''
        )
        self.assertEqual(expected, _table_tester_helper(c, data))

    @mock.patch('cliff.utils.terminal_width')
    def test_max_width_80(self, tw):
        # no resize
        width = tw.return_value = 80
        self.assertEqual(
            self._expected_mv[width],
            _table_tester_helper(self._col_names, self._col_data),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_max_width_50(self, tw):
        # resize 1 column
        width = tw.return_value = 50
        actual = _table_tester_helper(
            self._col_names, self._col_data, extra_args=['--fit-width']
        )
        self.assertEqual(self._expected_mv[width], actual)
        self.assertEqual(width, len(actual.splitlines()[0]))

    @mock.patch('cliff.utils.terminal_width')
    def test_max_width_45(self, tw):
        # resize 2 columns
        width = tw.return_value = 45
        actual = _table_tester_helper(
            self._col_names, self._col_data, extra_args=['--fit-width']
        )
        self.assertEqual(self._expected_mv[width], actual)
        self.assertEqual(width, len(actual.splitlines()[0]))

    @mock.patch('cliff.utils.terminal_width')
    def test_max_width_40(self, tw):
        # resize all columns
        width = tw.return_value = 40
        actual = _table_tester_helper(
            self._col_names, self._col_data, extra_args=['--fit-width']
        )
        self.assertEqual(self._expected_mv[width], actual)
        self.assertEqual(width, len(actual.splitlines()[0]))

    @mock.patch('cliff.utils.terminal_width')
    def test_max_width_10(self, tw):
        # resize all columns limited by min_width=8
        width = tw.return_value = 10
        actual = _table_tester_helper(
            self._col_names, self._col_data, extra_args=['--fit-width']
        )
        self.assertEqual(self._expected_mv[width], actual)
        # 3 columns each 8 wide, plus table spacing and borders
        expected_width = 11 * 3 + 1
        self.assertEqual(expected_width, len(actual.splitlines()[0]))

    # Force a wide terminal by overriding its width with envvar
    @mock.patch('cliff.utils.terminal_width')
    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
    def test_max_width_and_envvar_max(self, tw):
        # no resize
        tw.return_value = 80
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

        # resize 1 column
        tw.return_value = 50
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

        # resize 2 columns
        tw.return_value = 45
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

        # resize all columns
        tw.return_value = 40
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

        # resize all columns limited by min_width=8
        tw.return_value = 10
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

    # Force a narrow terminal by overriding its width with envvar
    @mock.patch('cliff.utils.terminal_width')
    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '47'})
    def test_max_width_and_envvar_mid(self, tw):
        # no resize
        tw.return_value = 80
        self.assertEqual(
            self._expected_mv[47],
            _table_tester_helper(self._col_names, self._col_data),
        )

        # resize 1 column
        tw.return_value = 50
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[47], actual)
        self.assertEqual(47, len(actual.splitlines()[0]))

        # resize 2 columns
        tw.return_value = 45
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[47], actual)
        self.assertEqual(47, len(actual.splitlines()[0]))

        # resize all columns
        tw.return_value = 40
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[47], actual)
        self.assertEqual(47, len(actual.splitlines()[0]))

        # resize all columns limited by min_width=8
        tw.return_value = 10
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[47], actual)
        self.assertEqual(47, len(actual.splitlines()[0]))

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '80'})
    def test_env_maxwidth_noresize(self):
        # no resize
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(self._col_names, self._col_data),
        )

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '50'})
    def test_env_maxwidth_resize_one(self):
        # resize 1 column
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[50], actual)
        self.assertEqual(50, len(actual.splitlines()[0]))

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '45'})
    def test_env_maxwidth_resize_two(self):
        # resize 2 columns
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[45], actual)
        self.assertEqual(45, len(actual.splitlines()[0]))

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '40'})
    def test_env_maxwidth_resize_all(self):
        # resize all columns
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[40], actual)
        self.assertEqual(40, len(actual.splitlines()[0]))

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '8'})
    def test_env_maxwidth_resize_all_tiny(self):
        # resize all columns limited by min_width=8
        actual = _table_tester_helper(self._col_names, self._col_data)
        self.assertEqual(self._expected_mv[10], actual)
        # 3 columns each 8 wide, plus table spacing and borders
        expected_width = 11 * 3 + 1
        self.assertEqual(expected_width, len(actual.splitlines()[0]))

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
    def test_env_maxwidth_args_big(self):
        self.assertEqual(
            self._expected_mv[80],
            _table_tester_helper(
                self._col_names,
                self._col_data,
                extra_args=_generate_namespace(666),
            ),
        )

    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
    def test_env_maxwidth_args_tiny(self):
        self.assertEqual(
            self._expected_mv[40],
            _table_tester_helper(
                self._col_names,
                self._col_data,
                extra_args=_generate_namespace(40),
            ),
        )

    @mock.patch('cliff.utils.terminal_width')
    def test_empty(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c')
        expected = '\n'
        self.assertEqual(expected, _table_tester_helper(c, []))

    @mock.patch('cliff.utils.terminal_width')
    def test_empty_table(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c')
        expected = textwrap.dedent(
            '''\
        +---+---+---+
        | a | b | c |
        +---+---+---+
        +---+---+---+
        '''
        )
        self.assertEqual(
            expected,
            _table_tester_helper(c, [], extra_args=['--print-empty']),
        )


class TestFieldWidths(base.TestBase):
    def test(self):
        tf = table.TableFormatter
        self.assertEqual(
            {'a': 1, 'b': 2, 'c': 3, 'd': 10},
            tf._field_widths(
                ['a', 'b', 'c', 'd'], '+---+----+-----+------------+'
            ),
        )

    def test_zero(self):
        tf = table.TableFormatter
        self.assertEqual(
            {'a': 0, 'b': 0, 'c': 0},
            tf._field_widths(['a', 'b', 'c'], '+--+-++'),
        )

    def test_info(self):
        tf = table.TableFormatter
        self.assertEqual((49, 4), (tf._width_info(80, 10)))
        self.assertEqual((76, 76), (tf._width_info(80, 1)))
        self.assertEqual((79, 0), (tf._width_info(80, 0)))
        self.assertEqual((0, 0), (tf._width_info(0, 80)))
