import contextlib
import dataclasses
import enum
import io
import json
import os
import pathlib
import sys
from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar, Union, cast

import pytest
from helptext_utils import get_helptext_with_checks
from typing_extensions import Annotated, Literal, NotRequired, TypedDict

import tyro
import tyro._strings


def test_helptext() -> None:
    @dataclasses.dataclass
    class Helptext:
        """This docstring should be printed as a description."""

        x: int  # Documentation 1

        # Documentation 2
        y: Annotated[int, "ignored"]

        z: int = 3
        """Documentation 3"""

    helptext = get_helptext_with_checks(Helptext)
    assert cast(str, helptext) in helptext
    assert "x INT" in helptext
    assert "y INT" in helptext
    assert "z INT" in helptext
    assert "Documentation 1 (required)" in helptext
    assert "Documentation 2 (required)" in helptext
    assert "Documentation 3 (default: 3)" in helptext


def test_helptext_sphinx_autodoc_style() -> None:
    @dataclasses.dataclass
    class Helptext:
        """This docstring should be printed as a description."""

        x: int  #: Documentation 1

        #:Documentation 2
        y: Annotated[int, "ignored"]
        z: int = 3

    helptext = get_helptext_with_checks(Helptext)
    assert cast(str, helptext) in helptext
    assert "x INT" in helptext
    assert "y INT" in helptext
    assert "z INT" in helptext
    assert "Documentation 1 (required)" in helptext
    assert ": Documentation 1" not in helptext
    assert "Documentation 2 (required)" in helptext
    assert ":Documentation 2" not in helptext

    # :Documentation 2 should not be applied to `z`.
    assert helptext.count("Documentation 2") == 1


def test_helptext_from_class_docstring() -> None:
    @dataclasses.dataclass
    class Helptext2:
        """This docstring should be printed as a description.

        Attributes:
            x: Documentation 1
            y: Documentation 2
            z: Documentation 3
        """

        x: int
        y: Annotated[int, "ignored"]
        z: int = 3

    helptext = get_helptext_with_checks(Helptext2)
    assert "This docstring should be printed as a description" in helptext
    assert "Attributes" not in helptext
    assert "x INT" in helptext
    assert "y INT" in helptext
    assert "z INT" in helptext
    assert "Documentation 1 (required)" in helptext
    assert "Documentation 2 (required)" in helptext
    assert "Documentation 3 (default: 3)" in helptext


def test_helptext_from_class_docstring_args() -> None:
    @dataclasses.dataclass
    class Helptext3:
        """This docstring should be printed as a description.

        Args:
            x: Documentation 1
            y: Documentation 2
            z: Documentation 3
        """

        x: int
        y: Annotated[int, "ignored"]
        z: int = 3

    helptext = get_helptext_with_checks(Helptext3)
    assert "This docstring should be printed as a description" in helptext
    assert "Args" not in helptext
    assert "x INT" in helptext
    assert "y INT" in helptext
    assert "z INT" in helptext
    assert "Documentation 1 (required)" in helptext
    assert "Documentation 2 (required)" in helptext
    assert "Documentation 3 (default: 3)" in helptext


def test_helptext_inherited() -> None:
    class UnrelatedParentClass:
        pass

    @dataclasses.dataclass
    class ActualParentClass:
        """This docstring should be printed as a description."""

        x: int  # Documentation 1

        # Documentation 2
        y: int

        # fmt: off

        z: int = 3
        def some_method(self) -> None:  # noqa
            """Coverage stress test."""
        # fmt: on

    @dataclasses.dataclass
    class ChildClass(UnrelatedParentClass, ActualParentClass):
        pass

    helptext = get_helptext_with_checks(ChildClass)
    assert "Documentation 1" in helptext
    assert "Documentation 2" in helptext


def test_helptext_inherited_default_override() -> None:
    @dataclasses.dataclass
    class ParentClass:
        """This docstring should __not__ be printed as a description."""

        x: int  # Documentation 1

        # Documentation 2
        y: int

        # fmt: off

        z: int = 3
        def some_method(self) -> None:  # noqa
            """Coverage stress test."""
        # fmt: on

    @dataclasses.dataclass
    class ChildClass(ParentClass):
        """This docstring should be printed as a description."""

    def main(x: ParentClass = ChildClass(x=5, y=5)) -> Any:
        """Main function."""
        return x

    helptext = get_helptext_with_checks(main)
    assert "Main function." in helptext
    assert "Documentation 1" in helptext
    assert "Documentation 2" in helptext
    assert "__not__" not in helptext
    assert helptext.count("should be printed") == 1


def test_helptext_nested() -> None:
    """For nested classes, we should pull helptext from the outermost docstring if
    possible. The class docstring can be used as a fallback."""

    class Inner:
        def __init__(self, a: int):
            """Something

            Args:
                a (int): Hello world!
            """

    def main_with_docstring(a: Inner) -> None:
        """main_with_docstring.

        Args:
            a: Documented in function.
        """

    def main_no_docstring(a: Inner) -> None:
        """main_no_docstring."""

    helptext = get_helptext_with_checks(main_with_docstring)
    assert "Documented in function" in helptext and str(Inner.__doc__) not in helptext
    assert "main_with_docstring." in helptext
    assert "Args:" not in helptext
    assert "Args:" not in helptext
    assert "Hello world!" in helptext

    helptext = get_helptext_with_checks(main_no_docstring)
    assert "Something" in helptext
    assert "main_no_docstring." in helptext
    assert "Args:" not in helptext
    assert "Hello world!" in helptext


@pytest.mark.skipif(sys.platform == "win32", reason="Path separators differ on Windows")
def test_helptext_defaults() -> None:
    class Color(enum.Enum):
        RED = enum.auto()
        GREEN = enum.auto()
        BLUE = enum.auto()

    @dataclasses.dataclass
    class HelptextWithVariousDefaults:
        x: pathlib.Path = pathlib.Path("/some/path/to/a/file")
        y: Color = Color.RED
        z: str = "%"

    helptext = get_helptext_with_checks(HelptextWithVariousDefaults)
    assert "show this help message and exit" in helptext
    assert "--x PATH" in helptext
    assert "(default: /some/path/to/a/file)" in helptext
    assert "--y {RED,GREEN,BLUE}" in helptext
    assert "(default: RED)" in helptext
    assert "--z STR" in helptext
    assert "(default: %)" in helptext


def test_helptext_upath_defaults() -> None:
    """Test UPath helptext with metavar and default rendering."""
    from upath import UPath

    @dataclasses.dataclass
    class HelptextWithUPath:
        x: UPath = UPath("s3://bucket/path")

    helptext = get_helptext_with_checks(HelptextWithUPath)
    assert "--x UPATH" in helptext
    assert "(default: s3://bucket/path)" in helptext


def test_multiline_helptext() -> None:
    @dataclasses.dataclass
    class HelptextMultiline:
        x: int  # Documentation 1

        # This comment should be ignored!

        # Documentation 2
        # Next line of documentation 2
        y: int

        z: int = 3
        """Documentation 3
        Next line of documentation 3"""

    helptext = get_helptext_with_checks(HelptextMultiline)
    assert "Documentation 1 (required)" in helptext
    assert "Documentation 2" in helptext
    assert "documentation 2" in helptext
    assert "Documentation 3" in helptext
    assert "documentation 3" in helptext


def test_grouped_helptext() -> None:
    @dataclasses.dataclass
    class HelptextGrouped:
        x: int  # Documentation 1
        # Description of both y and z.
        y: int
        z: int = 3

    helptext = get_helptext_with_checks(HelptextGrouped)
    assert "Documentation 1 (required)" in helptext
    assert "Description of both y and z. (required)" in helptext
    assert "Description of both y and z. (default: 3)" in helptext


def test_none_default_value_helptext() -> None:
    @dataclasses.dataclass
    class Config:
        x: Optional[int] = None
        """An optional variable."""

    helptext = get_helptext_with_checks(Config)
    assert "--x {None}|INT" in helptext
    assert "An optional variable. (default: None)" in helptext


def test_helptext_hard_bool() -> None:
    @dataclasses.dataclass
    class HelptextHardString:
        # fmt: off
        x: bool = (
            False
        )
        """Helptext. 2% milk."""
        # fmt: on

    # The percent symbol needs some extra handling in argparse.
    # https://stackoverflow.com/questions/21168120/python-argparse-errors-with-in-help-string

    helptext = get_helptext_with_checks(HelptextHardString)
    assert "--x" in helptext
    assert "2% milk." in helptext


def test_helptext_with_inheritance() -> None:
    @dataclasses.dataclass
    class Parent:
        # fmt: off
        x: str = (
            "This docstring may be tougher to parse!"
        )
        """Helptext."""
        # fmt: on

    @dataclasses.dataclass
    class Child(Parent):
        pass

    helptext = get_helptext_with_checks(Child)
    assert "--x STR" in helptext
    assert "Helptext." in helptext
    assert "(default: 'This docstring" in helptext


def test_helptext_with_inheritance_overriden() -> None:
    @dataclasses.dataclass
    class Parent2:
        # fmt: off
        x: str = (
            "This docstring may be tougher to parse!"
        )
        """Helptext."""
        # fmt: on

    @dataclasses.dataclass
    class Child2(Parent2):
        # fmt: off
        x: str = (
            "This docstring may be tougher to parse?"
        )
        """Helptext!"""
        # fmt: on

    helptext = get_helptext_with_checks(Child2)
    assert "--x STR" in helptext
    assert "Helptext! (default: 'This" in helptext


def test_tuple_helptext() -> None:
    @dataclasses.dataclass
    class TupleHelptext:
        x: Tuple[int, str, float]

    helptext = get_helptext_with_checks(TupleHelptext)
    assert "--x INT STR FLOAT" in helptext


def test_tuple_helptext_defaults() -> None:
    @dataclasses.dataclass
    class TupleHelptextDefaults:
        x: Tuple[int, str, str] = (5, "hello world", "hello")

    helptext = get_helptext_with_checks(TupleHelptextDefaults)
    assert "--x INT STR STR" in helptext
    assert "(default: 5 'hello world' hello)" in helptext


def test_generic_helptext() -> None:
    T = TypeVar("T")

    @dataclasses.dataclass
    class GenericTupleHelptext(Generic[T]):
        x: T

    helptext = get_helptext_with_checks(GenericTupleHelptext[int])
    assert "--x INT" in helptext


def test_generic_tuple_helptext() -> None:
    T = TypeVar("T")

    @dataclasses.dataclass
    class GenericTupleHelptext(Generic[T]):
        x: Tuple[T, T, T]

    helptext = get_helptext_with_checks(GenericTupleHelptext[int])
    assert "--x INT INT INT" in helptext


def test_generic_list_helptext() -> None:
    T = TypeVar("T")

    @dataclasses.dataclass
    class GenericTupleHelptext(Generic[T]):
        x: List[T]

    helptext = get_helptext_with_checks(GenericTupleHelptext[int])
    assert "--x [INT [INT ...]]" in helptext


def test_literal_helptext() -> None:
    @dataclasses.dataclass
    class LiteralHelptext:
        x: Literal[1, 2, 3]
        """A number."""

    helptext = get_helptext_with_checks(LiteralHelptext)
    assert "--x {1,2,3}" in helptext
    assert "A number. (required)" in helptext


def test_optional_literal_helptext() -> None:
    @dataclasses.dataclass
    class OptionalLiteralHelptext:
        x: Optional[Literal[1, 2, 3]] = None
        """A number."""

    helptext = get_helptext_with_checks(OptionalLiteralHelptext)
    assert "--x {None,1,2,3}" in helptext
    assert "A number. (default: None)" in helptext


def test_multiple_subparsers_helptext() -> None:
    @dataclasses.dataclass
    class Subcommand1:
        """2% milk."""  # % symbol is prone to bugs in argparse.

        x: int = 0

    @dataclasses.dataclass
    class Subcommand2:
        y: int = 1

    @dataclasses.dataclass
    class Subcommand3:
        z: int = 2

    @dataclasses.dataclass
    class MultipleSubparsers:
        # Field a description.
        a: Union[Subcommand1, Subcommand2, Subcommand3]
        # Field b description.
        b: Union[Subcommand1, Subcommand2, Subcommand3]
        # Field c description.
        c: Union[Subcommand1, Subcommand2, Subcommand3] = dataclasses.field(
            default_factory=Subcommand3
        )

        d: bool = False

    helptext = get_helptext_with_checks(MultipleSubparsers)

    assert "2% milk." in helptext
    assert "Field a description." in helptext
    # With tyro backend, all subparser groups in the frontier are shown.
    # With argparse backend, only the first subparser group is shown.

    if tyro._experimental_options["backend"] == "tyro":
        assert "Field b description." in helptext
        assert "Field c description." in helptext
    else:
        assert "Field b description." not in helptext
        assert "Field c description." not in helptext

    # Not enough args for usage shortening to kick in.
    assert "[OPTIONS]" not in helptext
    assert "[B:SUBCOMMAND2 OPTIONS]" not in helptext

    helptext = get_helptext_with_checks(
        MultipleSubparsers,
        args=["a:subcommand1", "b:subcommand1", "--help"],
    )

    assert "2% milk." in helptext
    assert "Field a description." not in helptext
    assert "Field b description." not in helptext
    assert "Field c description." in helptext
    assert "--no-d" not in helptext
    assert "(default: c:subcommand3)" in helptext

    # Not enough args for usage shortening to kick in.
    assert "[OPTIONS]" not in helptext
    assert "[B:SUBCOMMAND1 OPTIONS]" not in helptext
    assert "[B:SUBCOMMAND2 OPTIONS]" not in helptext

    # Argument should be pushed to the leaf.
    assert "--d, --no-d" in get_helptext_with_checks(
        MultipleSubparsers,
        args=["a:subcommand1", "b:subcommand1", "c:subcommand2", "--help"],
        config=(tyro.conf.CascadeSubcommandArgs,),
    )


def test_multiple_subparsers_helptext_shortened_usage() -> None:
    @dataclasses.dataclass
    class Subcommand1:
        """2% milk."""  # % symbol is prone to bugs in argparse.

        aaaaaaa: int = 0
        bbbbbbb: int = 0
        ccccccc: int = 0
        ddddddd: int = 0
        eeeeeee: int = 0

    @dataclasses.dataclass
    class Subcommand2:
        aaaaaaa: int = 0
        bbbbbbb: int = 0
        ccccccc: int = 0
        ddddddd: int = 0
        eeeeeee: int = 0

    @dataclasses.dataclass
    class Subcommand3:
        a: int = 0
        b: int = 0
        c: int = 0
        d: int = 0
        e: int = 0

    @dataclasses.dataclass
    class MultipleSubparsers:
        # Field a description.
        a: Union[Subcommand1, Subcommand2, Subcommand3]
        # Field b description.
        b: Union[Subcommand1, Subcommand2, Subcommand3]
        # Field c description.
        c: Union[Subcommand1, Subcommand2, Subcommand3] = dataclasses.field(
            default_factory=Subcommand3
        )

        ddddddd: bool = False
        fffffff: bool = False
        ggggggg: bool = False
        hhhhhhh: bool = False
        iiiiiii: bool = False

    helptext = get_helptext_with_checks(MultipleSubparsers)

    assert "2% milk." in helptext
    assert "Field a description." in helptext
    # With tyro backend, all subparser groups in the frontier are shown.
    # With argparse backend, only the first subparser group is shown.

    if tyro._experimental_options["backend"] == "tyro":
        assert "Field b description." in helptext
        assert "Field c description." in helptext
    else:
        assert "Field b description." not in helptext
        assert "Field c description." not in helptext

    assert "[OPTIONS]" in helptext
    assert "[B:SUBCOMMAND2 OPTIONS]" not in helptext

    helptext = get_helptext_with_checks(
        MultipleSubparsers, args=["a:subcommand1", "b:subcommand1", "--help"]
    )

    assert "2% milk." in helptext
    assert "Field a description." not in helptext
    assert "Field b description." not in helptext
    assert "Field c description." in helptext
    assert "(default: c:subcommand3)" in helptext

    assert "[OPTIONS]" not in helptext
    assert "[B:SUBCOMMAND1 OPTIONS]" in helptext
    assert "[B:SUBCOMMAND2 OPTIONS]" not in helptext


def test_optional_helptext() -> None:
    @dataclasses.dataclass
    class OptionalHelptext:
        """This docstring should be printed as a description. 2% milk."""

        x: Optional[int]  # Documentation 1

        # Documentation 2
        y: List[Optional[int]]

        z: Optional[int] = 3
        """Documentation 3"""

    helptext = get_helptext_with_checks(OptionalHelptext)
    assert cast(str, cast(str, OptionalHelptext.__doc__)[:20]) in helptext
    assert "2% milk" in helptext
    assert "--x {None}|INT" in helptext
    assert "--y [{None}|INT [{None}|INT ...]]" in helptext
    assert "[--z {None}|INT]" in helptext


def test_metavar_0() -> None:
    def main(x: Union[Literal[0, 1, 2, 3], Tuple[int, int]]) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--x {0,1,2,3}|{INT INT}" in helptext


def test_metavar_1() -> None:
    def main(
        x: Union[
            Literal[0, 1, 2, 3],
            Literal["hey,there", "hello"],
            List[int],
        ],
    ) -> None:
        pass

    # The comma formatting is unfortunate, but matches argparse's default behavior.
    helptext = get_helptext_with_checks(main)
    assert "--x {0,1,2,3,hey,there,hello}|{[INT [INT ...]]}" in helptext


def test_metavar_2() -> None:
    def main(
        x: Tuple[
            Literal[0, 1, 2, 3],
            Union[int, str],
        ],
    ) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--x {0,1,2,3} INT|STR" in helptext


def test_metavar_3() -> None:
    def main(
        x: Union[
            Literal[0, 1, 2, 3],
            Union[Tuple[int, int], Tuple[str]],
        ],
    ) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--x {0,1,2,3}|{INT INT}|STR" in helptext


def test_metavar_4() -> None:
    def main(
        x: Union[
            Literal[0, 1, 2, 3],
            Union[Tuple[int, int], Tuple[str, str, str]],
            Literal[True],
        ],
    ) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--x {0,1,2,3}|{INT INT}|{STR STR STR}|{True}" in helptext


def test_metavar_5() -> None:
    def main(
        x: List[Union[Tuple[int, int], Tuple[str, str]]] = [(1, 1), (2, 2)],
    ) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "[--x [{INT INT}|{STR STR} [{INT INT}|{STR STR} ...]]]" in helptext


def test_metavar_6() -> None:
    def main(x: Dict[Union[Tuple[int, int], Tuple[str, str]], Tuple[int, int]]) -> dict:
        return x

    helptext = get_helptext_with_checks(main)
    assert (
        "--x [{INT INT}|{STR STR} INT INT [{INT INT}|{STR STR} INT INT ...]]"
        in helptext
    )


def test_empty_metavar() -> None:
    """Test that empty metavar doesn't add trailing space."""

    @dataclasses.dataclass
    class Args:
        verbose: Annotated[int, tyro.conf.arg(metavar="", aliases=("-v",))] = 0

    helptext = get_helptext_with_checks(Args)
    # Should have no space after comma: "-v, --verbose".
    # Not: "-v , --verbose".
    assert "-v, --verbose" in helptext
    assert "-v , --verbose" not in helptext


def test_comment_in_subclass_list() -> None:
    @dataclasses.dataclass
    class Something(
        # This text should not show up in the helptext anywhere.
        object,
    ):
        a: int

        # But this text should!
        b: int

    helptext = get_helptext_with_checks(Something)
    assert "This text should not" not in helptext
    assert "But this text should!" in helptext


def test_pathlike() -> None:
    def main(x: os.PathLike) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--x PATHLIKE " in helptext


def test_nested_bool() -> None:
    @dataclasses.dataclass
    class Child:
        x: bool = False

    def main(child: Child) -> None:
        pass

    helptext = get_helptext_with_checks(main)
    assert "--child.x | --child.no-x" in helptext


def test_multiple_subparsers_helptext_hyphens() -> None:
    @dataclasses.dataclass
    class SubcommandOne:
        """2% milk."""  # % symbol is prone to bugs in argparse.

        arg_x: int = 0
        arg_flag: bool = False

    @dataclasses.dataclass
    class SubcommandTwo:
        arg_y: int = 1

    @dataclasses.dataclass
    class SubcommandThree:
        arg_z: int = 2

    @dataclasses.dataclass
    class MultipleSubparsers:
        # Field a description.
        a: Union[SubcommandOne, SubcommandTwo, SubcommandThree]
        # Field b description.
        b: Union[SubcommandOne, SubcommandTwo, SubcommandThree]
        # Field c description.
        c: Union[SubcommandOne, SubcommandTwo, SubcommandThree] = dataclasses.field(
            default_factory=SubcommandThree
        )

    helptext = get_helptext_with_checks(MultipleSubparsers)

    assert "2% milk." in helptext
    assert "Field a description." in helptext
    # With tyro backend, all subparser groups in the frontier are shown.
    # With argparse backend, only the first subparser group is shown.

    if tyro._experimental_options["backend"] == "tyro":
        assert "Field b description." in helptext
        assert "Field c description." in helptext
    else:
        assert "Field b description." not in helptext
        assert "Field c description." not in helptext

    helptext = get_helptext_with_checks(
        MultipleSubparsers, args=["a:subcommand-one", "b:subcommand-one", "--help"]
    )

    assert "2% milk." in helptext
    assert "Field a description." not in helptext
    assert "Field b description." not in helptext
    assert "Field c description." in helptext
    assert "(default: c:subcommand-three)" in helptext
    assert "--b.arg-x" in helptext
    assert "--b.no-arg-flag" in helptext
    assert "--b.arg-flag" in helptext


def test_multiple_subparsers_helptext_underscores() -> None:
    @dataclasses.dataclass
    class SubcommandOne:
        """2% milk."""  # % symbol is prone to bugs in argparse.

        arg_x: int = 0
        arg_flag: bool = False

    @dataclasses.dataclass
    class SubcommandTwo:
        arg_y: int = 1

    @dataclasses.dataclass
    class SubcommandThree:
        arg_z: int = 2

    @dataclasses.dataclass
    class MultipleSubparsers:
        # Field a description.
        a: Union[SubcommandOne, SubcommandTwo, SubcommandThree]
        # Field b description.
        b: Union[SubcommandOne, SubcommandTwo, SubcommandThree]
        # Field c description.
        c: Union[SubcommandOne, SubcommandTwo, SubcommandThree] = dataclasses.field(
            default_factory=SubcommandThree
        )

    helptext = get_helptext_with_checks(MultipleSubparsers, use_underscores=True)

    assert "2% milk." in helptext
    assert "Field a description." in helptext
    # With tyro backend, all subparser groups in the frontier are shown.
    # With argparse backend, only the first subparser group is shown.

    if tyro._experimental_options["backend"] == "tyro":
        assert "Field b description." in helptext
        assert "Field c description." in helptext
    else:
        assert "Field b description." not in helptext
        assert "Field c description." not in helptext

    helptext = get_helptext_with_checks(
        MultipleSubparsers,
        args=["a:subcommand_one", "b:subcommand_one", "--help"],
        use_underscores=True,
    )

    assert "2% milk." in helptext
    assert "Field a description." not in helptext
    assert "Field b description." not in helptext
    assert "Field c description." in helptext
    assert "(default: c:subcommand_three)" in helptext
    assert "--b.arg_x" in helptext
    assert "--b.no_arg_flag" in helptext
    assert "--b.arg_flag" in helptext


def test_subparsers_wrapping() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: int

    @dataclasses.dataclass
    class CheckoutCompletion:
        """Help message."""

        y: int

    help = get_helptext_with_checks(Union[A, CheckoutCompletion])  # type: ignore
    # Both backends use full metavar when there's a single subparser group.
    # Appears 3 times: usage line + subcommand list.
    assert help.count("checkout-completion") == 2


def test_subparsers_wrapping1() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: int

    @dataclasses.dataclass
    class CheckoutCompletio:
        """Help message."""

        y: int

    help = get_helptext_with_checks(Union[A, CheckoutCompletio])  # type: ignore
    # Both backends use full metavar when there's a single subparser group.
    assert help.count("checkout-completio") == 2


def test_subparsers_wrapping2() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: int

    @dataclasses.dataclass
    class CheckoutCompletionn:
        """Help message."""

        y: int

    help = get_helptext_with_checks(Union[A, CheckoutCompletionn])  # type: ignore
    # Both backends use full metavar when there's a single subparser group.
    assert help.count("checkout-completionn") == 2


def test_subparsers_wrapping3() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: int

    @dataclasses.dataclass
    class CmdCheckout012:
        """Help message."""

        y: int

    help = get_helptext_with_checks(Union[A, CmdCheckout012])  # type: ignore
    # Both backends use full metavar when there's a single subparser group.
    assert help.count("cmd-checkout012") == 2


def test_tuple_default() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: Tuple[str, str] = ("hello", "world")

    help = get_helptext_with_checks(A)
    assert "STR STR" in help
    assert "hello world" in help
    assert "('hello', 'world')" not in help


def test_argconf_constructor() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: Annotated[
            Tuple[str, str], tyro.conf.arg(constructor=lambda x: ("a", "b"))
        ] = ("hello", "world")

    help = get_helptext_with_checks(A)
    assert "STR STR" not in help
    # Unlike case above, should not be converted to 'hello world'.
    assert "('hello', 'world')" in help  # JSON special case.


def test_argconf_constructor_json_special_case() -> None:
    @dataclasses.dataclass
    class A:
        """Help message."""

        x: Annotated[
            # NOTE: this style of JSON construction is/will be deprecated.
            # Avoid.
            Tuple[str, str],
            tyro.conf.arg(constructor=json.loads, metavar="JSON"),
        ] = ("hello", "world")
        y: Annotated[
            # NOTE: this style of JSON construction is/will be deprecated.
            # Avoid.
            Tuple[int, int],
            tyro.conf.arg(constructor=json.loads, metavar="JSON"),
        ] = (3, 5)

    help = get_helptext_with_checks(A)
    assert "STR STR" not in help
    assert "JSON" in help
    assert '["hello", "world"]' in help  # JSON special case.
    assert "[3, 5]" in help, help  # JSON special case.


def test_optional_group() -> None:
    def f(
        x: Annotated[
            # NOTE: this style of JSON construction is/will be deprecated.
            # Avoid.
            Tuple[str, str],
            tyro.conf.arg(constructor=json.loads, metavar="JSON"),
        ] = ("hello", "world"),
        y: int = 3,
    ) -> int:
        del x, y
        return 5

    help = get_helptext_with_checks(f, default=3)
    assert 'default if used: ["hello", "world"]' in help
    assert "default if used: 3" in help


def test_append_fixed() -> None:
    def f(
        x: tyro.conf.UseAppendAction[Tuple[str, str]],
        y: int = 3,
    ) -> int:
        del x, y
        return 5

    help = get_helptext_with_checks(f)
    assert "repeatable" not in help, help


def test_append_good() -> None:
    def f(
        x: tyro.conf.UseAppendAction[Tuple[str, ...]],
        y: int = 3,
    ) -> int:
        del x, y
        return 5

    help = get_helptext_with_checks(f)
    assert "repeatable" in help, help


def test_append_with_default() -> None:
    def f(
        x: tyro.conf.UseAppendAction[Tuple[str, ...]] = ("hello world", "hello"),
        y: int = 3,
    ) -> int:
        del x, y
        return 5

    help = get_helptext_with_checks(f)
    assert "repeatable, appends to: 'hello world' hello" in help, help


def test_typeddict_exclude() -> None:
    class Special(TypedDict):
        x: NotRequired[int]

    help = get_helptext_with_checks(Special)
    assert "unset by default" in help, help


def test_conf_erased_argname() -> None:
    @dataclasses.dataclass(frozen=True)
    class Verbose:
        is_verbose: bool = True

    @dataclasses.dataclass(frozen=True)
    class Args:
        verbose: Annotated[Verbose, tyro.conf.arg(name="")]

    def main(args: Args) -> None:
        print(args)

    helptext = get_helptext_with_checks(main)
    assert "args options" in helptext
    assert "args.verbose options" not in helptext


def test_long_literal_options() -> None:
    """Test that long literal options don't get truncated in helptext."""

    def main(
        x: Literal[
            "one",
            "two",
            "three",
            "four",
            "five",
            "six",
            "seven",
            "eight",
            "nine",
            "ten",
            "eleven",
            "twelve",
            "thirteen",
            "fourteen",
            "fifteen",
            "sixteen",
            "seventeen",
            "eighteen",
            "nineteen",
            "twenty",
        ],
    ) -> None:
        print(x)

    helptext = get_helptext_with_checks(main)

    # Check that the helptext contains all options without truncation
    for option in [
        "one",
        "two",
        "three",
        "four",
        "five",
        "six",
        "seven",
        "eight",
        "nine",
        "ten",
        "eleven",
        "twelve",
        "thirteen",
        "fourteen",
        "fifteen",
        "sixteen",
        "seventeen",
        "eighteen",
        "nineteen",
        "twenty",
    ]:
        assert option in helptext

    # Check that there's no ellipsis character in the helptext
    assert "…" not in helptext


def test_bool_help_edge_cases() -> None:
    """Test edge cases in metavar generation for unions with tuples and literals."""

    # Use the exact same pattern as in bool_help_edge.py but as individual function definitions
    # Currently this passes, but represents edge cases that were problematic
    def main_int(x_int: Union[Tuple[int], Tuple[int, int]] = (1,)) -> None:
        pass

    def main_literal(
        x_literal: Union[
            Tuple[Literal[0, 1, 2]], Tuple[Literal[0, 1, 2], Literal[0, 1, 2]]
        ] = (0,),
    ) -> None:
        pass

    def main_bool(x_bool: Union[Tuple[bool], Tuple[bool, bool]] = (True,)) -> None:
        pass

    # Test int case - should show INT|{INT INT}
    helptext_int = get_helptext_with_checks(main_int)
    assert "--x-int INT|{INT INT}" in helptext_int

    # Test literal case - should show {0,1,2}|{{0,1,2} {0,1,2}}
    helptext_literal = get_helptext_with_checks(main_literal)
    assert "--x-literal {0,1,2}|{{0,1,2} {0,1,2}}" in helptext_literal

    # Test bool case - should show {True,False}|{{True,False} {True,False}}
    helptext_bool = get_helptext_with_checks(main_bool)
    assert "--x-bool {True,False}|{{True,False} {True,False}}" in helptext_bool


def test_set_accent_color() -> None:
    """Test that set_accent_color runs without error."""
    tyro.extras.set_accent_color("blue")


def test_help_with_required_subcommands_consolidated() -> None:
    """Test that --help shows help text for required subcommands in consolidated mode.

    This is a regression test for an issue where --help would raise a 'Missing subcommand'
    error instead of showing the help text when using CascadeSubcommandArgs with
    required subcommands (no default).
    """
    import pytest

    @dataclasses.dataclass
    class ConfigA:
        a: int = 1

    @dataclasses.dataclass
    class ConfigB:
        b: int = 2

    def main(config: Union[ConfigA, ConfigB]) -> None:
        """Test function with required union subcommand.

        Args:
            config: Configuration to use.
        """
        pass

    # This should show help text and exit, not raise a "Missing subcommand" error.
    with pytest.raises(SystemExit):
        tyro.cli(main, args=["--help"], config=(tyro.conf.CascadeSubcommandArgs,))


def test_implicit_arguments_show_required() -> None:
    """Test that default subcommand options show '(required)' indicator for required args."""

    @dataclasses.dataclass
    class SubcommandWithRequiredArg:
        """Subcommand with a required argument."""

        required_arg: int  # Required: no default value.
        optional_arg: str = "default"  # Optional: has default value.

    @dataclasses.dataclass
    class SubcommandAllOptional:
        """Another subcommand with all optional args."""

        x: int = 0

    def main(
        subcommand: Union[SubcommandWithRequiredArg, SubcommandAllOptional] = (
            SubcommandWithRequiredArg(required_arg=tyro.MISSING)  # type: ignore
        ),
    ) -> None:
        """Test function with default subcommand containing required arg.

        Args:
            subcommand: Subcommand choice with default.
        """
        pass

    # Get help text with CascadeSubcommandArgs enabled.
    # By default, the default subcommand's arguments appear with full descriptions.
    helptext = get_helptext_with_checks(
        main,
        args=["--help"],
        config=(tyro.conf.CascadeSubcommandArgs,),
    )

    # Implicit arguments only appear with tyro backend.
    if tyro._experimental_options["backend"] != "tyro":
        return

    # Check that required arg has "(required)" indicator.
    assert "--subcommand.required-arg" in helptext
    assert "--subcommand.optional-arg" in helptext
    assert "(required)" in helptext


def test_multiple_default_subcommands_with_required() -> None:
    """Test multiple default subcommands showing required indicators.

    This mirrors the pattern from examples/03_subcommands/03_multiple_subcommands.py
    where dataset and optimizer are both default subcommands with required fields.
    """

    @dataclasses.dataclass
    class Mnist:
        """MNIST dataset configuration."""

        binary: bool  # Required: no default value, like the example.

    @dataclasses.dataclass
    class ImageNet:
        """ImageNet dataset configuration."""

        subset: int = 1000

    @dataclasses.dataclass
    class Adam:
        """Adam optimizer configuration."""

        learning_rate: float = 1e-3

    @dataclasses.dataclass
    class Sgd:
        """SGD optimizer configuration."""

        learning_rate: float = 3e-4

    def train(
        dataset: Union[Mnist, ImageNet] = Mnist(tyro.MISSING),  # type: ignore
        optimizer: Union[Adam, Sgd] = Adam(),
    ) -> None:
        """Example training script.

        Args:
            dataset: Dataset to train on.
            optimizer: Optimizer to train with.
        """
        pass

    # Get help text with CascadeSubcommandArgs enabled.
    helptext = get_helptext_with_checks(
        train,
        args=["--help"],
        config=(tyro.conf.CascadeSubcommandArgs,),
    )

    # Implicit arguments only appear with tyro backend.
    if tyro._experimental_options["backend"] != "tyro":
        return

    # Check that default subcommand args are shown with full descriptions.
    assert "--dataset.binary" in helptext
    assert "(required)" in helptext
    assert "--optimizer.learning-rate" in helptext

    # Check that default subcommand groups are labeled.
    assert "source subcommand: dataset:mnist" in helptext
    assert "source subcommand: optimizer:adam" in helptext


def test_compact_help_short_text() -> None:
    """Test compact mode with short argument text that fits on one line."""
    # Skip this test for argparse backend - compact_help only supported with tyro.
    if tyro._experimental_options["backend"] != "tyro":
        return

    @dataclasses.dataclass
    class ShortArgs:
        """Test program with short arguments."""

        x: int = 5
        """Short description."""

        y: str = "hello"
        """Another short one."""

    # Test compact mode.
    compact_helptext = get_helptext_with_checks(ShortArgs, compact_help=True)
    assert "--help-verbose" in compact_helptext
    assert "--x INT" in compact_helptext
    assert "--y STR" in compact_helptext
    # In compact mode, primary help text is omitted, only defaults shown.
    assert "Short description" not in compact_helptext
    assert "(default: 5)" in compact_helptext
    assert "(default: hello)" in compact_helptext


def test_compact_help_long_text() -> None:
    """Test compact mode with very long argument names and behavior hints."""
    # Skip this test for argparse backend - compact_help only supported with tyro.
    if tyro._experimental_options["backend"] != "tyro":
        return

    @dataclasses.dataclass
    class LongArgs:
        """Test program with very long argument names."""

        very_long_argument_name_that_takes_up_a_lot_of_terminal_space: int = 5
        """This is an extremely long description that will definitely exceed the terminal width when combined with the argument name. It contains many words and should trigger the line-splitting behavior in compact mode."""

    # Test compact mode.
    compact_helptext = get_helptext_with_checks(LongArgs, compact_help=True)
    assert (
        "--very-long-argument-name-that-takes-up-a-lot-of-terminal-space"
        in compact_helptext
    )
    # In compact mode, primary descriptions are omitted.
    assert "extremely long description" not in compact_helptext
    # But default values are still shown.
    assert "(default: 5)" in compact_helptext


def test_verbose_help_short_invocation() -> None:
    """Test verbose mode with short invocation names."""
    # Skip this test for argparse backend - compact_help only supported with tyro.
    if tyro._experimental_options["backend"] != "tyro":
        return

    @dataclasses.dataclass
    class VerboseArgs:
        """Test program for verbose mode."""

        x: int = 5
        """This is a longer description that will appear in verbose mode. It provides detailed information about what the x parameter does and how it should be used."""

        y: str = "test"
        """Another detailed description for the y parameter."""

    # Test verbose mode (default behavior).
    verbose_helptext = get_helptext_with_checks(VerboseArgs, compact_help=False)
    assert "--help-verbose" not in verbose_helptext
    assert "--x INT" in verbose_helptext
    assert "--y STR" in verbose_helptext
    assert "longer description" in verbose_helptext
    assert "Another detailed description" in verbose_helptext


def test_verbose_help_long_invocation() -> None:
    """Test verbose mode with very long invocation names that exceed max width."""
    # Skip this test for argparse backend - compact_help only supported with tyro.
    if tyro._experimental_options["backend"] != "tyro":
        return

    @dataclasses.dataclass
    class VeryLongInvocationArgs:
        """Test program with very long argument names."""

        extremely_long_argument_name_that_exceeds_maximum_width_threshold: int = 5
        """Description for the long argument."""

        another_incredibly_long_argument_name_for_testing_purposes: str = "test"
        """Another description."""

    # Test verbose mode with long invocations.
    verbose_helptext = get_helptext_with_checks(
        VeryLongInvocationArgs, compact_help=False
    )
    assert (
        "--extremely-long-argument-name-that-exceeds-maximum-width-threshold"
        in verbose_helptext
    )
    assert (
        "--another-incredibly-long-argument-name-for-testing-purposes"
        in verbose_helptext
    )
    assert "Description for the long argument" in verbose_helptext


def test_compact_vs_verbose_help() -> None:
    """Test that compact and verbose modes produce different output."""
    # Skip this test for argparse backend - compact_help only supported with tyro.
    if tyro._experimental_options["backend"] != "tyro":
        return

    @dataclasses.dataclass
    class CompareArgs:
        """Test comparing compact vs verbose."""

        x: int = 5
        """A description for parameter x."""

    compact = get_helptext_with_checks(CompareArgs, compact_help=True)
    verbose = get_helptext_with_checks(CompareArgs, compact_help=False)

    # Compact should have --help-verbose.
    assert "--help-verbose" in compact
    assert "--help-verbose" not in verbose

    # Both should have the basic argument.
    assert "--x INT" in compact
    assert "--x INT" in verbose


def test_no_duplicate_description() -> None:
    """Test that description passed via description= kwarg only appears once.

    Regression test for issue where description would appear both after usage line
    and inside the options box, causing duplication.
    """

    def main(x: int) -> None:
        pass

    # Test with description passed as keyword argument.
    target = io.StringIO()
    with pytest.raises(SystemExit), contextlib.redirect_stdout(target):
        tyro.cli(
            main, args=["--help"], description="This helptext should only appear once."
        )

    helptext = tyro._strings.strip_ansi_sequences(target.getvalue())
    # The description should appear in the helptext.
    assert "This helptext should only appear once." in helptext
    # But it should only appear once, not twice.
    assert helptext.count("This helptext should only appear once.") == 1


def test_literal_invalid_choice_error_message() -> None:
    """Test that invalid choice error for Literal types shows clean argument name.

    Regression test for issue where internal implementation detail
    '__tyro-dummy-inner__' was exposed in error messages.
    """

    # When using a Literal type as the main CLI type, invalid choice errors
    # should show a clean argument name, not internal implementation details.
    # This will also be caught by the assertion in helptext_utils, but we test
    # it explicitly here for error messages.
    # Capture stderr to check the error message.
    stderr = io.StringIO()
    with pytest.raises(SystemExit):
        with contextlib.redirect_stderr(stderr):
            tyro.cli(Literal["a", "b"], args=["c"], return_unknown_args=True)  # type: ignore

    # Check the error message.
    error_message = stderr.getvalue()
    # The error message should mention the invalid choice 'c'.
    assert "invalid choice" in error_message.lower() or "c" in error_message
    # But it should NOT expose internal implementation details.
    assert "__tyro-dummy-inner__" not in error_message


def test_optional_union_subparser_helptext() -> None:
    """Test that optional union subparsers are marked as optional in help text."""

    @dataclasses.dataclass
    class StructA:
        """First option."""

        a: int = 1

    @dataclasses.dataclass
    class StructB:
        """Second option."""

        b: int = 2

    # Optional union (total=False).
    class OptionalConfig(TypedDict, total=False):
        """Config with optional union field."""

        choice: Union[StructA, StructB]
        other: int

    helptext_optional = get_helptext_with_checks(OptionalConfig)

    # Check that "(required)" is NOT shown for optional subparser.
    lines = helptext_optional.split("\n")
    in_subcommands_section = False
    for line in lines:
        if "subcommands" in line.lower():
            in_subcommands_section = True
        if in_subcommands_section and "choice:struct-a" in line:
            # The section header before the subcommand list should not say "(required)".
            assert (
                "(required)"
                not in helptext_optional.split("choice:struct-a")[0].split(
                    "subcommands"
                )[-1]
            )
            break

    # For tyro backend, check usage line shows optional subparser in brackets.
    # For argparse backend, the brackets may not show up in the usage line due to
    # argparse limitations, but "optional" should appear in the section title.
    if "[{choice:struct-a,choice:struct-b}]" not in helptext_optional:
        # Argparse backend - check for "optional" in section title.
        assert (
            "optional" in helptext_optional.lower()
            or "choice subcommands" in helptext_optional
        )

    # Required union (total=True by default).
    class RequiredConfig(TypedDict):
        """Config with required union field."""

        choice: Union[StructA, StructB]
        other: int

    helptext_required = get_helptext_with_checks(RequiredConfig)

    # Check usage line shows required subparser.
    assert "{choice:struct-a,choice:struct-b}" in helptext_required

    # Check that "(required)" IS shown for required subparser (tyro backend).
    # Argparse backend doesn't show "(required)" but doesn't mark subcommands as optional.
    if "(required)" not in helptext_required:
        # Argparse backend - ensure "optional subcommands" is NOT in the help text.
        # (The word "optional" may appear elsewhere, like in field descriptions)
        assert "optional subcommands" not in helptext_required.lower()


def test_subcommand_help_with_required_parent_args() -> None:
    """Test that subcommand --help works without satisfying parent required args.

    This is a regression test for https://github.com/brentyi/tyro/issues/403
    where using `python script.py sub:a --help` would fail with "Missing required
    argument: --x" instead of showing the subcommand help.
    """

    @dataclasses.dataclass
    class A:
        a: int

    @dataclasses.dataclass
    class B:
        b: int

    def main(x: int, sub: Union[A, B]) -> None:
        pass

    # This should show help text for the subcommand without requiring --x.
    with pytest.raises(SystemExit) as exc_info:
        tyro.cli(main, args=["sub:a", "--help"])
    assert exc_info.value.code == 0

    # Verify the help text is actually shown (not an error).
    captured = io.StringIO()
    with contextlib.redirect_stdout(captured):
        with pytest.raises(SystemExit):
            tyro.cli(main, args=["sub:a", "--help"])
    helptext = captured.getvalue()
    assert "sub:a" in helptext or "--sub.a" in helptext

    # Also test with -h flag.
    with pytest.raises(SystemExit) as exc_info:
        tyro.cli(main, args=["sub:a", "-h"])
    assert exc_info.value.code == 0

    # -H and --help-verbose are tyro-backend-only features, and only work
    # when compact_help=True.
    if tyro._experimental_options["backend"] == "tyro":
        # Test with compact_help=True: all help flags should work.
        with pytest.raises(SystemExit) as exc_info:
            tyro.cli(main, args=["sub:a", "--help"], compact_help=True)
        assert exc_info.value.code == 0

        with pytest.raises(SystemExit) as exc_info:
            tyro.cli(main, args=["sub:a", "-h"], compact_help=True)
        assert exc_info.value.code == 0

        with pytest.raises(SystemExit) as exc_info:
            tyro.cli(main, args=["sub:a", "-H"], compact_help=True)
        assert exc_info.value.code == 0

        with pytest.raises(SystemExit) as exc_info:
            tyro.cli(main, args=["sub:a", "--help-verbose"], compact_help=True)
        assert exc_info.value.code == 0


def test_comment_not_leaking_through_non_field_lines() -> None:
    """Comments above non-field lines should not leak to subsequent fields."""

    @dataclasses.dataclass
    class Config:
        x: int = 1
        # This comment is for the internal variable.
        _internal = "something"
        y: int = 2

    helptext = get_helptext_with_checks(Config)
    assert "This comment is for the internal variable" not in helptext
