"""chromaterm.cli tests"""
import itertools
import os
import re
import stat
import subprocess
import sys
import time

import chromaterm
import chromaterm.cli

# pylint: disable=too-many-lines

CLI = sys.executable + ' -m chromaterm.cli'

CODE_ISATTY = """import os, sys
stdin = os.isatty(sys.stdin.fileno())
stdout = os.isatty(sys.stdout.fileno())
print('stdin={}, stdout={}'.format(stdin, stdout))"""

CODE_TTYNAME = """import os, sys
print(os.ttyname(sys.stdin.fileno()) if os.isatty(sys.stdin.fileno()) else None)"""


def get_python_command(code):
    """Returns the python shell command that runs `code`."""
    return sys.executable + ' -c "{}"'.format('; '.join(code.splitlines()))


def test_baseline_tty_test_code_no_pipe():
    """Baseline the test code with no pipes on stdin or stdout."""
    master, slave = os.openpty()
    subprocess.run(get_python_command(CODE_ISATTY),
                   check=True,
                   shell=True,
                   stdin=slave,
                   stdout=slave)

    assert 'stdin=True, stdout=True' in os.read(master, 100).decode()


def test_baseline_tty_test_code_in_pipe():
    """Baseline the test code with a pipe on stdin."""
    master, slave = os.openpty()
    subprocess.run(get_python_command(CODE_ISATTY),
                   check=True,
                   shell=True,
                   stdin=subprocess.PIPE,
                   stdout=slave)

    assert 'stdin=False, stdout=True' in os.read(master, 100).decode()


def test_baseline_tty_test_code_out_pipe():
    """Baseline the test code with a pipe on stdout."""
    _, slave = os.openpty()
    result = subprocess.run(get_python_command(CODE_ISATTY),
                            check=True,
                            shell=True,
                            stdin=slave,
                            stdout=subprocess.PIPE)

    assert 'stdin=True, stdout=False' in result.stdout.decode()


def test_baseline_tty_test_code_in_out_pipe():
    """Baseline the test code with pipes on stdin and stdout."""
    result = subprocess.run(get_python_command(CODE_ISATTY),
                            check=True,
                            shell=True,
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE)

    assert 'stdin=False, stdout=False' in result.stdout.decode()


def test_baseline_tty_test_code_ttyname_same():
    """Baseline the ttyname code, ensuring it detects matching ttys."""
    master, slave = os.openpty()

    subprocess.run(get_python_command(CODE_TTYNAME),
                   check=True,
                   shell=True,
                   stdin=slave,
                   stdout=slave)

    assert os.ttyname(slave) in os.read(master, 100).decode()


def test_baseline_tty_test_code_ttyname_different():
    """Baseline the ttyname code, ensuring it detects different ttys."""
    master, slave = os.openpty()
    _, another_slave = os.openpty()

    subprocess.run(get_python_command(CODE_TTYNAME),
                   check=True,
                   shell=True,
                   stdin=slave,
                   stdout=slave)

    assert os.ttyname(another_slave) not in os.read(master, 100).decode()


def test_config_decode_sgr_bg():
    """Background colors and reset are being detected."""
    for code in ['\x1b[48;5;12m', '\x1b[48;2;1;1;1m', '\x1b[49m', '\x1b[101m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'bg'


def test_config_decode_sgr_fg():
    """Foreground colors and reset are being detected."""
    for code in ['\x1b[38;5;12m', '\x1b[38;2;1;1;1m', '\x1b[39m', '\x1b[91m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'fg'


def test_config_decode_sgr_styles_blink():
    """Blink and its reset are being detected."""
    for code in ['\x1b[5m', '\x1b[25m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'blink'


def test_config_decode_sgr_styles_bold():
    """Bold and its reset are being detected."""
    for code in ['\x1b[1m', '\x1b[2m', '\x1b[22m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'bold'


def test_config_decode_sgr_styles_italic():
    """Italic and its reset are being detected."""
    for code in ['\x1b[3m', '\x1b[23m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'italic'


def test_config_decode_sgr_styles_strike():
    """Strike and its reset are being detected."""
    for code in ['\x1b[9m', '\x1b[29m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'strike'


def test_config_decode_sgr_styles_underline():
    """Underline and its reset are being detected."""
    for code in ['\x1b[4m', '\x1b[24m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, _, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_type == 'underline'


def test_config_decode_sgr_full_reset():
    """Full reset detection."""
    for code in ['\x1b[00m', '\x1b[0m', '\x1b[m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, color_reset, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_reset is True
            assert color_type is None


def test_config_decode_sgr_malformed():
    """Malformed colors."""
    for code in ['\x1b[38;5m', '\x1b[38;2;1;1m', '\x1b[38;5;123;38;2;1;1m']:
        colors = chromaterm.cli.Config.decode_sgr(code)
        assert len(colors) == 1

        for color_code, color_reset, color_type in colors:
            assert repr(color_code) == repr(code)
            assert color_reset is False
            assert color_type is None


def test_config_decode_sgr_split_compound():
    """Split the a compound SGR into discrete SGR's."""
    colors = chromaterm.cli.Config.decode_sgr('\x1b[1;33;40m')
    codes = ['\x1b[1m', '\x1b[33m', '\x1b[40m']
    types = ['bold', 'fg', 'bg']

    for color, color_code, color_type in zip(colors, codes, types):
        assert repr(color_code) == repr(color[0])
        assert color[1] is False
        assert color_type == color[2]


def test_config_load_simple():
    """Parse config with a simple rule."""
    config = chromaterm.cli.Config()
    config.load('''rules:
    - regex: hello world
      color: f#fffaaa''')

    assert len(config.rules) == 1


def test_config_load_group():
    """Parse config with a group-specific rule."""
    config = chromaterm.cli.Config()
    config.load('''rules:
    - description: group-specific
      regex: h(el)lo (world)
      color:
        0: bold
        1: b#fffaaa
        2: f#123123''')

    assert len(config.rules) == 1
    assert config.rules[0].description == 'group-specific'
    assert config.rules[0].regex == re.compile(r'h(el)lo (world)')
    assert config.rules[0].colors[0].color == 'bold'
    assert config.rules[0].colors[1].color == 'b#fffaaa'
    assert config.rules[0].colors[2].color == 'f#123123'


def test_config_load_multiple_colors():
    """Parse config with a multi-color rule."""
    config = chromaterm.cli.Config()
    config.load('''rules:
    - regex: hello (world)
      color: b#fffaaa f#aaafff''')

    assert len(config.rules) == 1
    assert config.rules[0].color.color == 'b#fffaaa f#aaafff'


def test_config_load_rule_format_error(capsys):
    """Parse a config file with a syntax problem."""
    config = chromaterm.cli.Config()
    config.load('''rules:
    - 1''')

    assert 'Rule 1 not a dictionary' in capsys.readouterr().err


def test_config_load_yaml_format_error(capsys):
    """Parse an incorrect YAML file."""
    config = chromaterm.cli.Config()
    config.load('-x-\nhi:')

    assert 'Parse error:' in capsys.readouterr().err


def test_config_parse_rule_regex_missing():
    """Parse a rule without a `regex` key."""
    msg = 'regex must be a string'

    rule = {'color': 'b#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)


def test_config_parse_rule_regex_type_error():
    """Parse a rule with an incorrect `regex` value type."""
    msg = 'regex must be a string'

    rule = {'regex': ['hi'], 'color': 'b#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)

    rule = {'regex': 111, 'color': 'b#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)


def test_config_parse_rule_regex_invalid():
    """Parse a rule with an invalid `regex`."""
    msg = 're.error: '

    rule = {'regex': '+', 'color': 'b#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)


def test_config_parse_rule_color_missing():
    """Parse a rule without a `color` key."""
    msg_re = r'color .* is not a string'

    rule = {'regex': 'x(y)z'}
    assert re.search(msg_re, chromaterm.cli.Config.parse_rule(rule))


def test_config_parse_rule_color_type_error():
    """Parse a rule with an incorrect `color` value type."""
    msg_re = r'color .* is not a string'

    rule = {'regex': 'x(y)z', 'color': ['hi']}
    assert re.search(msg_re, chromaterm.cli.Config.parse_rule(rule))


def test_config_parse_rule_color_format_error():
    """Parse a rule with an incorrect `color` format."""
    msg = 'invalid color format'

    rule = {'regex': 'x(y)z', 'color': 'b#xyzxyz'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)

    rule = {'regex': 'x(y)z', 'color': 'x#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)

    rule = {'regex': 'x(y)z', 'color': 'b@fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)

    rule = {'regex': 'x(y)z', 'color': 'b#fffaaa-f#fffaaa'}
    assert msg in chromaterm.cli.Config.parse_rule(rule)


def test_config_parse_rule_group_type_error():
    """Parse a rule with an incorrect `group` value type."""
    msg = 'group must be an integer'

    rule = {'regex': 'x(y)z', 'color': {'1': 'b#fffaaa'}}
    assert msg in chromaterm.cli.Config.parse_rule(rule)


def test_config_parse_rule_group_out_of_bounds():
    """Parse a rule with `group` number not in the regex."""
    msg_re = r'regex only has .* group\(s\); .* is invalid'

    rule = {'regex': 'x(y)z', 'color': {2: 'b#fffaaa'}}
    assert re.search(msg_re, chromaterm.cli.Config.parse_rule(rule))


def test_config_highlight_tracking_common_beginning_type_different():
    """A rule with a match that has a color of a different type just before its
    start. The rule's color is closer to the match and the reset is unaffected by
    the existing color.
    1: x-------------"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('b#123123'))
    config.add_rule(rule)

    data = '\x1b[33mhello'
    expected = [
        '\x1b[33m', rule.color.color_code, 'hello', rule.color.color_reset
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_common_beginning_type_same():
    """A rule with a match that has a color of the same type just before its
    start. The rule's color is closer to the match and the reset used is the
    existing color.
    1: x-------------"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    data = '\x1b[33mhello'
    expected = ['\x1b[33m', rule.color.color_code, 'hello', '\x1b[33m']

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_common_end_type_different():
    """A rule with a match that has a color of a different type just after its
    end. The rule's reset is closer to the match and the reset is unaffected by
    the existing color.
    1: -------------x"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('b#123123'))
    config.add_rule(rule)

    data = 'hello\x1b[33m'
    expected = [
        rule.color.color_code, 'hello', rule.color.color_reset, '\x1b[33m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_common_end_type_same():
    """A rule with a match that has a color of the same type just after its end.
    The rule's reset is closer to the match and is unaffected by the existing
    color.
    1: -------------x"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    data = 'hello\x1b[33m'
    expected = [
        rule.color.color_code, 'hello', rule.color.color_reset, '\x1b[33m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_full_reset_beginning():
    """A rule with a match that has a full reset just before the start of the
    match. The rule's color is closer to the match and the reset used is the
    default for that color type.
    1: R-------------"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    data = '\x1b[mhello'
    expected = [
        '\x1b[m', rule.color.color_code, 'hello',
        chromaterm.COLOR_TYPES[rule.color.color_types[0][0]]['reset']
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_full_reset_end():
    """A rule with a match that has a full reset just after the end of the match.
    The rule's reset is closer to the match and the reset used is the default for
    that color type.
    1: -------------R"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    data = 'hello\x1b[m'
    expected = [
        rule.color.color_code, 'hello',
        chromaterm.COLOR_TYPES[rule.color.color_types[0][0]]['reset'], '\x1b[m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_full_reset_middle():
    """A rule with a match that has a full reset in the middle of it. The full
    reset is changed to the color code of the match and the reset of the match
    is changed to a full reset.
    1: ------R-------"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    data = 'hel\x1b[mlo'
    expected = [
        rule.color.color_code, 'hel', rule.color.color_code, 'lo', '\x1b[m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_malformed():
    """A rule with a match that has a malformed SGR in the middle. It should be
    ignored and inserted back into the match. Highlighting from the rule should
    still go through.
    1: x-------------"""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('b#123123'))
    config.add_rule(rule)

    data = 'he\x1b[38;5mllo'
    expected = [
        rule.color.color_code, 'he', '\x1b[38;5m', 'llo',
        rule.color.color_reset
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_mixed_full_reset():
    """Track multiple color types and ensure a full reset only defaults the types
    that were not updated by other colors in the data."""
    config = chromaterm.cli.Config()

    rule1 = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    rule2 = chromaterm.Rule('world', color=chromaterm.Color('b#123123'))
    config.add_rule(rule1)
    config.add_rule(rule2)

    data = '\x1b[33mhello\x1b[m there \x1b[43mworld'
    expected = [
        '\x1b[33m', rule1.color.color_code, 'hello', '\x1b[33m', '\x1b[m',
        ' there ', '\x1b[43m', rule2.color.color_code, 'world', '\x1b[43m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))

    # The color of rule1 was reset to its default because a full reset came after
    # it, but the color of rule2 was already updated so it wasn't affected by the
    # full reset
    data = 'hello there world'
    expected = [
        rule1.color.color_code, 'hello', rule1.color.color_reset, ' there ',
        rule2.color.color_code, 'world', '\x1b[43m'
    ]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_multiline_type_different():
    """Ensure that data with an existing color is tracked across highlights and
    does not affect the reset of a color of a different type."""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('b#123123'))
    config.add_rule(rule)

    # Inject a foreground color to have it tracked
    assert repr(config.highlight('\x1b[33m')) == repr('\x1b[33m')

    data = 'hello'
    expected = [rule.color.color_code, 'hello', rule.color.color_reset]

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_tracking_multiline_type_same():
    """Ensure that data with an existing color is tracked across highlights and
    affects the reset of a color of the same type."""
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello', color=chromaterm.Color('f#321321'))
    config.add_rule(rule)

    # Inject a foreground color to have it tracked
    assert repr(config.highlight('\x1b[33m')) == repr('\x1b[33m')

    data = 'hello'
    expected = [rule.color.color_code, 'hello', '\x1b[33m']

    assert repr(config.highlight(data)) == repr(''.join(expected))


def test_config_highlight_no_rules():
    """Highlight with config that has no rules – nothing is changed."""
    config = chromaterm.cli.Config()
    assert repr(config.highlight('hello world')) == repr('hello world')


def test_eprint(capsys):
    """Print a message to stderr."""
    msg = 'Some random error message'
    chromaterm.cli.eprint(msg)
    assert msg in capsys.readouterr().err


def test_process_input_decode_error(capsys):
    """Attempt to decode a character that is not UTF-8."""
    pipe_r, pipe_w = os.pipe()
    config = chromaterm.cli.Config()

    os.write(pipe_w, b'\x80')
    chromaterm.cli.process_input(config, pipe_r, max_wait=0)

    assert capsys.readouterr().out == '�'


def test_process_input_empty(capsys):
    """Input processing of empty input."""
    pipe_r, _ = os.pipe()
    config = chromaterm.cli.Config()

    chromaterm.cli.process_input(config, pipe_r, max_wait=0)

    assert capsys.readouterr().out == ''


def test_process_input_multiline(capsys):
    """Input processing with multiple lines of data."""
    pipe_r, pipe_w = os.pipe()
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('hello world', color=chromaterm.Color('bold'))
    config.add_rule(rule)

    os.write(pipe_w, b'\nt hello world t\n' * 2)
    chromaterm.cli.process_input(config, pipe_r, max_wait=0)

    assert capsys.readouterr().out == '\nt \x1b[1mhello world\x1b[22m t\n' * 2


def test_process_input_single_character(capsys):
    """Input processing for a single character. Even with a rule that matches
    single character, the output should not be highlighted as it is typically
    just keyboard input."""
    pipe_r, pipe_w = os.pipe()
    config = chromaterm.cli.Config()

    rule = chromaterm.Rule('.', color=chromaterm.Color('bold'))
    config.add_rule(rule)

    os.write(pipe_w, b'x')
    chromaterm.cli.process_input(config, pipe_r, max_wait=0)

    assert capsys.readouterr().out == 'x'


def test_read_file():
    """Read the default configuration file."""
    file = os.path.join(os.path.expanduser('~'), '.chromaterm.yml')
    assert chromaterm.cli.read_file(file) is not None


def test_read_file_no_file(capsys):
    """Read a non-existent file."""
    msg = 'Configuration file ' + __name__ + '1' + ' not found\n'
    chromaterm.cli.read_file(__name__ + '1')
    assert msg in capsys.readouterr().err


def test_read_file_no_permission(capsys):
    """Create a file with no permissions and attempt to read it. Delete the file
    once done with it."""
    msg = 'Cannot read configuration file ' + __name__ + '2' + ' (permission)\n'

    os.close(os.open(__name__ + '2', os.O_CREAT | os.O_WRONLY, 0o0000))
    chromaterm.cli.read_file(__name__ + '2')
    os.chmod(__name__ + '2', stat.S_IWRITE)
    os.remove(__name__ + '2')

    assert msg in capsys.readouterr().err


def test_read_ready_input():
    """Immediate ready when there is input buffered."""
    pipe_r, pipe_w = os.pipe()

    os.write(pipe_w, b'Hello world')
    assert chromaterm.cli.read_ready(pipe_r)


def test_read_ready_timeout_empty():
    """Wait with no input."""
    pipe_r, _ = os.pipe()

    before = time.time()
    assert not chromaterm.cli.read_ready(pipe_r, timeout=0.1)

    after = time.time()
    assert after - before >= 0.1


def test_read_ready_timeout_input():
    """Immediate ready with timeout when there is input buffered."""
    pipe_r, pipe_w = os.pipe()

    os.write(pipe_w, b'Hello world')
    before = time.time()
    assert chromaterm.cli.read_ready(pipe_r, timeout=0.1)

    after = time.time()
    assert after - before < 0.1


def test_split_buffer_new_line_r():
    """Split based on \\r"""
    data = 'Hello \rWorld'
    expected = (('Hello ', '\r'), ('World', ''))

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_new_line_r_n():
    """Split based on \\r\\n"""
    data = 'Hello \r\n World'
    expected = (('Hello ', '\r\n'), (' World', ''))

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_new_line_n():
    """Split based on \\n"""
    data = 'Hello \n World'
    expected = (('Hello ', '\n'), (' World', ''))

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_vertical_space():
    """Split based on \\v"""
    data = 'Hello \v World'
    expected = (('Hello ', '\v'), (' World', ''))

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_form_feed():
    """Split based on \\f"""
    data = 'Hello \f World'
    expected = (('Hello ', '\f'), (' World', ''))

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_c1_set():
    """Split based on the ECMA-048 C1 set, excluding CSI and OSC."""
    c1_except_csi_and_osc = itertools.chain(
        range(int('40', 16), int('5b', 16)),
        [int('5c', 16), int('5e', 16),
         int('5f', 16)],
    )

    for char_id in c1_except_csi_and_osc:
        data = 'Hello \x1b{} World'.format(chr(char_id))
        expected = (('Hello ', '\x1b' + chr(char_id)), (' World', ''))

        assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_csi_exclude_sgr():
    """Fail to split based on the ECMA-048 C1 CSI SGR. Added some intermediate
    characters to prevent matching other CSI codes; strictly checking empty SGR."""
    data = 'Hello \x1b[!0World'
    expected = (('Hello \x1b[!0World', ''), )

    assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_csi_no_parameter_no_intermediate():
    """Split based on CSI with no parameter or intermediate bytes."""
    csi_up_to_sgr = range(int('40', 16), int('6d', 16))
    csi_above_sgr = range(int('6e', 16), int('7f', 16))

    for char_id in itertools.chain(csi_up_to_sgr, csi_above_sgr):
        data = 'Hello \x1b[{} World'.format(chr(char_id))
        expected = (('Hello ', '\x1b[' + chr(char_id)), (' World', ''))

        assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_csi_no_parameter_intermediate():
    """Split based on CSI with intermediate bytes but no parameter bytes."""
    csi_up_to_sgr = range(int('40', 16), int('6d', 16))
    csi_above_sgr = range(int('6e', 16), int('7f', 16))

    for char_id in itertools.chain(csi_up_to_sgr, csi_above_sgr):
        for intermediate in range(int('20', 16), int('30', 16)):
            for count in range(1, 4):
                code = chr(intermediate) * count + chr(char_id)
                data = 'Hello \x1b[{} World'.format(code)
                expected = (('Hello ', '\x1b[' + code), (' World', ''))

                assert repr(
                    chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_csi_parameter_intermediate():
    """Split based on CSI with parameter and intermediate bytes. Up to 3 bytes
    each."""
    csi_up_to_sgr = range(int('40', 16), int('6d', 16))
    csi_above_sgr = range(int('6e', 16), int('7f', 16))

    for char_id in itertools.chain(csi_up_to_sgr, csi_above_sgr):
        for parameter in range(int('30', 16), int('40', 16)):
            for intermediate in range(int('20', 16), int('30', 16)):
                for count in range(1, 4):
                    code = chr(parameter) * count + chr(
                        intermediate) * count + chr(char_id)
                    data = 'Hello \x1b[{} World'.format(code)
                    expected = (('Hello ', '\x1b[' + code), (' World', ''))

                    assert repr(
                        chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_csi_parameter_no_intermediate():
    """Split based on CSI with parameters bytes but no intermediate bytes. Up to
    3 bytes."""
    csi_up_to_sgr = range(int('40', 16), int('6d', 16))
    csi_above_sgr = range(int('6e', 16), int('7f', 16))

    for char_id in itertools.chain(csi_up_to_sgr, csi_above_sgr):
        for parameter in range(int('30', 16), int('40', 16)):
            for count in range(1, 4):
                code = chr(parameter) * count + chr(char_id)
                data = 'Hello \x1b[{} World'.format(code)
                expected = (('Hello ', '\x1b[' + code), (' World', ''))

                assert repr(
                    chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_osc_title():
    """Operating System Command (OSC) can supply arbitrary commands within the
    visible character set."""
    for end in ['\x07', '\x1b\x5c']:
        osc = '\x1b]Ignored{}'.format(end)
        data = '{}Hello world'.format(osc)
        expected = (('', osc), ('Hello world', ''))

        assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_split_buffer_scs():
    """Select Character Set (SCS) is used to change the terminal character set."""
    g_sets = ['\x28', '\x29', '\x2a', '\x2b', '\x2d', '\x2e', '\x2f']
    char_sets = map(chr, range(int('20', 16), int('7e', 16)))

    for g_set in g_sets:
        for char_set in char_sets:
            code = '\x1b{}{}'.format(g_set, char_set)
            data = 'Hello {} World'.format(code)
            expected = (('Hello ', code), (' World', ''))

            assert repr(chromaterm.cli.split_buffer(data)) == repr(expected)


def test_main_broken_pipe():
    """Break a pipe while CT is trying to write to it. The echo at the end will
    close before CT has had the chance to write to the pipe."""
    result = subprocess.run('echo | ' + CLI + ' | echo',
                            check=False,
                            shell=True,
                            stderr=subprocess.PIPE)

    assert b'Broken pipe' not in result.stderr


def test_main_buffer_close_time():
    """Confirm that the program exists as soon as stdin closes."""
    before = time.time()
    subprocess.run('echo hi | ' + CLI, check=True, shell=True)
    after = time.time()

    assert after - before < 1


def test_main_reload_config():
    """Reload the configuration while the program is running."""
    try:
        # The initial configuration file
        with open(__name__ + '3', 'w') as file:
            file.write('''rules:
            - regex: Hello
              color: f#123123
            - regex: world
              color: b#123123''')

        expected = [
            '\x1b[38;2;18;49;35m', 'Hello', '\x1b[39m', ' ',
            '\x1b[48;2;18;49;35m', 'world', '\x1b[49m', '\n'
        ]

        stdin_r, stdin_w = os.pipe()
        stdout_r, stdout_w = os.pipe()

        process = subprocess.Popen(CLI + ' --rgb --config ' + __name__ + '3',
                                   shell=True,
                                   stdin=stdin_r,
                                   stdout=stdout_w)

        os.write(stdin_w, b'Hello world\n')
        time.sleep(0.1)  # Any processing delay

        assert repr(os.read(stdout_r, 100).decode()) == repr(''.join(expected))

        # Create file without the 'world' rule
        os.remove(__name__ + '3')
        with open(__name__ + '3', 'w') as file:
            file.write('''rules:
            - regex: Hello
              color: f#123123''')

        expected = ['\x1b[38;2;18;49;35m', 'Hello', '\x1b[39m', ' world\n']

        # Reload config
        subprocess.run(CLI + ' --reload', check=False, shell=True)

        os.write(stdin_w, b'Hello world\n')
        time.sleep(0.1)  # Any processing delay

        assert repr(os.read(stdout_r, 100).decode()) == repr(''.join(expected))
    finally:
        os.close(stdin_r)
        os.close(stdin_w)
        os.close(stdout_r)
        os.close(stdout_w)
        os.remove(__name__ + '3')
        process.kill()


def test_main_reload_processes():
    """Reload all other CT processes."""
    processes = [
        subprocess.Popen('sleep 1 | ' + CLI, shell=True) for _ in range(3)
    ]

    result = subprocess.run(CLI + ' --reload',
                            check=False,
                            shell=True,
                            stderr=subprocess.PIPE)

    assert result.stderr == b'Processes reloaded: 3\n'

    for process in processes:
        process.wait()


def test_main_run_child_ttyname():
    """Ensure that CT spawns the child in a pseudo-terminal."""
    master, slave = os.openpty()
    subprocess.run(CLI + ' ' + get_python_command(CODE_TTYNAME),
                   check=True,
                   shell=True,
                   stdin=slave,
                   stdout=slave)

    assert os.ttyname(slave) not in os.read(master, 100).decode()


def test_main_run_no_file_found():
    """Have CT run with an unavailable command."""
    result = subprocess.run(CLI + ' plz-no-work',
                            check=False,
                            stdout=subprocess.PIPE,
                            shell=True)

    output = re.sub(br'\x1b\[[\d;]+?m', b'', result.stdout)
    assert b'plz-no-work: command not found' in output


def test_main_run_no_pipe():
    """Have CT run the tty test code with no pipes."""
    master, slave = os.openpty()
    subprocess.run(CLI + ' ' + get_python_command(CODE_ISATTY),
                   check=True,
                   shell=True,
                   stdin=slave,
                   stdout=slave)

    assert 'stdin=True, stdout=True' in os.read(master, 100).decode()


def test_main_run_pipe_in():
    """Have CT run the tty test code with a pipe on stdin."""
    master, slave = os.openpty()
    subprocess.run(CLI + ' ' + get_python_command(CODE_ISATTY),
                   check=True,
                   shell=True,
                   stdin=subprocess.PIPE,
                   stdout=slave)

    assert 'stdin=True, stdout=True' in os.read(master, 100).decode()


def test_main_run_pipe_in_out():
    """Have CT run the tty test code with pipes on stdin and stdout."""
    result = subprocess.run(CLI + ' ' + get_python_command(CODE_ISATTY),
                            check=True,
                            shell=True,
                            stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE)

    assert 'stdin=True, stdout=True' in result.stdout.decode()


def test_main_run_pipe_out():
    """Have CT run the tty test code with a pipe on stdout."""
    _, slave = os.openpty()
    result = subprocess.run(CLI + ' ' + get_python_command(CODE_ISATTY),
                            check=True,
                            shell=True,
                            stdin=slave,
                            stdout=subprocess.PIPE)

    assert 'stdin=True, stdout=True' in result.stdout.decode()


def test_main_stdin_processing():
    """General stdin processing with relation to READ_SIZE and WAIT_FOR_SPLIT."""
    try:
        stdin_r, stdin_w = os.pipe()
        stdout_r, stdout_w = os.pipe()

        process = subprocess.Popen(CLI,
                                   shell=True,
                                   stdin=stdin_r,
                                   stdout=stdout_w)

        time.sleep(0.5)  # Any startup delay
        assert process.poll() is None

        os.write(stdin_w, b'Hello world\n')
        time.sleep(0.1)  # Any processing delay
        assert os.read(stdout_r, 100) == b'Hello world\n'

        os.write(stdin_w, b'Hey there')
        time.sleep(0.1 + chromaterm.cli.WAIT_FOR_SPLIT)  # Include split wait
        assert os.read(stdout_r, 100) == b'Hey there'

        write_size = chromaterm.cli.READ_SIZE + 1
        os.write(stdin_w, b'x' * write_size)
        time.sleep(0.1 + chromaterm.cli.WAIT_FOR_SPLIT)  # Include split wait
        assert os.read(stdout_r, write_size * 2) == b'x' * write_size
    finally:
        os.close(stdin_r)
        os.close(stdin_w)
        os.close(stdout_r)
        os.close(stdout_w)
        process.kill()