import argparse
import locale
import os
from pathlib import Path
import shlex
import subprocess
import sys

from _pytest.config import argparsing as parseopt
from _pytest.config.exceptions import UsageError
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
import pytest


@pytest.fixture
def parser() -> parseopt.Parser:
    return parseopt.Parser(_ispytest=True)


class TestParser:
    def test_no_help_by_default(self) -> None:
        parser = parseopt.Parser(usage="xyz", _ispytest=True)
        pytest.raises(UsageError, lambda: parser.parse(["-h"]))

    def test_custom_prog(self, parser: parseopt.Parser) -> None:
        """Custom prog can be set for `argparse.ArgumentParser`."""
        assert parser._getparser().prog == os.path.basename(sys.argv[0])
        parser.prog = "custom-prog"
        assert parser._getparser().prog == "custom-prog"

    def test_argument(self) -> None:
        with pytest.raises(parseopt.ArgumentError):
            # need a short or long option
            argument = parseopt.Argument()
        argument = parseopt.Argument("-t")
        assert argument._short_opts == ["-t"]
        assert argument._long_opts == []
        assert argument.dest == "t"
        argument = parseopt.Argument("-t", "--test")
        assert argument._short_opts == ["-t"]
        assert argument._long_opts == ["--test"]
        assert argument.dest == "test"
        argument = parseopt.Argument("-t", "--test", dest="abc")
        assert argument.dest == "abc"
        assert str(argument) == (
            "Argument(_short_opts: ['-t'], _long_opts: ['--test'], dest: 'abc')"
        )

    def test_argument_type(self) -> None:
        argument = parseopt.Argument("-t", dest="abc", type=int)
        assert argument.type is int
        argument = parseopt.Argument("-t", dest="abc", type=str)
        assert argument.type is str
        argument = parseopt.Argument("-t", dest="abc", type=float)
        assert argument.type is float
        with pytest.warns(DeprecationWarning):
            with pytest.raises(KeyError):
                argument = parseopt.Argument("-t", dest="abc", type="choice")
        argument = parseopt.Argument(
            "-t", dest="abc", type=str, choices=["red", "blue"]
        )
        assert argument.type is str

    def test_argument_processopt(self) -> None:
        argument = parseopt.Argument("-t", type=int)
        argument.default = 42
        argument.dest = "abc"
        res = argument.attrs()
        assert res["default"] == 42
        assert res["dest"] == "abc"

    def test_group_add_and_get(self, parser: parseopt.Parser) -> None:
        group = parser.getgroup("hello", description="desc")
        assert group.name == "hello"
        assert group.description == "desc"

    def test_getgroup_simple(self, parser: parseopt.Parser) -> None:
        group = parser.getgroup("hello", description="desc")
        assert group.name == "hello"
        assert group.description == "desc"
        group2 = parser.getgroup("hello")
        assert group2 is group

    def test_group_ordering(self, parser: parseopt.Parser) -> None:
        parser.getgroup("1")
        parser.getgroup("2")
        parser.getgroup("3", after="1")
        groups = parser._groups
        groups_names = [x.name for x in groups]
        assert groups_names == list("132")

    def test_group_addoption(self) -> None:
        group = parseopt.OptionGroup("hello", _ispytest=True)
        group.addoption("--option1", action="store_true")
        assert len(group.options) == 1
        assert isinstance(group.options[0], parseopt.Argument)

    def test_group_addoption_conflict(self) -> None:
        group = parseopt.OptionGroup("hello again", _ispytest=True)
        group.addoption("--option1", "--option-1", action="store_true")
        with pytest.raises(ValueError) as err:
            group.addoption("--option1", "--option-one", action="store_true")
        assert str({"--option1"}) in str(err.value)

    def test_group_shortopt_lowercase(self, parser: parseopt.Parser) -> None:
        group = parser.getgroup("hello")
        with pytest.raises(ValueError):
            group.addoption("-x", action="store_true")
        assert len(group.options) == 0
        group._addoption("-x", action="store_true")
        assert len(group.options) == 1

    def test_parser_addoption(self, parser: parseopt.Parser) -> None:
        group = parser.getgroup("custom options")
        assert len(group.options) == 0
        group.addoption("--option1", action="store_true")
        assert len(group.options) == 1

    def test_parse(self, parser: parseopt.Parser) -> None:
        parser.addoption("--hello", dest="hello", action="store")
        args = parser.parse(["--hello", "world"])
        assert args.hello == "world"
        assert not getattr(args, parseopt.FILE_OR_DIR)

    def test_parse2(self, parser: parseopt.Parser) -> None:
        args = parser.parse([Path(".")])
        assert getattr(args, parseopt.FILE_OR_DIR)[0] == "."

    def test_parse_known_args(self, parser: parseopt.Parser) -> None:
        parser.parse_known_args([Path(".")])
        parser.addoption("--hello", action="store_true")
        ns = parser.parse_known_args(["x", "--y", "--hello", "this"])
        assert ns.hello
        assert ns.file_or_dir == ["x"]

    def test_parse_known_and_unknown_args(self, parser: parseopt.Parser) -> None:
        parser.addoption("--hello", action="store_true")
        ns, unknown = parser.parse_known_and_unknown_args(
            ["x", "--y", "--hello", "this"]
        )
        assert ns.hello
        assert ns.file_or_dir == ["x"]
        assert unknown == ["--y", "this"]

    def test_parse_will_set_default(self, parser: parseopt.Parser) -> None:
        parser.addoption("--hello", dest="hello", default="x", action="store")
        option = parser.parse([])
        assert option.hello == "x"
        del option.hello
        parser.parse_setoption([], option)
        assert option.hello == "x"

    def test_parse_setoption(self, parser: parseopt.Parser) -> None:
        parser.addoption("--hello", dest="hello", action="store")
        parser.addoption("--world", dest="world", default=42)

        option = argparse.Namespace()
        args = parser.parse_setoption(["--hello", "world"], option)
        assert option.hello == "world"
        assert option.world == 42
        assert not args

    def test_parse_special_destination(self, parser: parseopt.Parser) -> None:
        parser.addoption("--ultimate-answer", type=int)
        args = parser.parse(["--ultimate-answer", "42"])
        assert args.ultimate_answer == 42

    def test_parse_split_positional_arguments(self, parser: parseopt.Parser) -> None:
        parser.addoption("-R", action="store_true")
        parser.addoption("-S", action="store_false")
        args = parser.parse(["-R", "4", "2", "-S"])
        assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
        args = parser.parse(["-R", "-S", "4", "2", "-R"])
        assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
        assert args.R is True
        assert args.S is False
        args = parser.parse(["-R", "4", "-S", "2"])
        assert getattr(args, parseopt.FILE_OR_DIR) == ["4", "2"]
        assert args.R is True
        assert args.S is False

    def test_parse_defaultgetter(self) -> None:
        def defaultget(option):
            if not hasattr(option, "type"):
                return
            if option.type is int:
                option.default = 42
            elif option.type is str:
                option.default = "world"

        parser = parseopt.Parser(processopt=defaultget, _ispytest=True)
        parser.addoption("--this", dest="this", type=int, action="store")
        parser.addoption("--hello", dest="hello", type=str, action="store")
        parser.addoption("--no", dest="no", action="store_true")
        option = parser.parse([])
        assert option.hello == "world"
        assert option.this == 42
        assert option.no is False

    def test_drop_short_helper(self) -> None:
        parser = argparse.ArgumentParser(
            formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False
        )
        parser.add_argument(
            "-t", "--twoword", "--duo", "--two-word", "--two", help="foo"
        )
        # throws error on --deux only!
        parser.add_argument(
            "-d", "--deuxmots", "--deux-mots", action="store_true", help="foo"
        )
        parser.add_argument("-s", action="store_true", help="single short")
        parser.add_argument("--abc", "-a", action="store_true", help="bar")
        parser.add_argument("--klm", "-k", "--kl-m", action="store_true", help="bar")
        parser.add_argument(
            "-P", "--pq-r", "-p", "--pqr", action="store_true", help="bar"
        )
        parser.add_argument(
            "--zwei-wort", "--zweiwort", "--zweiwort", action="store_true", help="bar"
        )
        parser.add_argument(
            "-x", "--exit-on-first", "--exitfirst", action="store_true", help="spam"
        )
        parser.add_argument("files_and_dirs", nargs="*")
        args = parser.parse_args(["-k", "--duo", "hallo", "--exitfirst"])
        assert args.twoword == "hallo"
        assert args.klm is True
        assert args.zwei_wort is False
        assert args.exit_on_first is True
        assert args.s is False
        args = parser.parse_args(["--deux-mots"])
        with pytest.raises(AttributeError):
            assert args.deux_mots is True
        assert args.deuxmots is True
        args = parser.parse_args(["file", "dir"])
        assert "|".join(args.files_and_dirs) == "file|dir"

    def test_drop_short_0(self, parser: parseopt.Parser) -> None:
        parser.addoption("--funcarg", "--func-arg", action="store_true")
        parser.addoption("--abc-def", "--abc-def", action="store_true")
        parser.addoption("--klm-hij", action="store_true")
        with pytest.raises(UsageError):
            parser.parse(["--funcarg", "--k"])

    def test_drop_short_2(self, parser: parseopt.Parser) -> None:
        parser.addoption("--func-arg", "--doit", action="store_true")
        args = parser.parse(["--doit"])
        assert args.func_arg is True

    def test_drop_short_3(self, parser: parseopt.Parser) -> None:
        parser.addoption("--func-arg", "--funcarg", "--doit", action="store_true")
        args = parser.parse(["abcd"])
        assert args.func_arg is False
        assert args.file_or_dir == ["abcd"]

    def test_drop_short_help0(self, parser: parseopt.Parser) -> None:
        parser.addoption("--func-args", "--doit", help="foo", action="store_true")
        parser.parse([])
        help = parser.optparser.format_help()
        assert "--func-args, --doit  foo" in help

    # testing would be more helpful with all help generated
    def test_drop_short_help1(self, parser: parseopt.Parser) -> None:
        group = parser.getgroup("general")
        group.addoption("--doit", "--func-args", action="store_true", help="foo")
        group._addoption(
            "-h",
            "--help",
            action="store_true",
            dest="help",
            help="show help message and configuration info",
        )
        parser.parse(["-h"])
        help = parser.optparser.format_help()
        assert "-doit, --func-args  foo" in help

    def test_multiple_metavar_help(self, parser: parseopt.Parser) -> None:
        """
        Help text for options with a metavar tuple should display help
        in the form "--preferences=value1 value2 value3" (#2004).
        """
        group = parser.getgroup("general")
        group.addoption(
            "--preferences", metavar=("value1", "value2", "value3"), nargs=3
        )
        group._addoption("-h", "--help", action="store_true", dest="help")
        parser.parse(["-h"])
        help = parser.optparser.format_help()
        assert "--preferences=value1 value2 value3" in help


def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
    if sys.version_info >= (3, 11):
        # New in Python 3.11, ignores utf-8 mode
        encoding = locale.getencoding()
    else:
        encoding = locale.getpreferredencoding(False)
    try:
        bash_version = subprocess.run(
            ["bash", "--version"],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            check=True,
            text=True,
            encoding=encoding,
        ).stdout
    except (OSError, subprocess.CalledProcessError):
        pytest.skip("bash is not available")
    if "GNU bash" not in bash_version:
        # See #7518.
        pytest.skip("not a real bash")

    script = str(pytester.path.joinpath("test_argcomplete"))

    with open(str(script), "w", encoding="utf-8") as fp:
        # redirect output from argcomplete to stdin and stderr is not trivial
        # http://stackoverflow.com/q/12589419/1307905
        # so we use bash
        fp.write(
            f'COMP_WORDBREAKS="$COMP_WORDBREAKS" {shlex.quote(sys.executable)} -m pytest 8>&1 9>&2'
        )
    # alternative would be extended Pytester.{run(),_run(),popen()} to be able
    # to handle a keyword argument env that replaces os.environ in popen or
    # extends the copy, advantage: could not forget to restore
    monkeypatch.setenv("_ARGCOMPLETE", "1")
    monkeypatch.setenv("_ARGCOMPLETE_IFS", "\x0b")
    monkeypatch.setenv("COMP_WORDBREAKS", " \\t\\n\"\\'><=;|&(:")

    arg = "--fu"
    monkeypatch.setenv("COMP_LINE", "pytest " + arg)
    monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg)))
    result = pytester.run("bash", str(script), arg)
    if result.ret == 255:
        # argcomplete not found
        pytest.skip("argcomplete not available")
    elif not result.stdout.str():
        pytest.skip(
            f"bash provided no output on stdout, argcomplete not available? (stderr={result.stderr.str()!r})"
        )
    else:
        result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"])
    os.mkdir("test_argcomplete.d")
    arg = "test_argc"
    monkeypatch.setenv("COMP_LINE", "pytest " + arg)
    monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg)))
    result = pytester.run("bash", str(script), arg)
    result.stdout.fnmatch_lines(["test_argcomplete", "test_argcomplete.d/"])
