"""
    Command line test
    ~~~~~~~~~~~~~~~~~

    :copyright: Copyright 2006-2023 by the Pygments team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import io
import os
import re
import sys
import tempfile
from io import BytesIO
from os import path

import pytest
from pytest import raises

from pygments import cmdline, highlight

TESTDIR = path.dirname(path.abspath(__file__))
TESTFILE = path.join(TESTDIR, 'test_cmdline.py')

TESTCODE = '''\
def func(args):
    pass
'''


def _decode_output(text):
    try:
        return text.decode('utf-8')
    except UnicodeEncodeError:  # implicit encode on Python 2 with data loss
        return text


def run_cmdline(*args, **kwds):
    saved_stdin = sys.stdin
    saved_stdout = sys.stdout
    saved_stderr = sys.stderr
    stdin_buffer = BytesIO()
    stdout_buffer = BytesIO()
    stderr_buffer = BytesIO()
    new_stdin = sys.stdin = io.TextIOWrapper(stdin_buffer, 'utf-8')
    new_stdout = sys.stdout = io.TextIOWrapper(stdout_buffer, 'utf-8')
    new_stderr = sys.stderr = io.TextIOWrapper(stderr_buffer, 'utf-8')
    new_stdin.write(kwds.get('stdin', ''))
    new_stdin.seek(0, 0)
    try:
        ret = cmdline.main(['pygmentize'] + list(args))
    finally:
        sys.stdin = saved_stdin
        sys.stdout = saved_stdout
        sys.stderr = saved_stderr
    new_stdout.flush()
    new_stderr.flush()
    out, err = stdout_buffer.getvalue(), \
        stderr_buffer.getvalue()
    return (ret, _decode_output(out), _decode_output(err))


def check_success(*cmdline, **kwds):
    code, out, err = run_cmdline(*cmdline, **kwds)
    assert code == 0
    assert err == ''
    return out


def check_failure(*cmdline, **kwds):
    expected_code = kwds.pop('code', 1)
    try:
        code, out, err = run_cmdline(*cmdline, **kwds)
    except SystemExit as err:
        assert err.args[0] == expected_code
    else:
        assert code == expected_code
        assert out == ''
        return err


def test_normal():
    # test that cmdline gives the same output as library api
    from pygments.lexers import PythonLexer
    from pygments.formatters import HtmlFormatter
    filename = TESTFILE
    with open(filename, 'rb') as fp:
        code = fp.read()

    output = highlight(code, PythonLexer(), HtmlFormatter())

    o = check_success('-lpython', '-fhtml', filename)
    assert o == output


def test_stdin():
    o = check_success('-lpython', '-fhtml', stdin=TESTCODE)
    o = re.sub('<[^>]*>', '', o)
    # rstrip is necessary since HTML inserts a \n after the last </div>
    assert o.rstrip() == TESTCODE.rstrip()

    # guess if no lexer given
    o = check_success('-fhtml', stdin=TESTCODE)
    o = re.sub('<[^>]*>', '', o)
    # rstrip is necessary since HTML inserts a \n after the last </div>
    assert o.rstrip() == TESTCODE.rstrip()


def test_outfile():
    # test that output file works with and without encoding
    fd, name = tempfile.mkstemp()
    os.close(fd)
    for opts in [['-fhtml', '-o', name, TESTFILE],
                 ['-flatex', '-o', name, TESTFILE],
                 ['-fhtml', '-o', name, '-O', 'encoding=utf-8', TESTFILE]]:
        try:
            check_success(*opts)
        finally:
            os.unlink(name)


def test_load_from_file():
    lexer_file = os.path.join(TESTDIR, 'support', 'python_lexer.py')
    formatter_file = os.path.join(TESTDIR, 'support', 'html_formatter.py')

    # By default, use CustomLexer
    o = check_success('-l', lexer_file, '-f', 'html', '-x', stdin=TESTCODE)
    o = re.sub('<[^>]*>', '', o)
    # rstrip is necessary since HTML inserts a \n after the last </div>
    assert o.rstrip() == TESTCODE.rstrip()

    # If user specifies a name, use it
    o = check_success('-f', 'html', '-x', '-l',
                      lexer_file + ':LexerWrapper', stdin=TESTCODE)
    o = re.sub('<[^>]*>', '', o)
    # rstrip is necessary since HTML inserts a \n after the last </div>
    assert o.rstrip() == TESTCODE.rstrip()

    # Should also work for formatters
    o = check_success('-lpython', '-f',
                      formatter_file + ':HtmlFormatterWrapper',
                      '-x', stdin=TESTCODE)
    o = re.sub('<[^>]*>', '', o)
    # rstrip is necessary since HTML inserts a \n after the last </div>
    assert o.rstrip() == TESTCODE.rstrip()


def test_stream_opt():
    o = check_success('-lpython', '-s', '-fterminal', stdin=TESTCODE)
    o = re.sub(r'\x1b\[.*?m', '', o)
    assert o.replace('\r\n', '\n') == TESTCODE


def test_h_opt():
    o = check_success('-h')
    assert 'usage:' in o


def test_L_opt():
    o = check_success('-L')
    assert 'Lexers' in o and 'Formatters' in o and 'Filters' in o and 'Styles' in o
    o = check_success('-L', 'lexer')
    assert 'Lexers' in o and 'Formatters' not in o
    check_success('-L', 'lexers')


def test_O_opt():
    filename = TESTFILE
    o = check_success('-Ofull=1,linenos=true,foo=bar', '-fhtml', filename)
    assert '<html' in o
    assert 'class="linenos"' in o

    # "foobar" is invalid for a bool option
    e = check_failure('-Ostripnl=foobar', TESTFILE)
    assert 'Error: Invalid value' in e
    e = check_failure('-Ostripnl=foobar', '-lpy')
    assert 'Error: Invalid value' in e


def test_P_opt():
    filename = TESTFILE
    o = check_success('-Pfull', '-Ptitle=foo, bar=baz=,', '-fhtml', filename)
    assert '<title>foo, bar=baz=,</title>' in o


def test_F_opt():
    filename = TESTFILE
    o = check_success('-Fhighlight:tokentype=Name.Blubb,'
                      'names=TESTFILE filename', '-fhtml', filename)
    assert '<span class="n n-Blubb' in o


def test_H_opt():
    o = check_success('-H', 'formatter', 'html')
    assert 'HTML' in o
    o = check_success('-H', 'lexer', 'python')
    assert 'Python' in o
    o = check_success('-H', 'filter', 'raiseonerror')
    assert 'raiseonerror' in o
    e = check_failure('-H', 'lexer', 'foobar')
    assert 'not found' in e


def test_S_opt():
    o = check_success('-S', 'default', '-f', 'html', '-O', 'linenos=1')
    lines = o.splitlines()
    for line in lines[5:]:
        # every line is for a token class, except for the first 5 lines,
        # which define styles for `pre` and line numbers
        parts = line.split()
        assert parts[0].startswith('.')
        assert parts[1] == '{'
        if parts[0] != '.hll':
            assert parts[-4] == '}'
            assert parts[-3] == '/*'
            assert parts[-1] == '*/'
    check_failure('-S', 'default', '-f', 'foobar')


def test_N_opt():
    o = check_success('-N', 'test.py')
    assert 'python' == o.strip()
    o = check_success('-N', 'test.unknown')
    assert 'text' == o.strip()


def test_C_opt():
    o = check_success('-C', stdin='#!python3\n')
    assert 'python' == o.strip()
    o = check_success('-C', stdin='x')
    assert 'text' == o.strip()


@pytest.mark.parametrize('opts', [
    ('-X',),
    ('-L', '-lpy'),
    ('-L', '-fhtml'),
    ('-L', '-Ox'),
    ('-S', 'default', '-l', 'py', '-f', 'html'),
    ('-S', 'default'),
    ('-a', 'arg'),
    ('-H',),
    (TESTFILE, TESTFILE),
    ('-H', 'formatter'),
    ('-H', 'foo', 'bar'),
    ('-s',),
    ('-s', TESTFILE),
])
def test_invalid_opts(opts):
    check_failure(*opts, code=2)


def test_errors():
    # input file not found
    e = check_failure('-lpython', 'nonexistent.py')
    assert 'Error: cannot read infile' in e
    assert 'nonexistent.py' in e

    # lexer not found
    e = check_failure('-lfooo', TESTFILE)
    assert 'Error: no lexer for alias' in e

    # cannot load .py file without load_from_file flag
    e = check_failure('-l', 'nonexistent.py', TESTFILE)
    assert 'Error: no lexer for alias' in e

    # lexer file is missing/unreadable
    e = check_failure('-l', 'nonexistent.py', '-x', TESTFILE)
    assert 'Error: cannot read' in e

    # lexer file is malformed
    e = check_failure('-l', path.join(TESTDIR, 'support', 'empty.py'),
                      '-x', TESTFILE)
    assert 'Error: no valid CustomLexer class found' in e

    # formatter not found
    e = check_failure('-lpython', '-ffoo', TESTFILE)
    assert 'Error: no formatter found for name' in e

    # formatter for outfile not found
    e = check_failure('-ofoo.foo', TESTFILE)
    assert 'Error: no formatter found for file name' in e

    # cannot load .py file without load_from_file flag
    e = check_failure('-f', 'nonexistent.py', TESTFILE)
    assert 'Error: no formatter found for name' in e

    # formatter file is missing/unreadable
    e = check_failure('-f', 'nonexistent.py', '-x', TESTFILE)
    assert 'Error: cannot read' in e

    # formatter file is malformed
    e = check_failure('-f', path.join(TESTDIR, 'support', 'empty.py'),
                      '-x', TESTFILE)
    assert 'Error: no valid CustomFormatter class found' in e

    # output file not writable
    e = check_failure('-o', os.path.join('nonexistent', 'dir', 'out.html'),
                      '-lpython', TESTFILE)
    assert 'Error: cannot open outfile' in e
    assert 'out.html' in e

    # unknown filter
    e = check_failure('-F', 'foo', TESTFILE)
    assert 'Error: filter \'foo\' not found' in e


def test_exception():
    cmdline.highlight = None  # override callable to provoke TypeError
    try:
        # unexpected exception while highlighting
        e = check_failure('-lpython', TESTFILE)
        assert '*** Error while highlighting:' in e
        assert 'TypeError' in e

        # same with -v: should reraise the exception
        assert raises(Exception, check_failure, '-lpython', '-v', TESTFILE)
    finally:
        cmdline.highlight = highlight


def test_parse_opts():
    assert cmdline._parse_options(['  ', 'keyonly,key = value ']) == \
        {'keyonly': True, 'key': 'value'}
