#!/usr/bin/env python
#
#  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


class args(object):
    def __init__(self, max_width=0, print_empty=False, fit_width=False):
        self.fit_width = fit_width
        if max_width > 0:
            self.max_width = max_width
        else:
            # Envvar is only taken into account iff CLI parameter not given
            self.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0))
        self.print_empty = print_empty


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

    tags - tuple of strings for data tags (column headers or fields)
    data - tuple of strings for single data row
         - list of tuples of strings for multiple rows of data
    extra_args - an instance of class args
               - a list of strings for CLI arguments
    """
    sf = table.TableFormatter()

    if extra_args is None:
        # Default to no CLI arguments
        parsed_args = args()
    elif type(extra_args) == args:
        # 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

    # 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

    @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=args(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=args()),
        )

    @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=args(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=args(40)),
        )

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

    @mock.patch('cliff.utils.terminal_width')
    def test_empty_table(self, tw):
        tw.return_value = 80
        c = ('a', 'b', 'c')
        data = []
        expected = textwrap.dedent('''\
        +---+---+---+
        | a | b | c |
        +---+---+---+
        +---+---+---+
        ''')
        self.assertEqual(
            expected,
            _table_tester_helper(c, data,
                                 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)))
