# coding=utf-8
# flake8: noqa E302
"""
Unit testing for cmd2/utils.py module.
"""
import os
import signal
import sys
import time

import pytest

import cmd2.utils as cu
from cmd2 import (
    ansi,
)
from cmd2.constants import (
    HORIZONTAL_ELLIPSIS,
)

try:
    import mock
except ImportError:
    from unittest import (
        mock,
    )


HELLO_WORLD = 'Hello, world!'


def test_strip_quotes_no_quotes():
    base_str = HELLO_WORLD
    stripped = cu.strip_quotes(base_str)
    assert base_str == stripped


def test_strip_quotes_with_quotes():
    base_str = '"' + HELLO_WORLD + '"'
    stripped = cu.strip_quotes(base_str)
    assert stripped == HELLO_WORLD


def test_remove_duplicates_no_duplicates():
    no_dups = [5, 4, 3, 2, 1]
    assert cu.remove_duplicates(no_dups) == no_dups


def test_remove_duplicates_with_duplicates():
    duplicates = [1, 1, 2, 3, 9, 9, 7, 8]
    assert cu.remove_duplicates(duplicates) == [1, 2, 3, 9, 7, 8]


def test_unicode_normalization():
    s1 = 'café'
    s2 = 'cafe\u0301'
    assert s1 != s2
    assert cu.norm_fold(s1) == cu.norm_fold(s2)


def test_unicode_casefold():
    micro = 'µ'
    micro_cf = micro.casefold()
    assert micro != micro_cf
    assert cu.norm_fold(micro) == cu.norm_fold(micro_cf)


def test_alphabetical_sort():
    my_list = ['café', 'µ', 'A', 'micro', 'unity', 'cafeteria']
    assert cu.alphabetical_sort(my_list) == ['A', 'cafeteria', 'café', 'micro', 'unity', 'µ']
    my_list = ['a3', 'a22', 'A2', 'A11', 'a1']
    assert cu.alphabetical_sort(my_list) == ['a1', 'A11', 'A2', 'a22', 'a3']


def test_try_int_or_force_to_lower_case():
    str1 = '17'
    assert cu.try_int_or_force_to_lower_case(str1) == 17
    str1 = 'ABC'
    assert cu.try_int_or_force_to_lower_case(str1) == 'abc'
    str1 = 'X19'
    assert cu.try_int_or_force_to_lower_case(str1) == 'x19'
    str1 = ''
    assert cu.try_int_or_force_to_lower_case(str1) == ''


def test_natural_keys():
    my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22']
    my_list.sort(key=cu.natural_keys)
    assert my_list == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ']
    my_list = ['a3', 'a22', 'A2', 'A11', 'a1']
    my_list.sort(key=cu.natural_keys)
    assert my_list == ['a1', 'A2', 'a3', 'A11', 'a22']


def test_natural_sort():
    my_list = ['café', 'µ', 'A', 'micro', 'unity', 'x1', 'X2', 'X11', 'X0', 'x22']
    assert cu.natural_sort(my_list) == ['A', 'café', 'micro', 'unity', 'X0', 'x1', 'X2', 'X11', 'x22', 'µ']
    my_list = ['a3', 'a22', 'A2', 'A11', 'a1']
    assert cu.natural_sort(my_list) == ['a1', 'A2', 'a3', 'A11', 'a22']


def test_is_quoted_short():
    my_str = ''
    assert not cu.is_quoted(my_str)
    your_str = '"'
    assert not cu.is_quoted(your_str)


def test_is_quoted_yes():
    my_str = '"This is a test"'
    assert cu.is_quoted(my_str)
    your_str = "'of the emergengy broadcast system'"
    assert cu.is_quoted(your_str)


def test_is_quoted_no():
    my_str = '"This is a test'
    assert not cu.is_quoted(my_str)
    your_str = "of the emergengy broadcast system'"
    assert not cu.is_quoted(your_str)
    simple_str = "hello world"
    assert not cu.is_quoted(simple_str)


def test_quote_string():
    my_str = "Hello World"
    assert cu.quote_string(my_str) == '"' + my_str + '"'

    my_str = "'Hello World'"
    assert cu.quote_string(my_str) == '"' + my_str + '"'

    my_str = '"Hello World"'
    assert cu.quote_string(my_str) == "'" + my_str + "'"


def test_quote_string_if_needed_yes():
    my_str = "Hello World"
    assert cu.quote_string_if_needed(my_str) == '"' + my_str + '"'
    your_str = '"foo" bar'
    assert cu.quote_string_if_needed(your_str) == "'" + your_str + "'"


def test_quote_string_if_needed_no():
    my_str = "HelloWorld"
    assert cu.quote_string_if_needed(my_str) == my_str
    your_str = "'Hello World'"
    assert cu.quote_string_if_needed(your_str) == your_str


@pytest.fixture
def stdout_sim():
    stdsim = cu.StdSim(sys.stdout, echo=True)
    return stdsim


def test_stdsim_write_str(stdout_sim):
    my_str = 'Hello World'
    stdout_sim.write(my_str)
    assert stdout_sim.getvalue() == my_str


def test_stdsim_write_bytes(stdout_sim):
    b_str = b'Hello World'
    with pytest.raises(TypeError):
        stdout_sim.write(b_str)


def test_stdsim_buffer_write_bytes(stdout_sim):
    b_str = b'Hello World'
    stdout_sim.buffer.write(b_str)
    assert stdout_sim.getvalue() == b_str.decode()
    assert stdout_sim.getbytes() == b_str


def test_stdsim_buffer_write_str(stdout_sim):
    my_str = 'Hello World'
    with pytest.raises(TypeError):
        stdout_sim.buffer.write(my_str)


def test_stdsim_read(stdout_sim):
    my_str = 'Hello World'
    stdout_sim.write(my_str)
    # getvalue() returns the value and leaves it unaffected internally
    assert stdout_sim.getvalue() == my_str
    # read() returns the value and then clears the internal buffer
    assert stdout_sim.read() == my_str
    assert stdout_sim.getvalue() == ''

    stdout_sim.write(my_str)

    assert stdout_sim.getvalue() == my_str
    assert stdout_sim.read(2) == my_str[:2]
    assert stdout_sim.getvalue() == my_str[2:]


def test_stdsim_read_bytes(stdout_sim):
    b_str = b'Hello World'
    stdout_sim.buffer.write(b_str)
    # getbytes() returns the value and leaves it unaffected internally
    assert stdout_sim.getbytes() == b_str
    # read_bytes() returns the value and then clears the internal buffer
    assert stdout_sim.readbytes() == b_str
    assert stdout_sim.getbytes() == b''


def test_stdsim_clear(stdout_sim):
    my_str = 'Hello World'
    stdout_sim.write(my_str)
    assert stdout_sim.getvalue() == my_str
    stdout_sim.clear()
    assert stdout_sim.getvalue() == ''


def test_stdsim_getattr_exist(stdout_sim):
    # Here the StdSim getattr is allowing us to access methods within StdSim
    my_str = 'Hello World'
    stdout_sim.write(my_str)
    val_func = getattr(stdout_sim, 'getvalue')
    assert val_func() == my_str


def test_stdsim_getattr_noexist(stdout_sim):
    # Here the StdSim getattr is allowing us to access methods defined by the inner stream
    assert not stdout_sim.isatty()


def test_stdsim_pause_storage(stdout_sim):
    # Test pausing storage for string data
    my_str = 'Hello World'

    stdout_sim.pause_storage = False
    stdout_sim.write(my_str)
    assert stdout_sim.read() == my_str

    stdout_sim.pause_storage = True
    stdout_sim.write(my_str)
    assert stdout_sim.read() == ''

    # Test pausing storage for binary data
    b_str = b'Hello World'

    stdout_sim.pause_storage = False
    stdout_sim.buffer.write(b_str)
    assert stdout_sim.readbytes() == b_str

    stdout_sim.pause_storage = True
    stdout_sim.buffer.write(b_str)
    assert stdout_sim.getbytes() == b''


def test_stdsim_line_buffering(base_app):
    # This exercises the case of writing binary data that contains new lines/carriage returns to a StdSim
    # when line buffering is on. The output should immediately be flushed to the underlying stream.
    import os
    import tempfile

    file = tempfile.NamedTemporaryFile(mode='wt')
    file.line_buffering = True

    stdsim = cu.StdSim(file, echo=True)
    saved_size = os.path.getsize(file.name)

    bytes_to_write = b'hello\n'
    stdsim.buffer.write(bytes_to_write)
    assert os.path.getsize(file.name) == saved_size + len(bytes_to_write)
    saved_size = os.path.getsize(file.name)

    bytes_to_write = b'hello\r'
    stdsim.buffer.write(bytes_to_write)
    assert os.path.getsize(file.name) == saved_size + len(bytes_to_write)


@pytest.fixture
def pr_none():
    import subprocess

    # Start a long running process so we have time to run tests on it before it finishes
    # Put the new process into a separate group so its signal are isolated from ours
    kwargs = dict()
    if sys.platform.startswith('win'):
        command = 'timeout -t 5 /nobreak'
        kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        command = 'sleep 5'
        kwargs['start_new_session'] = True

    proc = subprocess.Popen(command, shell=True, **kwargs)
    pr = cu.ProcReader(proc, None, None)
    return pr


def test_proc_reader_send_sigint(pr_none):
    assert pr_none._proc.poll() is None
    pr_none.send_sigint()

    wait_start = time.monotonic()
    pr_none.wait()
    wait_finish = time.monotonic()

    # Make sure the process exited before sleep of 5 seconds finished
    # 3 seconds accounts for some delay but is long enough for the process to exit
    assert wait_finish - wait_start < 3

    ret_code = pr_none._proc.poll()
    if sys.platform.startswith('win'):
        assert ret_code is not None
    else:
        assert ret_code == -signal.SIGINT


def test_proc_reader_terminate(pr_none):
    assert pr_none._proc.poll() is None
    pr_none.terminate()

    wait_start = time.monotonic()
    pr_none.wait()
    wait_finish = time.monotonic()

    # Make sure the process exited before sleep of 5 seconds finished
    # 3 seconds accounts for some delay but is long enough for the process to exit
    assert wait_finish - wait_start < 3

    ret_code = pr_none._proc.poll()
    if sys.platform.startswith('win'):
        assert ret_code is not None
    else:
        assert ret_code == -signal.SIGTERM


@pytest.fixture
def context_flag():
    return cu.ContextFlag()


def test_context_flag_bool(context_flag):
    assert not context_flag
    with context_flag:
        assert context_flag


def test_context_flag_exit_err(context_flag):
    with pytest.raises(ValueError):
        context_flag.__exit__()


def test_remove_overridden_styles():
    from typing import (
        List,
    )

    from cmd2 import (
        Bg,
        EightBitBg,
        EightBitFg,
        Fg,
        RgbBg,
        RgbFg,
        TextStyle,
    )

    def make_strs(styles_list: List[ansi.AnsiSequence]) -> List[str]:
        return [str(s) for s in styles_list]

    # Test Reset All
    styles_to_parse = make_strs([Fg.BLUE, TextStyle.UNDERLINE_DISABLE, TextStyle.INTENSITY_DIM, TextStyle.RESET_ALL])
    expected = make_strs([TextStyle.RESET_ALL])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([Fg.BLUE, TextStyle.UNDERLINE_DISABLE, TextStyle.INTENSITY_DIM, TextStyle.ALT_RESET_ALL])
    expected = make_strs([TextStyle.ALT_RESET_ALL])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    # Test colors
    styles_to_parse = make_strs([Fg.BLUE, Fg.RED, Fg.GREEN, Bg.BLUE, Bg.RED, Bg.GREEN])
    expected = make_strs([Fg.GREEN, Bg.GREEN])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([EightBitFg.BLUE, EightBitFg.RED, EightBitBg.BLUE, EightBitBg.RED])
    expected = make_strs([EightBitFg.RED, EightBitBg.RED])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([RgbFg(0, 3, 4), RgbFg(5, 6, 7), RgbBg(8, 9, 10), RgbBg(11, 12, 13)])
    expected = make_strs([RgbFg(5, 6, 7), RgbBg(11, 12, 13)])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    # Test text styles
    styles_to_parse = make_strs([TextStyle.INTENSITY_DIM, TextStyle.INTENSITY_NORMAL, TextStyle.ITALIC_ENABLE])
    expected = make_strs([TextStyle.INTENSITY_NORMAL, TextStyle.ITALIC_ENABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([TextStyle.INTENSITY_DIM, TextStyle.ITALIC_ENABLE, TextStyle.ITALIC_DISABLE])
    expected = make_strs([TextStyle.INTENSITY_DIM, TextStyle.ITALIC_DISABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([TextStyle.INTENSITY_BOLD, TextStyle.OVERLINE_DISABLE, TextStyle.OVERLINE_ENABLE])
    expected = make_strs([TextStyle.INTENSITY_BOLD, TextStyle.OVERLINE_ENABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([TextStyle.OVERLINE_DISABLE, TextStyle.STRIKETHROUGH_DISABLE, TextStyle.STRIKETHROUGH_ENABLE])
    expected = make_strs([TextStyle.OVERLINE_DISABLE, TextStyle.STRIKETHROUGH_ENABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([TextStyle.STRIKETHROUGH_DISABLE, TextStyle.UNDERLINE_DISABLE, TextStyle.UNDERLINE_ENABLE])
    expected = make_strs([TextStyle.STRIKETHROUGH_DISABLE, TextStyle.UNDERLINE_ENABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    styles_to_parse = make_strs([TextStyle.UNDERLINE_DISABLE])
    expected = make_strs([TextStyle.UNDERLINE_DISABLE])
    assert cu._remove_overridden_styles(styles_to_parse) == expected

    # Test unrecognized styles
    slow_blink = ansi.CSI + str(5)
    rapid_blink = ansi.CSI + str(6)
    styles_to_parse = [slow_blink, rapid_blink]
    expected = styles_to_parse
    assert cu._remove_overridden_styles(styles_to_parse) == expected


def test_truncate_line():
    line = 'long'
    max_width = 3
    truncated = cu.truncate_line(line, max_width)
    assert truncated == 'lo' + HORIZONTAL_ELLIPSIS


def test_truncate_line_already_fits():
    line = 'long'
    max_width = 4
    truncated = cu.truncate_line(line, max_width)
    assert truncated == line


def test_truncate_line_with_newline():
    line = 'fo\no'
    max_width = 2
    with pytest.raises(ValueError):
        cu.truncate_line(line, max_width)


def test_truncate_line_width_is_too_small():
    line = 'foo'
    max_width = 0
    with pytest.raises(ValueError):
        cu.truncate_line(line, max_width)


def test_truncate_line_wide_text():
    line = '苹苹other'
    max_width = 6
    truncated = cu.truncate_line(line, max_width)
    assert truncated == '苹苹o' + HORIZONTAL_ELLIPSIS


def test_truncate_line_split_wide_text():
    """Test when truncation results in a string which is shorter than max_width"""
    line = '1苹2苹'
    max_width = 3
    truncated = cu.truncate_line(line, max_width)
    assert truncated == '1' + HORIZONTAL_ELLIPSIS


def test_truncate_line_tabs():
    line = 'has\ttab'
    max_width = 9
    truncated = cu.truncate_line(line, max_width)
    assert truncated == 'has    t' + HORIZONTAL_ELLIPSIS


def test_truncate_with_style():
    from cmd2 import (
        Fg,
        TextStyle,
    )

    before_text = Fg.BLUE + TextStyle.UNDERLINE_ENABLE
    after_text = Fg.RESET + TextStyle.UNDERLINE_DISABLE + TextStyle.ITALIC_ENABLE + TextStyle.ITALIC_DISABLE

    # This is what the styles after the truncated text should look like since they will be
    # filtered by _remove_overridden_styles.
    filtered_after_text = Fg.RESET + TextStyle.UNDERLINE_DISABLE + TextStyle.ITALIC_DISABLE

    # Style only before truncated text
    line = before_text + 'long'
    max_width = 3
    truncated = cu.truncate_line(line, max_width)
    assert truncated == before_text + 'lo' + HORIZONTAL_ELLIPSIS

    # Style before and after truncated text
    line = before_text + 'long' + after_text
    max_width = 3
    truncated = cu.truncate_line(line, max_width)
    assert truncated == before_text + 'lo' + HORIZONTAL_ELLIPSIS + filtered_after_text

    # Style only after truncated text
    line = 'long' + after_text
    max_width = 3
    truncated = cu.truncate_line(line, max_width)
    assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + filtered_after_text


def test_align_text_fill_char_is_tab():
    text = 'foo'
    fill_char = '\t'
    width = 5
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
    assert aligned == text + '  '


def test_align_text_with_style():
    from cmd2 import (
        Fg,
        TextStyle,
        style,
    )

    fill_char = '-'
    styled_fill_char = style(fill_char, fg=Fg.LIGHT_YELLOW)

    # Single line with only left fill
    text = style('line1', fg=Fg.LIGHT_BLUE)
    width = 8

    aligned = cu.align_text(text, cu.TextAlignment.RIGHT, fill_char=styled_fill_char, width=width)

    left_fill = TextStyle.RESET_ALL + Fg.LIGHT_YELLOW + (fill_char * 3) + Fg.RESET + TextStyle.RESET_ALL
    right_fill = TextStyle.RESET_ALL
    line_1_text = Fg.LIGHT_BLUE + 'line1' + Fg.RESET

    assert aligned == (left_fill + line_1_text + right_fill)

    # Single line with only right fill
    text = style('line1', fg=Fg.LIGHT_BLUE)
    width = 8

    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=styled_fill_char, width=width)

    left_fill = TextStyle.RESET_ALL
    right_fill = TextStyle.RESET_ALL + Fg.LIGHT_YELLOW + (fill_char * 3) + Fg.RESET + TextStyle.RESET_ALL
    line_1_text = Fg.LIGHT_BLUE + 'line1' + Fg.RESET

    assert aligned == (left_fill + line_1_text + right_fill)

    # Multiple lines to show that style is preserved across all lines. Also has left and right fill.
    text = style('line1\nline2', fg=Fg.LIGHT_BLUE)
    width = 9

    aligned = cu.align_text(text, cu.TextAlignment.CENTER, fill_char=styled_fill_char, width=width)

    left_fill = TextStyle.RESET_ALL + Fg.LIGHT_YELLOW + (fill_char * 2) + Fg.RESET + TextStyle.RESET_ALL
    right_fill = TextStyle.RESET_ALL + Fg.LIGHT_YELLOW + (fill_char * 2) + Fg.RESET + TextStyle.RESET_ALL
    line_1_text = Fg.LIGHT_BLUE + 'line1'
    line_2_text = Fg.LIGHT_BLUE + 'line2' + Fg.RESET

    assert aligned == (left_fill + line_1_text + right_fill + '\n' + left_fill + line_2_text + right_fill)


def test_align_text_width_is_too_small():
    text = 'foo'
    fill_char = '-'
    width = 0
    with pytest.raises(ValueError):
        cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)


def test_align_text_fill_char_is_too_long():
    text = 'foo'
    fill_char = 'fill'
    width = 5
    with pytest.raises(TypeError):
        cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)


def test_align_text_fill_char_is_newline():
    text = 'foo'
    fill_char = '\n'
    width = 5
    with pytest.raises(ValueError):
        cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)


def test_align_text_has_tabs():
    text = '\t\tfoo'
    fill_char = '-'
    width = 10
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=2)
    assert aligned == '    ' + 'foo' + '---'


def test_align_text_blank():
    text = ''
    fill_char = '-'
    width = 5
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
    assert aligned == fill_char * width


def test_align_text_wider_than_width():
    text = 'long text field'
    fill_char = '-'
    width = 8
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)
    assert aligned == text


def test_align_text_wider_than_width_truncate():
    text = 'long text field'
    fill_char = '-'
    width = 8
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True)
    assert aligned == 'long te' + HORIZONTAL_ELLIPSIS


def test_align_text_wider_than_width_truncate_add_fill():
    """Test when truncation results in a string which is shorter than width and align_text adds filler"""
    text = '1苹2苹'
    fill_char = '-'
    width = 3
    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True)
    assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char


def test_align_text_has_unprintable():
    text = 'foo\x02'
    fill_char = '-'
    width = 5
    with pytest.raises(ValueError):
        cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width)


def test_align_text_term_width():
    import shutil

    text = 'foo'
    fill_char = ' '

    term_width = shutil.get_terminal_size().columns
    expected_fill = (term_width - ansi.style_aware_wcswidth(text)) * fill_char

    aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char)
    assert aligned == text + expected_fill


def test_align_left():
    text = 'foo'
    fill_char = '-'
    width = 5
    aligned = cu.align_left(text, fill_char=fill_char, width=width)
    assert aligned == text + fill_char + fill_char


def test_align_left_multiline():
    # Without style
    text = "foo\nshoes"
    fill_char = '-'
    width = 7
    aligned = cu.align_left(text, fill_char=fill_char, width=width)
    assert aligned == 'foo----\nshoes--'

    # With style
    reset_all = str(ansi.TextStyle.RESET_ALL)
    blue = str(ansi.Fg.BLUE)
    red = str(ansi.Fg.RED)
    green = str(ansi.Fg.GREEN)
    fg_reset = str(ansi.Fg.RESET)

    text = f"{blue}foo{red}moo\nshoes{fg_reset}"
    fill_char = f"{green}-{fg_reset}"
    width = 7
    aligned = cu.align_left(text, fill_char=fill_char, width=width)

    expected = f"{reset_all}{blue}foo{red}moo{reset_all}{green}-{fg_reset}{reset_all}\n"
    expected += f"{reset_all}{red}shoes{fg_reset}{reset_all}{green}--{fg_reset}{reset_all}"
    assert aligned == expected


def test_align_left_wide_text():
    text = '苹'
    fill_char = '-'
    width = 4
    aligned = cu.align_left(text, fill_char=fill_char, width=width)
    assert aligned == text + fill_char + fill_char


def test_align_left_wide_fill():
    text = 'foo'
    fill_char = '苹'
    width = 5
    aligned = cu.align_left(text, fill_char=fill_char, width=width)
    assert aligned == text + fill_char


def test_align_left_wide_fill_needs_padding():
    """Test when fill_char's display width does not divide evenly into gap"""
    text = 'foo'
    fill_char = '苹'
    width = 6
    aligned = cu.align_left(text, fill_char=fill_char, width=width)
    assert aligned == text + fill_char + ' '


def test_align_center():
    text = 'foo'
    fill_char = '-'
    width = 5
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + text + fill_char


def test_align_center_multiline():
    # Without style
    text = "foo\nshoes"
    fill_char = '-'
    width = 7
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == '--foo--\n-shoes-'

    # With style
    reset_all = str(ansi.TextStyle.RESET_ALL)
    blue = str(ansi.Fg.BLUE)
    red = str(ansi.Fg.RED)
    green = str(ansi.Fg.GREEN)
    fg_reset = str(ansi.Fg.RESET)

    text = f"{blue}foo{red}moo\nshoes{fg_reset}"
    fill_char = f"{green}-{fg_reset}"
    width = 10
    aligned = cu.align_center(text, fill_char=fill_char, width=width)

    expected = f"{reset_all}{green}--{fg_reset}{reset_all}{blue}foo{red}moo{reset_all}{green}--{fg_reset}{reset_all}\n"
    expected += f"{reset_all}{green}--{fg_reset}{reset_all}{red}shoes{fg_reset}{reset_all}{green}---{fg_reset}{reset_all}"
    assert aligned == expected


def test_align_center_wide_text():
    text = '苹'
    fill_char = '-'
    width = 4
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + text + fill_char


def test_align_center_wide_fill():
    text = 'foo'
    fill_char = '苹'
    width = 7
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + text + fill_char


def test_align_center_wide_fill_needs_right_padding():
    """Test when fill_char's display width does not divide evenly into right gap"""
    text = 'foo'
    fill_char = '苹'
    width = 8
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + text + fill_char + ' '


def test_align_center_wide_fill_needs_left_and_right_padding():
    """Test when fill_char's display width does not divide evenly into either gap"""
    text = 'foo'
    fill_char = '苹'
    width = 9
    aligned = cu.align_center(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + ' ' + text + fill_char + ' '


def test_align_right():
    text = 'foo'
    fill_char = '-'
    width = 5
    aligned = cu.align_right(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + fill_char + text


def test_align_right_multiline():
    # Without style
    text = "foo\nshoes"
    fill_char = '-'
    width = 7
    aligned = cu.align_right(text, fill_char=fill_char, width=width)
    assert aligned == '----foo\n--shoes'

    # With style
    reset_all = str(ansi.TextStyle.RESET_ALL)
    blue = str(ansi.Fg.BLUE)
    red = str(ansi.Fg.RED)
    green = str(ansi.Fg.GREEN)
    fg_reset = str(ansi.Fg.RESET)

    text = f"{blue}foo{red}moo\nshoes{fg_reset}"
    fill_char = f"{green}-{fg_reset}"
    width = 7
    aligned = cu.align_right(text, fill_char=fill_char, width=width)

    expected = f"{reset_all}{green}-{fg_reset}{reset_all}{blue}foo{red}moo{reset_all}\n"
    expected += f"{reset_all}{green}--{fg_reset}{reset_all}{red}shoes{fg_reset}{reset_all}"
    assert aligned == expected


def test_align_right_wide_text():
    text = '苹'
    fill_char = '-'
    width = 4
    aligned = cu.align_right(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + fill_char + text


def test_align_right_wide_fill():
    text = 'foo'
    fill_char = '苹'
    width = 5
    aligned = cu.align_right(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + text


def test_align_right_wide_fill_needs_padding():
    """Test when fill_char's display width does not divide evenly into gap"""
    text = 'foo'
    fill_char = '苹'
    width = 6
    aligned = cu.align_right(text, fill_char=fill_char, width=width)
    assert aligned == fill_char + ' ' + text


def test_str_to_bool_true():
    assert cu.str_to_bool('true')
    assert cu.str_to_bool('True')
    assert cu.str_to_bool('TRUE')
    assert cu.str_to_bool('tRuE')


def test_str_to_bool_false():
    assert not cu.str_to_bool('false')
    assert not cu.str_to_bool('False')
    assert not cu.str_to_bool('FALSE')
    assert not cu.str_to_bool('fAlSe')


def test_str_to_bool_invalid():
    with pytest.raises(ValueError):
        cu.str_to_bool('other')


def test_str_to_bool_bad_input():
    with pytest.raises(ValueError):
        cu.str_to_bool(1)


def test_find_editor_specified():
    expected_editor = os.path.join('fake_dir', 'editor')
    with mock.patch.dict(os.environ, {'EDITOR': expected_editor}):
        editor = cu.find_editor()
    assert editor == expected_editor


def test_find_editor_not_specified():
    # Use existing path env setting. Something in the editor list should be found.
    editor = cu.find_editor()
    assert editor

    # Overwrite path env setting with invalid path, clear all other env vars so no editor should be found.
    with mock.patch.dict(os.environ, {'PATH': 'fake_dir'}, clear=True):
        editor = cu.find_editor()
    assert editor is None
