import argparse
import asyncio
import re
import sys
import time
import typing
from enum import IntEnum
from typing import Any, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union

import pytest
import typing_extensions
from pydantic import (
    AliasChoices,
    AliasGenerator,
    AliasPath,
    BaseModel,
    ConfigDict,
    DirectoryPath,
    Discriminator,
    Field,
    Tag,
    ValidationError,
)
from pydantic import (
    dataclasses as pydantic_dataclasses,
)
from pydantic._internal._repr import Representation
from typing_extensions import Annotated, Literal

from pydantic_settings import (
    BaseSettings,
    CliApp,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)
from pydantic_settings.sources import (
    CLI_SUPPRESS,
    CliExplicitFlag,
    CliImplicitFlag,
    CliMutuallyExclusiveGroup,
    CliPositionalArg,
    CliSettingsSource,
    CliSubCommand,
    CliSuppress,
    SettingsError,
    get_subcommand,
)

ARGPARSE_OPTIONS_TEXT = 'options' if sys.version_info >= (3, 10) else 'optional arguments'


@pytest.fixture(autouse=True)
def cli_test_env_autouse(cli_test_env):
    pass


def foobar(a, b, c=4):
    pass


class FruitsEnum(IntEnum):
    pear = 0
    kiwi = 1
    lime = 2


T = TypeVar('T')


class LoggedVar(Generic[T]):
    def get(self) -> T: ...


class SimpleSettings(BaseSettings):
    apple: str


class SettingWithIgnoreEmpty(BaseSettings):
    apple: str = 'default'

    model_config = SettingsConfigDict(env_ignore_empty=True)


class CliDummyArgGroup(BaseModel, arbitrary_types_allowed=True):
    group: argparse._ArgumentGroup

    def add_argument(self, *args: Any, **kwargs: Any) -> None:
        self.group.add_argument(*args, **kwargs)


class CliDummySubParsers(BaseModel, arbitrary_types_allowed=True):
    sub_parser: argparse._SubParsersAction

    def add_parser(self, *args: Any, **kwargs: Any) -> 'CliDummyParser':
        return CliDummyParser(parser=self.sub_parser.add_parser(*args, **kwargs))


class CliDummyParser(BaseModel, arbitrary_types_allowed=True):
    parser: argparse.ArgumentParser = Field(default_factory=lambda: argparse.ArgumentParser())

    def add_argument(self, *args: Any, **kwargs: Any) -> None:
        self.parser.add_argument(*args, **kwargs)

    def add_argument_group(self, *args: Any, **kwargs: Any) -> CliDummyArgGroup:
        return CliDummyArgGroup(group=self.parser.add_argument_group(*args, **kwargs))

    def add_subparsers(self, *args: Any, **kwargs: Any) -> CliDummySubParsers:
        return CliDummySubParsers(sub_parser=self.parser.add_subparsers(*args, **kwargs))

    def parse_args(self, *args: Any, **kwargs: Any) -> argparse.Namespace:
        return self.parser.parse_args(*args, **kwargs)


def test_cli_validation_alias_with_cli_prefix():
    class Settings(BaseSettings, cli_exit_on_error=False):
        foobar: str = Field(validation_alias='foo')

        model_config = SettingsConfigDict(cli_prefix='p')

    with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --foo bar'):
        CliApp.run(Settings, cli_args=['--foo', 'bar'])

    assert CliApp.run(Settings, cli_args=['--p.foo', 'bar']).foobar == 'bar'


@pytest.mark.parametrize(
    'alias_generator',
    [
        AliasGenerator(validation_alias=lambda s: AliasChoices(s, s.replace('_', '-'))),
        AliasGenerator(validation_alias=lambda s: AliasChoices(s.replace('_', '-'), s)),
    ],
)
def test_cli_alias_resolution_consistency_with_env(env, alias_generator):
    class SubModel(BaseModel):
        v1: str = 'model default'

    class Settings(BaseSettings):
        model_config = SettingsConfigDict(
            env_nested_delimiter='__',
            nested_model_default_partial_update=True,
            alias_generator=alias_generator,
        )

        sub_model: SubModel = SubModel(v1='top default')

    assert CliApp.run(Settings, cli_args=[]).model_dump() == {'sub_model': {'v1': 'top default'}}

    env.set('SUB_MODEL__V1', 'env default')
    assert CliApp.run(Settings, cli_args=[]).model_dump() == {'sub_model': {'v1': 'env default'}}

    assert CliApp.run(Settings, cli_args=['--sub-model.v1=cli default']).model_dump() == {
        'sub_model': {'v1': 'cli default'}
    }


def test_cli_nested_arg():
    class SubSubValue(BaseModel):
        v6: str

    class SubValue(BaseModel):
        v4: str
        v5: int
        sub_sub: SubSubValue

    class TopValue(BaseModel):
        v1: str
        v2: str
        v3: str
        sub: SubValue

    class Cfg(BaseSettings):
        v0: str
        v0_union: Union[SubValue, int]
        top: TopValue

    args: List[str] = []
    args += ['--top', '{"v1": "json-1", "v2": "json-2", "sub": {"v5": "xx"}}']
    args += ['--top.sub.v5', '5']
    args += ['--v0', '0']
    args += ['--top.v2', '2']
    args += ['--top.v3', '3']
    args += ['--v0_union', '0']
    args += ['--top.sub.sub_sub.v6', '6']
    args += ['--top.sub.v4', '4']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {
        'v0': '0',
        'v0_union': 0,
        'top': {
            'v1': 'json-1',
            'v2': '2',
            'v3': '3',
            'sub': {'v4': '4', 'v5': 5, 'sub_sub': {'v6': '6'}},
        },
    }


def test_cli_source_prioritization(env):
    class CfgDefault(BaseSettings):
        foo: str

    class CfgPrioritized(BaseSettings):
        foo: str

        @classmethod
        def settings_customise_sources(
            cls,
            settings_cls: Type[BaseSettings],
            init_settings: PydanticBaseSettingsSource,
            env_settings: PydanticBaseSettingsSource,
            dotenv_settings: PydanticBaseSettingsSource,
            file_secret_settings: PydanticBaseSettingsSource,
        ) -> Tuple[PydanticBaseSettingsSource, ...]:
            return env_settings, CliSettingsSource(settings_cls, cli_parse_args=['--foo', 'FOO FROM CLI'])

    env.set('FOO', 'FOO FROM ENV')

    cfg = CliApp.run(CfgDefault, cli_args=['--foo', 'FOO FROM CLI'])
    assert cfg.model_dump() == {'foo': 'FOO FROM CLI'}

    cfg = CfgPrioritized()
    assert cfg.model_dump() == {'foo': 'FOO FROM ENV'}


def test_cli_alias_subcommand_and_positional_args(capsys, monkeypatch):
    class SubCmd(BaseModel):
        pos_arg: CliPositionalArg[str] = Field(validation_alias='pos-arg')

    class Cfg(BaseSettings):
        sub_cmd: CliSubCommand[SubCmd] = Field(validation_alias='sub-cmd')

    cfg = Cfg(**{'sub-cmd': {'pos-arg': 'howdy'}})
    assert cfg.model_dump() == {'sub_cmd': {'pos_arg': 'howdy'}}

    cfg = CliApp.run(Cfg, cli_args=['sub-cmd', 'howdy'])
    assert cfg.model_dump() == {'sub_cmd': {'pos_arg': 'howdy'}}

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Cfg)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{sub-cmd}} ...

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit

subcommands:
  {{sub-cmd}}
    sub-cmd
"""
        )
        m.setattr(sys, 'argv', ['example.py', 'sub-cmd', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Cfg)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py sub-cmd [-h] POS-ARG

positional arguments:
  POS-ARG

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
"""
        )


@pytest.mark.parametrize('avoid_json', [True, False])
def test_cli_alias_arg(capsys, monkeypatch, avoid_json):
    class Cfg(BaseSettings, cli_avoid_json=avoid_json):
        alias_choice_w_path: str = Field(validation_alias=AliasChoices('a', AliasPath('path0', 1)))
        alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1)))
        alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c'))
        alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1))
        alias_str: str = Field(validation_alias='str')

    cfg = CliApp.run(
        Cfg,
        cli_args=[
            '-a',
            'a',
            '-b',
            'b',
            '--str',
            'str',
            '--path0',
            'a0,b0,c0',
            '--path1',
            'a1,b1,c1',
            '--path2',
            '{"deep": ["a2","b2","c2"]}',
        ],
    )
    assert cfg.model_dump() == {
        'alias_choice_w_path': 'a',
        'alias_choice_w_only_path': 'b1',
        'alias_choice_no_path': 'b',
        'alias_path': 'b2',
        'alias_str': 'str',
    }


@pytest.mark.parametrize('avoid_json', [True, False])
def test_cli_alias_nested_arg(capsys, monkeypatch, avoid_json):
    class Nested(BaseModel):
        alias_choice_w_path: str = Field(validation_alias=AliasChoices('a', AliasPath('path0', 1)))
        alias_choice_w_only_path: str = Field(validation_alias=AliasChoices(AliasPath('path1', 1)))
        alias_choice_no_path: str = Field(validation_alias=AliasChoices('b', 'c'))
        alias_path: str = Field(validation_alias=AliasPath('path2', 'deep', 1))
        alias_str: str = Field(validation_alias='str')

    class Cfg(BaseSettings, cli_avoid_json=avoid_json):
        nest: Nested

    cfg = CliApp.run(
        Cfg,
        cli_args=[
            '--nest.a',
            'a',
            '--nest.b',
            'b',
            '--nest.str',
            'str',
            '--nest',
            '{"path0": ["a0","b0","c0"], "path1": ["a1","b1","c1"], "path2": {"deep": ["a2","b2","c2"]}}',
        ],
    )
    assert cfg.model_dump() == {
        'nest': {
            'alias_choice_w_path': 'a',
            'alias_choice_w_only_path': 'b1',
            'alias_choice_no_path': 'b',
            'alias_path': 'b2',
            'alias_str': 'str',
        }
    }


def test_cli_alias_exceptions(capsys, monkeypatch):
    with pytest.raises(SettingsError, match='subcommand argument BadCliSubCommand.foo has multiple aliases'):

        class SubCmd(BaseModel):
            v0: int

        class BadCliSubCommand(BaseSettings):
            foo: CliSubCommand[SubCmd] = Field(validation_alias=AliasChoices('bar', 'boo'))

        CliApp.run(BadCliSubCommand)

    with pytest.raises(SettingsError, match='positional argument BadCliPositionalArg.foo has multiple alias'):

        class BadCliPositionalArg(BaseSettings):
            foo: CliPositionalArg[int] = Field(validation_alias=AliasChoices('bar', 'boo'))

        CliApp.run(BadCliPositionalArg)


def test_cli_case_insensitive_arg():
    class Cfg(BaseSettings, cli_exit_on_error=False):
        foo: str = Field(validation_alias=AliasChoices('F', 'Foo'))
        bar: str = Field(validation_alias=AliasChoices('B', 'Bar'))

    cfg = CliApp.run(
        Cfg,
        cli_args=[
            '--FOO=--VAL',
            '--BAR',
            '"--VAL"',
        ],
    )
    assert cfg.model_dump() == {'foo': '--VAL', 'bar': '"--VAL"'}

    cfg = CliApp.run(
        Cfg,
        cli_args=[
            '-f=-V',
            '-b',
            '"-V"',
        ],
    )
    assert cfg.model_dump() == {'foo': '-V', 'bar': '"-V"'}

    cfg = Cfg(_cli_parse_args=['--Foo=--VAL', '--Bar', '"--VAL"'], _case_sensitive=True)
    assert cfg.model_dump() == {'foo': '--VAL', 'bar': '"--VAL"'}

    cfg = Cfg(_cli_parse_args=['-F=-V', '-B', '"-V"'], _case_sensitive=True)
    assert cfg.model_dump() == {'foo': '-V', 'bar': '"-V"'}

    with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --FOO=--VAL --BAR "--VAL"'):
        Cfg(_cli_parse_args=['--FOO=--VAL', '--BAR', '"--VAL"'], _case_sensitive=True)

    with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: -f=-V -b "-V"'):
        Cfg(_cli_parse_args=['-f=-V', '-b', '"-V"'], _case_sensitive=True)

    with pytest.raises(SettingsError, match='Case-insensitive matching is only supported on the internal root parser'):
        CliSettingsSource(Cfg, root_parser=CliDummyParser(), case_sensitive=False)


def test_cli_help_differentiation(capsys, monkeypatch):
    class Cfg(BaseSettings):
        foo: str
        bar: int = 123
        boo: int = Field(default_factory=lambda: 456)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Cfg)

        assert (
            re.sub(r'0x\w+', '0xffffffff', capsys.readouterr().out, flags=re.MULTILINE)
            == f"""usage: example.py [-h] [--foo str] [--bar int] [--boo int]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
  --foo str   (required)
  --bar int   (default: 123)
  --boo int   (default factory: <lambda>)
"""
        )


def test_cli_help_string_format(capsys, monkeypatch):
    class Cfg(BaseSettings, cli_parse_args=True):
        date_str: str = '%Y-%m-%d'

    class MultilineDoc(BaseSettings, cli_parse_args=True):
        """
        My
        Multiline
        Doc
        """

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Cfg()

        assert (
            re.sub(r'0x\w+', '0xffffffff', capsys.readouterr().out, flags=re.MULTILINE)
            == f"""usage: example.py [-h] [--date_str str]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help      show this help message and exit
  --date_str str  (default: %Y-%m-%d)
"""
        )

        with pytest.raises(SystemExit):
            MultilineDoc()
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h]

My
Multiline
Doc

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
"""
        )

        with pytest.raises(SystemExit):
            cli_settings_source = CliSettingsSource(MultilineDoc, formatter_class=argparse.HelpFormatter)
            MultilineDoc(_cli_settings_source=cli_settings_source(args=True))
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h]

My Multiline Doc

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
"""
        )


def test_cli_help_union_of_models(capsys, monkeypatch):
    class Cat(BaseModel):
        meow: str = 'meow'

    class Dog(BaseModel):
        bark: str = 'bark'

    class Bird(BaseModel):
        caww: str = 'caww'
        tweet: str

    class Tiger(Cat):
        roar: str = 'roar'

    class Car(BaseSettings, cli_parse_args=True):
        driver: Union[Cat, Dog, Bird] = Tiger(meow='purr')

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Car()
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--driver JSON] [--driver.meow str] [--driver.bark str]
                  [--driver.caww str] [--driver.tweet str]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help          show this help message and exit

driver options:
  --driver JSON       set driver from JSON string
  --driver.meow str   (default: purr)
  --driver.bark str   (default: bark)
  --driver.caww str   (default: caww)
  --driver.tweet str  (ifdef: required)
"""
        )


def test_cli_help_default_or_none_model(capsys, monkeypatch):
    class DeeperSubModel(BaseModel):
        flag: bool

    class DeepSubModel(BaseModel):
        flag: bool
        deeper: Optional[DeeperSubModel] = None

    class SubModel(BaseModel):
        flag: bool
        deep: DeepSubModel = DeepSubModel(flag=True)

    class Settings(BaseSettings, cli_parse_args=True):
        flag: bool = True
        sub_model: SubModel = SubModel(flag=False)
        opt_model: Optional[DeepSubModel] = Field(None, description='Group Doc')
        fact_model: SubModel = Field(default_factory=lambda: SubModel(flag=True))

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Settings()
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--flag bool] [--sub_model JSON]
                  [--sub_model.flag bool] [--sub_model.deep JSON]
                  [--sub_model.deep.flag bool]
                  [--sub_model.deep.deeper {{JSON,null}}]
                  [--sub_model.deep.deeper.flag bool]
                  [--opt_model {{JSON,null}}] [--opt_model.flag bool]
                  [--opt_model.deeper {{JSON,null}}]
                  [--opt_model.deeper.flag bool] [--fact_model JSON]
                  [--fact_model.flag bool] [--fact_model.deep JSON]
                  [--fact_model.deep.flag bool]
                  [--fact_model.deep.deeper {{JSON,null}}]
                  [--fact_model.deep.deeper.flag bool]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit
  --flag bool           (default: True)

sub_model options:
  --sub_model JSON      set sub_model from JSON string
  --sub_model.flag bool
                        (default: False)

sub_model.deep options:
  --sub_model.deep JSON
                        set sub_model.deep from JSON string
  --sub_model.deep.flag bool
                        (default: True)

sub_model.deep.deeper options:
  default: null (undefined)

  --sub_model.deep.deeper {{JSON,null}}
                        set sub_model.deep.deeper from JSON string
  --sub_model.deep.deeper.flag bool
                        (ifdef: required)

opt_model options:
  default: null (undefined)
  Group Doc

  --opt_model {{JSON,null}}
                        set opt_model from JSON string
  --opt_model.flag bool
                        (ifdef: required)

opt_model.deeper options:
  default: null (undefined)

  --opt_model.deeper {{JSON,null}}
                        set opt_model.deeper from JSON string
  --opt_model.deeper.flag bool
                        (ifdef: required)

fact_model options:
  --fact_model JSON     set fact_model from JSON string
  --fact_model.flag bool
                        (default factory: <lambda>)

fact_model.deep options:
  --fact_model.deep JSON
                        set fact_model.deep from JSON string
  --fact_model.deep.flag bool
                        (default factory: <lambda>)

fact_model.deep.deeper options:
  --fact_model.deep.deeper {{JSON,null}}
                        set fact_model.deep.deeper from JSON string
  --fact_model.deep.deeper.flag bool
                        (default factory: <lambda>)
"""
        )


def test_cli_nested_dataclass_arg():
    @pydantic_dataclasses.dataclass
    class MyDataclass:
        foo: int
        bar: str

    class Settings(BaseSettings):
        n: MyDataclass

    s = CliApp.run(Settings, cli_args=['--n.foo', '123', '--n.bar', 'bar value'])
    assert isinstance(s.n, MyDataclass)
    assert s.n.foo == 123
    assert s.n.bar == 'bar value'


def no_add_cli_arg_spaces(arg_str: str, has_quote_comma: bool = False) -> str:
    return arg_str


def add_cli_arg_spaces(arg_str: str, has_quote_comma: bool = False) -> str:
    arg_str = arg_str.replace('[', ' [ ')
    arg_str = arg_str.replace(']', ' ] ')
    arg_str = arg_str.replace('{', ' { ')
    arg_str = arg_str.replace('}', ' } ')
    arg_str = arg_str.replace(':', ' : ')
    if not has_quote_comma:
        arg_str = arg_str.replace(',', ' , ')
    else:
        arg_str = arg_str.replace('",', '" , ')
    return f' {arg_str} '


@pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces])
@pytest.mark.parametrize('prefix', ['', 'child.'])
def test_cli_list_arg(prefix, arg_spaces):
    class Obj(BaseModel):
        val: int

    class Child(BaseModel):
        num_list: Optional[List[int]] = None
        obj_list: Optional[List[Obj]] = None
        str_list: Optional[List[str]] = None
        union_list: Optional[List[Union[Obj, int]]] = None

    class Cfg(BaseSettings):
        num_list: Optional[List[int]] = None
        obj_list: Optional[List[Obj]] = None
        union_list: Optional[List[Union[Obj, int]]] = None
        str_list: Optional[List[str]] = None
        child: Optional[Child] = None

    def check_answer(cfg, prefix, expected):
        if prefix:
            assert cfg.model_dump() == {
                'num_list': None,
                'obj_list': None,
                'union_list': None,
                'str_list': None,
                'child': expected,
            }
        else:
            expected['child'] = None
            assert cfg.model_dump() == expected

    args: List[str] = []
    args = [f'--{prefix}num_list', arg_spaces('[1,2]')]
    args += [f'--{prefix}num_list', arg_spaces('3,4')]
    args += [f'--{prefix}num_list', '5', f'--{prefix}num_list', '6']
    cfg = CliApp.run(Cfg, cli_args=args)
    expected = {
        'num_list': [1, 2, 3, 4, 5, 6],
        'obj_list': None,
        'union_list': None,
        'str_list': None,
    }
    check_answer(cfg, prefix, expected)

    args = [f'--{prefix}obj_list', arg_spaces('[{"val":1},{"val":2}]')]
    args += [f'--{prefix}obj_list', arg_spaces('{"val":3},{"val":4}')]
    args += [f'--{prefix}obj_list', arg_spaces('{"val":5}'), f'--{prefix}obj_list', arg_spaces('{"val":6}')]
    cfg = CliApp.run(Cfg, cli_args=args)
    expected = {
        'num_list': None,
        'obj_list': [{'val': 1}, {'val': 2}, {'val': 3}, {'val': 4}, {'val': 5}, {'val': 6}],
        'union_list': None,
        'str_list': None,
    }
    check_answer(cfg, prefix, expected)

    args = [f'--{prefix}union_list', arg_spaces('[{"val":1},2]'), f'--{prefix}union_list', arg_spaces('[3,{"val":4}]')]
    args += [f'--{prefix}union_list', arg_spaces('{"val":5},6'), f'--{prefix}union_list', arg_spaces('7,{"val":8}')]
    args += [f'--{prefix}union_list', arg_spaces('{"val":9}'), f'--{prefix}union_list', '10']
    cfg = CliApp.run(Cfg, cli_args=args)
    expected = {
        'num_list': None,
        'obj_list': None,
        'union_list': [{'val': 1}, 2, 3, {'val': 4}, {'val': 5}, 6, 7, {'val': 8}, {'val': 9}, 10],
        'str_list': None,
    }
    check_answer(cfg, prefix, expected)

    args = [f'--{prefix}str_list', arg_spaces('["0,0","1,1"]', has_quote_comma=True)]
    args += [f'--{prefix}str_list', arg_spaces('"2,2","3,3"', has_quote_comma=True)]
    args += [
        f'--{prefix}str_list',
        arg_spaces('"4,4"', has_quote_comma=True),
        f'--{prefix}str_list',
        arg_spaces('"5,5"', has_quote_comma=True),
    ]
    cfg = CliApp.run(Cfg, cli_args=args)
    expected = {
        'num_list': None,
        'obj_list': None,
        'union_list': None,
        'str_list': ['0,0', '1,1', '2,2', '3,3', '4,4', '5,5'],
    }
    check_answer(cfg, prefix, expected)


@pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces])
def test_cli_list_json_value_parsing(arg_spaces):
    class Cfg(BaseSettings):
        json_list: List[Union[str, bool, None]]

    assert CliApp.run(
        Cfg,
        cli_args=[
            '--json_list',
            arg_spaces('true,"true"'),
            '--json_list',
            arg_spaces('false,"false"'),
            '--json_list',
            arg_spaces('null,"null"'),
            '--json_list',
            arg_spaces('hi,"bye"'),
        ],
    ).model_dump() == {'json_list': [True, 'true', False, 'false', None, 'null', 'hi', 'bye']}

    assert CliApp.run(Cfg, cli_args=['--json_list', '"","","",""']).model_dump() == {'json_list': ['', '', '', '']}
    assert CliApp.run(Cfg, cli_args=['--json_list', ',,,']).model_dump() == {'json_list': ['', '', '', '']}


@pytest.mark.parametrize('arg_spaces', [no_add_cli_arg_spaces, add_cli_arg_spaces])
@pytest.mark.parametrize('prefix', ['', 'child.'])
def test_cli_dict_arg(prefix, arg_spaces):
    class Child(BaseModel):
        check_dict: Dict[str, str]

    class Cfg(BaseSettings):
        check_dict: Optional[Dict[str, str]] = None
        child: Optional[Child] = None

    args: List[str] = []
    args = [f'--{prefix}check_dict', arg_spaces('{"k1":"a","k2":"b"}')]
    args += [f'--{prefix}check_dict', arg_spaces('{"k3":"c"},{"k4":"d"}')]
    args += [f'--{prefix}check_dict', arg_spaces('{"k5":"e"}'), f'--{prefix}check_dict', arg_spaces('{"k6":"f"}')]
    args += [f'--{prefix}check_dict', arg_spaces('[k7=g,k8=h]')]
    args += [f'--{prefix}check_dict', arg_spaces('k9=i,k10=j')]
    args += [f'--{prefix}check_dict', arg_spaces('k11=k'), f'--{prefix}check_dict', arg_spaces('k12=l')]
    args += [
        f'--{prefix}check_dict',
        arg_spaces('[{"k13":"m"},k14=n]'),
        f'--{prefix}check_dict',
        arg_spaces('[k15=o,{"k16":"p"}]'),
    ]
    args += [
        f'--{prefix}check_dict',
        arg_spaces('{"k17":"q"},k18=r'),
        f'--{prefix}check_dict',
        arg_spaces('k19=s,{"k20":"t"}'),
    ]
    args += [f'--{prefix}check_dict', arg_spaces('{"k21":"u"},k22=v,{"k23":"w"}')]
    args += [f'--{prefix}check_dict', arg_spaces('k24=x,{"k25":"y"},k26=z')]
    args += [f'--{prefix}check_dict', arg_spaces('[k27="x,y",k28="x,y"]', has_quote_comma=True)]
    args += [f'--{prefix}check_dict', arg_spaces('k29="x,y",k30="x,y"', has_quote_comma=True)]
    args += [
        f'--{prefix}check_dict',
        arg_spaces('k31="x,y"', has_quote_comma=True),
        f'--{prefix}check_dict',
        arg_spaces('k32="x,y"', has_quote_comma=True),
    ]
    cfg = CliApp.run(Cfg, cli_args=args)
    expected: Dict[str, Any] = {
        'check_dict': {
            'k1': 'a',
            'k2': 'b',
            'k3': 'c',
            'k4': 'd',
            'k5': 'e',
            'k6': 'f',
            'k7': 'g',
            'k8': 'h',
            'k9': 'i',
            'k10': 'j',
            'k11': 'k',
            'k12': 'l',
            'k13': 'm',
            'k14': 'n',
            'k15': 'o',
            'k16': 'p',
            'k17': 'q',
            'k18': 'r',
            'k19': 's',
            'k20': 't',
            'k21': 'u',
            'k22': 'v',
            'k23': 'w',
            'k24': 'x',
            'k25': 'y',
            'k26': 'z',
            'k27': 'x,y',
            'k28': 'x,y',
            'k29': 'x,y',
            'k30': 'x,y',
            'k31': 'x,y',
            'k32': 'x,y',
        }
    }
    if prefix:
        expected = {'check_dict': None, 'child': expected}
    else:
        expected['child'] = None
    assert cfg.model_dump() == expected

    with pytest.raises(SettingsError, match=f'Parsing error encountered for {prefix}check_dict: Mismatched quotes'):
        cfg = CliApp.run(Cfg, cli_args=[f'--{prefix}check_dict', 'k9="i'])

    with pytest.raises(SettingsError, match=f'Parsing error encountered for {prefix}check_dict: Mismatched quotes'):
        cfg = CliApp.run(Cfg, cli_args=[f'--{prefix}check_dict', 'k9=i"'])


def test_cli_union_dict_arg():
    class Cfg(BaseSettings):
        union_str_dict: Union[str, Dict[str, Any]]

    with pytest.raises(ValidationError) as exc_info:
        args = ['--union_str_dict', 'hello world', '--union_str_dict', 'hello world']
        cfg = CliApp.run(Cfg, cli_args=args)
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': [
                'hello world',
                'hello world',
            ],
            'loc': (
                'union_str_dict',
                'str',
            ),
            'msg': 'Input should be a valid string',
            'type': 'string_type',
        },
        {
            'input': [
                'hello world',
                'hello world',
            ],
            'loc': (
                'union_str_dict',
                'dict[str,any]',
            ),
            'msg': 'Input should be a valid dictionary',
            'type': 'dict_type',
        },
    ]

    args = ['--union_str_dict', 'hello world']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_str_dict': 'hello world'}

    args = ['--union_str_dict', '{"hello": "world"}']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_str_dict': {'hello': 'world'}}

    args = ['--union_str_dict', 'hello=world']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_str_dict': {'hello': 'world'}}

    args = ['--union_str_dict', '"hello=world"']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_str_dict': 'hello=world'}

    class Cfg(BaseSettings):
        union_list_dict: Union[List[str], Dict[str, Any]]

    with pytest.raises(ValidationError) as exc_info:
        args = ['--union_list_dict', 'hello,world']
        cfg = CliApp.run(Cfg, cli_args=args)
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'hello,world',
            'loc': (
                'union_list_dict',
                'list[str]',
            ),
            'msg': 'Input should be a valid list',
            'type': 'list_type',
        },
        {
            'input': 'hello,world',
            'loc': (
                'union_list_dict',
                'dict[str,any]',
            ),
            'msg': 'Input should be a valid dictionary',
            'type': 'dict_type',
        },
    ]

    args = ['--union_list_dict', 'hello,world', '--union_list_dict', 'hello,world']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_list_dict': ['hello', 'world', 'hello', 'world']}

    args = ['--union_list_dict', '[hello,world]']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_list_dict': ['hello', 'world']}

    args = ['--union_list_dict', '{"hello": "world"}']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_list_dict': {'hello': 'world'}}

    args = ['--union_list_dict', 'hello=world']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_list_dict': {'hello': 'world'}}

    with pytest.raises(ValidationError) as exc_info:
        args = ['--union_list_dict', '"hello=world"']
        cfg = CliApp.run(Cfg, cli_args=args)
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'hello=world',
            'loc': (
                'union_list_dict',
                'list[str]',
            ),
            'msg': 'Input should be a valid list',
            'type': 'list_type',
        },
        {
            'input': 'hello=world',
            'loc': (
                'union_list_dict',
                'dict[str,any]',
            ),
            'msg': 'Input should be a valid dictionary',
            'type': 'dict_type',
        },
    ]

    args = ['--union_list_dict', '["hello=world"]']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'union_list_dict': ['hello=world']}


def test_cli_nested_dict_arg():
    class Cfg(BaseSettings):
        check_dict: Dict[str, Any]

    args = ['--check_dict', '{"k1":{"a": 1}},{"k2":{"b": 2}}']
    cfg = CliApp.run(Cfg, cli_args=args)
    assert cfg.model_dump() == {'check_dict': {'k1': {'a': 1}, 'k2': {'b': 2}}}

    with pytest.raises(
        SettingsError,
        match=re.escape('Parsing error encountered for check_dict: not enough values to unpack (expected 2, got 1)'),
    ):
        args = ['--check_dict', '{"k1":{"a": 1}},"k2":{"b": 2}}']
        cfg = CliApp.run(Cfg, cli_args=args)

    with pytest.raises(SettingsError, match='Parsing error encountered for check_dict: Missing end delimiter "}"'):
        args = ['--check_dict', '{"k1":{"a": 1}},{"k2":{"b": 2}']
        cfg = CliApp.run(Cfg, cli_args=args)


def test_cli_subcommand_union(capsys, monkeypatch):
    class AlphaCmd(BaseModel):
        """Alpha Help"""

        a: str

    class BetaCmd(BaseModel):
        """Beta Help"""

        b: str

    class GammaCmd(BaseModel):
        """Gamma Help"""

        g: str

    class Root1(BaseSettings):
        """Root Help"""

        subcommand: CliSubCommand[Union[AlphaCmd, BetaCmd, GammaCmd]] = Field(description='Field Help')

    alpha = CliApp.run(Root1, cli_args=['AlphaCmd', '-a=alpha'])
    assert get_subcommand(alpha).model_dump() == {'a': 'alpha'}
    assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}}
    beta = CliApp.run(Root1, cli_args=['BetaCmd', '-b=beta'])
    assert get_subcommand(beta).model_dump() == {'b': 'beta'}
    assert beta.model_dump() == {'subcommand': {'b': 'beta'}}
    gamma = CliApp.run(Root1, cli_args=['GammaCmd', '-g=gamma'])
    assert get_subcommand(gamma).model_dump() == {'g': 'gamma'}
    assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}}

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Root1)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{AlphaCmd,BetaCmd,GammaCmd}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  Field Help

  {{AlphaCmd,BetaCmd,GammaCmd}}
    AlphaCmd
    BetaCmd
    GammaCmd
"""
        )

        with pytest.raises(SystemExit):
            Root1(_cli_parse_args=True, _cli_use_class_docs_for_groups=True)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{AlphaCmd,BetaCmd,GammaCmd}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  Field Help

  {{AlphaCmd,BetaCmd,GammaCmd}}
    AlphaCmd            Alpha Help
    BetaCmd             Beta Help
    GammaCmd            Gamma Help
"""
        )

    class Root2(BaseSettings):
        """Root Help"""

        subcommand: CliSubCommand[Union[AlphaCmd, GammaCmd]] = Field(description='Field Help')
        beta: CliSubCommand[BetaCmd] = Field(description='Field Beta Help')

    alpha = CliApp.run(Root2, cli_args=['AlphaCmd', '-a=alpha'])
    assert get_subcommand(alpha).model_dump() == {'a': 'alpha'}
    assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}, 'beta': None}
    beta = CliApp.run(Root2, cli_args=['beta', '-b=beta'])
    assert get_subcommand(beta).model_dump() == {'b': 'beta'}
    assert beta.model_dump() == {'subcommand': None, 'beta': {'b': 'beta'}}
    gamma = CliApp.run(Root2, cli_args=['GammaCmd', '-g=gamma'])
    assert get_subcommand(gamma).model_dump() == {'g': 'gamma'}
    assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}, 'beta': None}

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Root2, cli_args=True)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{AlphaCmd,GammaCmd,beta}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  Field Help

  {{AlphaCmd,GammaCmd,beta}}
    AlphaCmd
    GammaCmd
    beta                Field Beta Help
"""
        )

        with pytest.raises(SystemExit):
            Root2(_cli_parse_args=True, _cli_use_class_docs_for_groups=True)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{AlphaCmd,GammaCmd,beta}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  Field Help

  {{AlphaCmd,GammaCmd,beta}}
    AlphaCmd            Alpha Help
    GammaCmd            Gamma Help
    beta                Beta Help
"""
        )

    class Root3(BaseSettings):
        """Root Help"""

        beta: CliSubCommand[BetaCmd] = Field(description='Field Beta Help')
        subcommand: CliSubCommand[Union[AlphaCmd, GammaCmd]] = Field(description='Field Help')

    alpha = CliApp.run(Root3, cli_args=['AlphaCmd', '-a=alpha'])
    assert get_subcommand(alpha).model_dump() == {'a': 'alpha'}
    assert alpha.model_dump() == {'subcommand': {'a': 'alpha'}, 'beta': None}
    beta = CliApp.run(Root3, cli_args=['beta', '-b=beta'])
    assert get_subcommand(beta).model_dump() == {'b': 'beta'}
    assert beta.model_dump() == {'subcommand': None, 'beta': {'b': 'beta'}}
    gamma = CliApp.run(Root3, cli_args=['GammaCmd', '-g=gamma'])
    assert get_subcommand(gamma).model_dump() == {'g': 'gamma'}
    assert gamma.model_dump() == {'subcommand': {'g': 'gamma'}, 'beta': None}

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Root3)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{beta,AlphaCmd,GammaCmd}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  {{beta,AlphaCmd,GammaCmd}}
    beta                Field Beta Help
    AlphaCmd
    GammaCmd
"""
        )

        with pytest.raises(SystemExit):
            Root3(_cli_parse_args=True, _cli_use_class_docs_for_groups=True)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] {{beta,AlphaCmd,GammaCmd}} ...

Root Help

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

subcommands:
  {{beta,AlphaCmd,GammaCmd}}
    beta                Beta Help
    AlphaCmd            Alpha Help
    GammaCmd            Gamma Help
"""
        )


def test_cli_subcommand_with_positionals():
    @pydantic_dataclasses.dataclass
    class FooPlugin:
        my_feature: bool = False

    @pydantic_dataclasses.dataclass
    class BarPlugin:
        my_feature: bool = False

    bar = BarPlugin()
    with pytest.raises(SystemExit, match='Error: CLI subcommand is required but no subcommands were found.'):
        get_subcommand(bar)
    with pytest.raises(SettingsError, match='Error: CLI subcommand is required but no subcommands were found.'):
        get_subcommand(bar, cli_exit_on_error=False)

    @pydantic_dataclasses.dataclass
    class Plugins:
        foo: CliSubCommand[FooPlugin]
        bar: CliSubCommand[BarPlugin]

    class Clone(BaseModel):
        repository: CliPositionalArg[str]
        directory: CliPositionalArg[str]
        local: bool = False
        shared: bool = False

    class Init(BaseModel):
        directory: CliPositionalArg[str]
        quiet: bool = False
        bare: bool = False

    class Git(BaseSettings):
        clone: CliSubCommand[Clone]
        init: CliSubCommand[Init]
        plugins: CliSubCommand[Plugins]

    git = CliApp.run(Git, cli_args=[])
    assert git.model_dump() == {
        'clone': None,
        'init': None,
        'plugins': None,
    }
    assert get_subcommand(git, is_required=False) is None
    with pytest.raises(SystemExit, match='Error: CLI subcommand is required {clone, init, plugins}'):
        get_subcommand(git)
    with pytest.raises(SettingsError, match='Error: CLI subcommand is required {clone, init, plugins}'):
        get_subcommand(git, cli_exit_on_error=False)

    git = CliApp.run(Git, cli_args=['init', '--quiet', 'true', 'dir/path'])
    assert git.model_dump() == {
        'clone': None,
        'init': {'directory': 'dir/path', 'quiet': True, 'bare': False},
        'plugins': None,
    }
    assert get_subcommand(git) == git.init
    assert get_subcommand(git, is_required=False) == git.init

    git = CliApp.run(Git, cli_args=['clone', 'repo', '.', '--shared', 'true'])
    assert git.model_dump() == {
        'clone': {'repository': 'repo', 'directory': '.', 'local': False, 'shared': True},
        'init': None,
        'plugins': None,
    }
    assert get_subcommand(git) == git.clone
    assert get_subcommand(git, is_required=False) == git.clone

    git = CliApp.run(Git, cli_args=['plugins', 'bar'])
    assert git.model_dump() == {
        'clone': None,
        'init': None,
        'plugins': {'foo': None, 'bar': {'my_feature': False}},
    }
    assert get_subcommand(git) == git.plugins
    assert get_subcommand(git, is_required=False) == git.plugins
    assert get_subcommand(get_subcommand(git)) == git.plugins.bar
    assert get_subcommand(get_subcommand(git), is_required=False) == git.plugins.bar

    class NotModel: ...

    with pytest.raises(
        SettingsError, match='Error: NotModel is not subclass of BaseModel or pydantic.dataclasses.dataclass'
    ):
        get_subcommand(NotModel())

    class NotSettingsConfigDict(BaseModel):
        model_config = ConfigDict(cli_exit_on_error='not a bool')

    with pytest.raises(SystemExit, match='Error: CLI subcommand is required but no subcommands were found.'):
        get_subcommand(NotSettingsConfigDict())

    with pytest.raises(SettingsError, match='Error: CLI subcommand is required but no subcommands were found.'):
        get_subcommand(NotSettingsConfigDict(), cli_exit_on_error=False)


def test_cli_union_similar_sub_models():
    class ChildA(BaseModel):
        name: str = 'child a'
        diff_a: str = 'child a difference'

    class ChildB(BaseModel):
        name: str = 'child b'
        diff_b: str = 'child b difference'

    class Cfg(BaseSettings):
        child: Union[ChildA, ChildB]

    cfg = CliApp.run(Cfg, cli_args=['--child.name', 'new name a', '--child.diff_a', 'new diff a'])
    assert cfg.model_dump() == {'child': {'name': 'new name a', 'diff_a': 'new diff a'}}


def test_cli_optional_positional_arg(env):
    class Main(BaseSettings):
        model_config = SettingsConfigDict(
            cli_parse_args=True,
            cli_enforce_required=True,
        )

        value: CliPositionalArg[int] = 123

    assert CliApp.run(Main, cli_args=[]).model_dump() == {'value': 123}

    env.set('VALUE', '456')
    assert CliApp.run(Main, cli_args=[]).model_dump() == {'value': 456}

    assert CliApp.run(Main, cli_args=['789']).model_dump() == {'value': 789}


def test_cli_variadic_positional_arg(env):
    class MainRequired(BaseSettings):
        model_config = SettingsConfigDict(cli_parse_args=True)

        values: CliPositionalArg[List[int]]

    class MainOptional(MainRequired):
        values: CliPositionalArg[List[int]] = [1, 2, 3]

    assert CliApp.run(MainOptional, cli_args=[]).model_dump() == {'values': [1, 2, 3]}
    with pytest.raises(SettingsError, match='error parsing CLI: the following arguments are required: VALUES'):
        CliApp.run(MainRequired, cli_args=[], cli_exit_on_error=False)

    env.set('VALUES', '[4,5,6]')
    assert CliApp.run(MainOptional, cli_args=[]).model_dump() == {'values': [4, 5, 6]}
    with pytest.raises(SettingsError, match='error parsing CLI: the following arguments are required: VALUES'):
        CliApp.run(MainRequired, cli_args=[], cli_exit_on_error=False)

    assert CliApp.run(MainOptional, cli_args=['7', '8', '9']).model_dump() == {'values': [7, 8, 9]}
    assert CliApp.run(MainRequired, cli_args=['7', '8', '9']).model_dump() == {'values': [7, 8, 9]}


def test_cli_enums(capsys, monkeypatch):
    class Pet(IntEnum):
        dog = 0
        cat = 1
        bird = 2

    class Cfg(BaseSettings):
        pet: Pet = Pet.dog
        union_pet: Union[Pet, int] = 43

    cfg = CliApp.run(Cfg, cli_args=['--pet', 'cat', '--union_pet', 'dog'])
    assert cfg.model_dump() == {'pet': Pet.cat, 'union_pet': Pet.dog}

    with pytest.raises(ValidationError) as exc_info:
        CliApp.run(Cfg, cli_args=['--pet', 'rock'])
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'enum',
            'loc': ('pet',),
            'msg': 'Input should be 0, 1 or 2',
            'input': 'rock',
            'ctx': {'expected': '0, 1 or 2'},
        }
    ]

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Cfg)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--pet {{dog,cat,bird}}]
                  [--union_pet {{{{dog,cat,bird}},int}}]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit
  --pet {{dog,cat,bird}}  (default: dog)
  --union_pet {{{{dog,cat,bird}},int}}
                        (default: 43)
"""
        )


def test_cli_literals():
    class Cfg(BaseSettings):
        pet: Literal['dog', 'cat', 'bird']

    cfg = CliApp.run(Cfg, cli_args=['--pet', 'cat'])
    assert cfg.model_dump() == {'pet': 'cat'}

    with pytest.raises(ValidationError) as exc_info:
        CliApp.run(Cfg, cli_args=['--pet', 'rock'])
    assert exc_info.value.errors(include_url=False) == [
        {
            'ctx': {'expected': "'dog', 'cat' or 'bird'"},
            'type': 'literal_error',
            'loc': ('pet',),
            'msg': "Input should be 'dog', 'cat' or 'bird'",
            'input': 'rock',
        }
    ]


def test_cli_annotation_exceptions(monkeypatch):
    class SubCmdAlt(BaseModel):
        pass

    class SubCmd(BaseModel):
        pass

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(
            SettingsError, match='CliSubCommand is not outermost annotation for SubCommandNotOutermost.subcmd'
        ):

            class SubCommandNotOutermost(BaseSettings, cli_parse_args=True):
                subcmd: Union[int, CliSubCommand[SubCmd]]

            SubCommandNotOutermost()

        with pytest.raises(SettingsError, match='subcommand argument SubCommandHasDefault.subcmd has a default value'):

            class SubCommandHasDefault(BaseSettings, cli_parse_args=True):
                subcmd: CliSubCommand[SubCmd] = SubCmd()

            SubCommandHasDefault()

        with pytest.raises(
            SettingsError,
            match='subcommand argument SubCommandMultipleTypes.subcmd has type not derived from BaseModel',
        ):

            class SubCommandMultipleTypes(BaseSettings, cli_parse_args=True):
                subcmd: CliSubCommand[Union[SubCmd, str]]

            SubCommandMultipleTypes()

        with pytest.raises(
            SettingsError, match='subcommand argument SubCommandNotModel.subcmd has type not derived from BaseModel'
        ):

            class SubCommandNotModel(BaseSettings, cli_parse_args=True):
                subcmd: CliSubCommand[str]

            SubCommandNotModel()

        with pytest.raises(
            SettingsError, match='CliPositionalArg is not outermost annotation for PositionalArgNotOutermost.pos_arg'
        ):

            class PositionalArgNotOutermost(BaseSettings, cli_parse_args=True):
                pos_arg: Union[int, CliPositionalArg[str]]

            PositionalArgNotOutermost()

        with pytest.raises(
            SettingsError,
            match='MultipleVariadicPositionialArgs has multiple variadic positonal arguments: strings, numbers',
        ):

            class MultipleVariadicPositionialArgs(BaseSettings, cli_parse_args=True):
                strings: CliPositionalArg[List[str]]
                numbers: CliPositionalArg[List[int]]

            MultipleVariadicPositionialArgs()

        with pytest.raises(
            SettingsError,
            match='VariadicPositionialArgAndSubCommand has variadic positonal arguments and subcommand arguments: strings, sub_cmd',
        ):

            class VariadicPositionialArgAndSubCommand(BaseSettings, cli_parse_args=True):
                strings: CliPositionalArg[List[str]]
                sub_cmd: CliSubCommand[SubCmd]

            VariadicPositionialArgAndSubCommand()

    with pytest.raises(
        SettingsError, match=re.escape("cli_parse_args must be List[str] or Tuple[str, ...], recieved <class 'str'>")
    ):

        class InvalidCliParseArgsType(BaseSettings, cli_parse_args='invalid type'):
            val: int

        InvalidCliParseArgsType()

    with pytest.raises(SettingsError, match='CliExplicitFlag argument CliFlagNotBool.flag is not of type bool'):

        class CliFlagNotBool(BaseSettings, cli_parse_args=True):
            flag: CliExplicitFlag[int] = False

        CliFlagNotBool()

    if sys.version_info < (3, 9):
        with pytest.raises(
            SettingsError,
            match='CliImplicitFlag argument CliFlag38NotOpt.flag must have default for python versions < 3.9',
        ):

            class CliFlag38NotOpt(BaseSettings, cli_parse_args=True):
                flag: CliImplicitFlag[bool]

            CliFlag38NotOpt()


@pytest.mark.parametrize('enforce_required', [True, False])
def test_cli_bool_flags(monkeypatch, enforce_required):
    if sys.version_info < (3, 9):

        class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required):
            explicit_req: bool
            explicit_opt: bool = False
            implicit_opt: CliImplicitFlag[bool] = False

        class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required):
            explicit_req: bool
            explicit_opt: CliExplicitFlag[bool] = False
            implicit_opt: bool = False

        expected = {
            'explicit_req': True,
            'explicit_opt': False,
            'implicit_opt': False,
        }

        assert CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True']).model_dump() == expected
        assert CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True']).model_dump() == expected
    else:

        class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required):
            explicit_req: bool
            explicit_opt: bool = False
            implicit_req: CliImplicitFlag[bool]
            implicit_opt: CliImplicitFlag[bool] = False

        class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required):
            explicit_req: CliExplicitFlag[bool]
            explicit_opt: CliExplicitFlag[bool] = False
            implicit_req: bool
            implicit_opt: bool = False

        expected = {
            'explicit_req': True,
            'explicit_opt': False,
            'implicit_req': True,
            'implicit_opt': False,
        }

        assert CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected
        assert CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']).model_dump() == expected


def test_cli_avoid_json(capsys, monkeypatch):
    class SubModel(BaseModel):
        v1: int

    class Settings(BaseSettings):
        sub_model: SubModel

        model_config = SettingsConfigDict(cli_parse_args=True)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Settings(_cli_avoid_json=False)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help          show this help message and exit

sub_model options:
  --sub_model JSON    set sub_model from JSON string
  --sub_model.v1 int  (required)
"""
        )

        with pytest.raises(SystemExit):
            Settings(_cli_avoid_json=True)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--sub_model.v1 int]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help          show this help message and exit

sub_model options:
  --sub_model.v1 int  (required)
"""
        )


def test_cli_remove_empty_groups(capsys, monkeypatch):
    class SubModel(BaseModel):
        pass

    class Settings(BaseSettings):
        sub_model: SubModel

        model_config = SettingsConfigDict(cli_parse_args=True)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Settings(_cli_avoid_json=False)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--sub_model JSON]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help        show this help message and exit

sub_model options:
  --sub_model JSON  set sub_model from JSON string
"""
        )

        with pytest.raises(SystemExit):
            Settings(_cli_avoid_json=True)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
"""
        )


def test_cli_hide_none_type(capsys, monkeypatch):
    class Settings(BaseSettings):
        v0: Optional[str]

        model_config = SettingsConfigDict(cli_parse_args=True)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Settings(_cli_hide_none_type=False)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--v0 {{str,null}}]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help       show this help message and exit
  --v0 {{str,null}}  (required)
"""
        )

        with pytest.raises(SystemExit):
            Settings(_cli_hide_none_type=True)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--v0 str]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
  --v0 str    (required)
"""
        )


def test_cli_use_class_docs_for_groups(capsys, monkeypatch):
    class SubModel(BaseModel):
        """The help text from the class docstring"""

        v1: int

    class Settings(BaseSettings):
        """My application help text."""

        sub_model: SubModel = Field(description='The help text from the field description')

        model_config = SettingsConfigDict(cli_parse_args=True)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            Settings(_cli_use_class_docs_for_groups=False)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int]

My application help text.

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help          show this help message and exit

sub_model options:
  The help text from the field description

  --sub_model JSON    set sub_model from JSON string
  --sub_model.v1 int  (required)
"""
        )

        with pytest.raises(SystemExit):
            Settings(_cli_use_class_docs_for_groups=True)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] [--sub_model JSON] [--sub_model.v1 int]

My application help text.

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help          show this help message and exit

sub_model options:
  The help text from the class docstring

  --sub_model JSON    set sub_model from JSON string
  --sub_model.v1 int  (required)
"""
        )


def test_cli_enforce_required(env):
    class Settings(BaseSettings, cli_exit_on_error=False):
        my_required_field: str

    env.set('MY_REQUIRED_FIELD', 'hello from environment')

    assert Settings(_cli_parse_args=[], _cli_enforce_required=False).model_dump() == {
        'my_required_field': 'hello from environment'
    }

    with pytest.raises(
        SettingsError, match='error parsing CLI: the following arguments are required: --my_required_field'
    ):
        Settings(_cli_parse_args=[], _cli_enforce_required=True).model_dump()


def test_cli_exit_on_error(capsys, monkeypatch):
    class Settings(BaseSettings, cli_parse_args=True): ...

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--bad-arg'])

        with pytest.raises(SystemExit):
            Settings()
        assert (
            capsys.readouterr().err
            == """usage: example.py [-h]
example.py: error: unrecognized arguments: --bad-arg
"""
        )

        with pytest.raises(SettingsError, match='error parsing CLI: unrecognized arguments: --bad-arg'):
            CliApp.run(Settings, cli_exit_on_error=False)


def test_cli_ignore_unknown_args():
    class Cfg(BaseSettings, cli_ignore_unknown_args=True):
        this: str = 'hello'
        that: int = 123

    cfg = CliApp.run(Cfg, cli_args=['not_my_positional_arg', '--not-my-optional-arg=456'])
    assert cfg.model_dump() == {'this': 'hello', 'that': 123}

    cfg = CliApp.run(
        Cfg, cli_args=['not_my_positional_arg', '--not-my-optional-arg=456', '--this=goodbye', '--that=789']
    )
    assert cfg.model_dump() == {'this': 'goodbye', 'that': 789}


def test_cli_flag_prefix_char():
    class Cfg(BaseSettings, cli_flag_prefix_char='+'):
        my_var: str = Field(validation_alias=AliasChoices('m', 'my-var'))

    cfg = CliApp.run(Cfg, cli_args=['++my-var=hello'])
    assert cfg.model_dump() == {'my_var': 'hello'}

    cfg = CliApp.run(Cfg, cli_args=['+m=hello'])
    assert cfg.model_dump() == {'my_var': 'hello'}


@pytest.mark.parametrize('parser_type', [pytest.Parser, argparse.ArgumentParser, CliDummyParser])
@pytest.mark.parametrize('prefix', ['', 'cfg'])
def test_cli_user_settings_source(parser_type, prefix):
    class Cfg(BaseSettings):
        pet: Literal['dog', 'cat', 'bird'] = 'bird'

    if parser_type is pytest.Parser:
        parser = pytest.Parser(_ispytest=True)
        parse_args = parser.parse
        add_arg = parser.addoption
        cli_cfg_settings = CliSettingsSource(
            Cfg,
            cli_prefix=prefix,
            root_parser=parser,
            parse_args_method=pytest.Parser.parse,
            add_argument_method=pytest.Parser.addoption,
            add_argument_group_method=pytest.Parser.getgroup,
            add_parser_method=None,
            add_subparsers_method=None,
            formatter_class=None,
        )
    elif parser_type is CliDummyParser:
        parser = CliDummyParser()
        parse_args = parser.parse_args
        add_arg = parser.add_argument
        cli_cfg_settings = CliSettingsSource(
            Cfg,
            cli_prefix=prefix,
            root_parser=parser,
            parse_args_method=CliDummyParser.parse_args,
            add_argument_method=CliDummyParser.add_argument,
            add_argument_group_method=CliDummyParser.add_argument_group,
            add_parser_method=CliDummySubParsers.add_parser,
            add_subparsers_method=CliDummyParser.add_subparsers,
        )
    else:
        parser = argparse.ArgumentParser()
        parse_args = parser.parse_args
        add_arg = parser.add_argument
        cli_cfg_settings = CliSettingsSource(Cfg, cli_prefix=prefix, root_parser=parser)

    add_arg('--fruit', choices=['pear', 'kiwi', 'lime'])
    add_arg('--num-list', action='append', type=int)
    add_arg('--num', type=int)

    args = ['--fruit', 'pear', '--num', '0', '--num-list', '1', '--num-list', '2', '--num-list', '3']
    parsed_args = parse_args(args)
    assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'bird'}
    assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'bird'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'bird'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'bird'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}

    arg_prefix = f'{prefix}.' if prefix else ''
    args = [
        '--fruit',
        'kiwi',
        '--num',
        '0',
        '--num-list',
        '1',
        '--num-list',
        '2',
        '--num-list',
        '3',
        f'--{arg_prefix}pet',
        'dog',
    ]
    parsed_args = parse_args(args)
    assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'dog'}
    assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {'pet': 'dog'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'dog'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'dog'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}

    parsed_args = parse_args(
        [
            '--fruit',
            'kiwi',
            '--num',
            '0',
            '--num-list',
            '1',
            '--num-list',
            '2',
            '--num-list',
            '3',
            f'--{arg_prefix}pet',
            'cat',
        ]
    )
    assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'cat'
    }
    assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=vars(parsed_args))).model_dump() == {'pet': 'cat'}
    assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}


@pytest.mark.parametrize('prefix', ['', 'cfg'])
def test_cli_dummy_user_settings_with_subcommand(prefix):
    class DogCommands(BaseModel):
        name: str = 'Bob'
        command: Literal['roll', 'bark', 'sit'] = 'sit'

    class Cfg(BaseSettings):
        pet: Literal['dog', 'cat', 'bird'] = 'bird'
        command: CliSubCommand[DogCommands]

    parser = CliDummyParser()
    cli_cfg_settings = CliSettingsSource(
        Cfg,
        root_parser=parser,
        cli_prefix=prefix,
        parse_args_method=CliDummyParser.parse_args,
        add_argument_method=CliDummyParser.add_argument,
        add_argument_group_method=CliDummyParser.add_argument_group,
        add_parser_method=CliDummySubParsers.add_parser,
        add_subparsers_method=CliDummyParser.add_subparsers,
    )

    parser.add_argument('--fruit', choices=['pear', 'kiwi', 'lime'])

    args = ['--fruit', 'pear']
    parsed_args = parser.parse_args(args)
    assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'bird',
        'command': None,
    }
    assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'bird',
        'command': None,
    }

    arg_prefix = f'{prefix}.' if prefix else ''
    args = ['--fruit', 'kiwi', f'--{arg_prefix}pet', 'dog']
    parsed_args = parser.parse_args(args)
    assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'dog',
        'command': None,
    }
    assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'dog',
        'command': None,
    }

    parsed_args = parser.parse_args(['--fruit', 'kiwi', f'--{arg_prefix}pet', 'cat'])
    assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'cat',
        'command': None,
    }

    args = ['--fruit', 'kiwi', f'--{arg_prefix}pet', 'dog', 'command', '--name', 'ralph', '--command', 'roll']
    parsed_args = parser.parse_args(args)
    assert CliApp.run(Cfg, cli_args=vars(parsed_args), cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'dog',
        'command': {'name': 'ralph', 'command': 'roll'},
    }
    assert CliApp.run(Cfg, cli_args=args, cli_settings_source=cli_cfg_settings).model_dump() == {
        'pet': 'dog',
        'command': {'name': 'ralph', 'command': 'roll'},
    }


def test_cli_user_settings_source_exceptions():
    class Cfg(BaseSettings):
        pet: Literal['dog', 'cat', 'bird'] = 'bird'

    with pytest.raises(SettingsError, match='`args` and `parsed_args` are mutually exclusive'):
        args = ['--pet', 'dog']
        parsed_args = {'pet': 'dog'}
        cli_cfg_settings = CliSettingsSource(Cfg)
        Cfg(_cli_settings_source=cli_cfg_settings(args=args, parsed_args=parsed_args))

    with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: .cfg'):
        CliSettingsSource(Cfg, cli_prefix='.cfg')

    with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: cfg.'):
        CliSettingsSource(Cfg, cli_prefix='cfg.')

    with pytest.raises(SettingsError, match='CLI settings source prefix is invalid: 123'):
        CliSettingsSource(Cfg, cli_prefix='123')

    class Food(BaseModel):
        fruit: FruitsEnum = FruitsEnum.kiwi

    class CfgWithSubCommand(BaseSettings):
        pet: Literal['dog', 'cat', 'bird'] = 'bird'
        food: CliSubCommand[Food]

    with pytest.raises(
        SettingsError,
        match='cannot connect CLI settings source root parser: add_subparsers_method is set to `None` but is needed for connecting',
    ):
        CliSettingsSource(CfgWithSubCommand, add_subparsers_method=None)


@pytest.mark.parametrize(
    'value,expected',
    [
        (str, 'str'),
        ('foobar', 'str'),
        ('SomeForwardRefString', 'str'),  # included to document current behavior; could be changed
        (List['SomeForwardRef'], "List[ForwardRef('SomeForwardRef')]"),  # noqa: F821
        (Union[str, int], '{str,int}'),
        (list, 'list'),
        (List, 'List'),
        ([1, 2, 3], 'list'),
        (List[Dict[str, int]], 'List[Dict[str,int]]'),
        (Tuple[str, int, float], 'Tuple[str,int,float]'),
        (Tuple[str, ...], 'Tuple[str,...]'),
        (Union[int, List[str], Tuple[str, int]], '{int,List[str],Tuple[str,int]}'),
        (foobar, 'foobar'),
        (LoggedVar, 'LoggedVar'),
        (LoggedVar(), 'LoggedVar'),
        (Representation(), 'Representation()'),
        (typing.Literal[1, 2, 3], '{1,2,3}'),
        (typing_extensions.Literal[1, 2, 3], '{1,2,3}'),
        (typing.Literal['a', 'b', 'c'], '{a,b,c}'),
        (typing_extensions.Literal['a', 'b', 'c'], '{a,b,c}'),
        (SimpleSettings, 'JSON'),
        (Union[SimpleSettings, SettingWithIgnoreEmpty], 'JSON'),
        (Union[SimpleSettings, str, SettingWithIgnoreEmpty], '{JSON,str}'),
        (Union[str, SimpleSettings, SettingWithIgnoreEmpty], '{str,JSON}'),
        (Annotated[SimpleSettings, 'annotation'], 'JSON'),
        (DirectoryPath, 'Path'),
        (FruitsEnum, '{pear,kiwi,lime}'),
        (time.time_ns, 'time_ns'),
        (foobar, 'foobar'),
        (CliDummyParser.add_argument, 'CliDummyParser.add_argument'),
    ],
)
@pytest.mark.parametrize('hide_none_type', [True, False])
def test_cli_metavar_format(hide_none_type, value, expected):
    cli_settings = CliSettingsSource(SimpleSettings, cli_hide_none_type=hide_none_type)
    if hide_none_type:
        if value == [1, 2, 3] or isinstance(value, LoggedVar) or isinstance(value, Representation):
            pytest.skip()
        if value in ('foobar', 'SomeForwardRefString'):
            expected = f"ForwardRef('{value}')"  # forward ref implicit cast
        if typing_extensions.get_origin(value) is Union:
            args = typing_extensions.get_args(value)
            value = Union[args + (None,) if args else (value, None)]
        else:
            value = Union[(value, None)]
    assert cli_settings._metavar_format(value) == expected


@pytest.mark.skipif(sys.version_info < (3, 10), reason='requires python 3.10 or higher')
@pytest.mark.parametrize(
    'value_gen,expected',
    [
        (lambda: str | int, '{str,int}'),
        (lambda: list[int], 'list[int]'),
        (lambda: List[int], 'List[int]'),
        (lambda: list[dict[str, int]], 'list[dict[str,int]]'),
        (lambda: list[Union[str, int]], 'list[{str,int}]'),
        (lambda: list[str | int], 'list[{str,int}]'),
        (lambda: LoggedVar[int], 'LoggedVar[int]'),
        (lambda: LoggedVar[Dict[int, str]], 'LoggedVar[Dict[int,str]]'),
    ],
)
@pytest.mark.parametrize('hide_none_type', [True, False])
def test_cli_metavar_format_310(hide_none_type, value_gen, expected):
    value = value_gen()
    cli_settings = CliSettingsSource(SimpleSettings, cli_hide_none_type=hide_none_type)
    if hide_none_type:
        if typing_extensions.get_origin(value) is Union:
            args = typing_extensions.get_args(value)
            value = Union[args + (None,) if args else (value, None)]
        else:
            value = Union[(value, None)]
    assert cli_settings._metavar_format(value) == expected


@pytest.mark.skipif(sys.version_info < (3, 12), reason='requires python 3.12 or higher')
def test_cli_metavar_format_type_alias_312():
    exec(
        """
type TypeAliasInt = int
assert CliSettingsSource(SimpleSettings)._metavar_format(TypeAliasInt) == 'TypeAliasInt'
"""
    )


def test_cli_app():
    class Init(BaseModel):
        directory: CliPositionalArg[str]

        def cli_cmd(self) -> None:
            self.directory = 'ran Init.cli_cmd'

        def alt_cmd(self) -> None:
            self.directory = 'ran Init.alt_cmd'

    class Clone(BaseModel):
        repository: CliPositionalArg[str]
        directory: CliPositionalArg[str]

        def cli_cmd(self) -> None:
            self.repository = 'ran Clone.cli_cmd'

        def alt_cmd(self) -> None:
            self.repository = 'ran Clone.alt_cmd'

    class Git(BaseModel):
        clone: CliSubCommand[Clone]
        init: CliSubCommand[Init]

        def cli_cmd(self) -> None:
            CliApp.run_subcommand(self)

        def alt_cmd(self) -> None:
            CliApp.run_subcommand(self, cli_cmd_method_name='alt_cmd')

    assert CliApp.run(Git, cli_args=['init', 'dir']).model_dump() == {
        'clone': None,
        'init': {'directory': 'ran Init.cli_cmd'},
    }
    assert CliApp.run(Git, cli_args=['init', 'dir'], cli_cmd_method_name='alt_cmd').model_dump() == {
        'clone': None,
        'init': {'directory': 'ran Init.alt_cmd'},
    }
    assert CliApp.run(Git, cli_args=['clone', 'repo', 'dir']).model_dump() == {
        'clone': {'repository': 'ran Clone.cli_cmd', 'directory': 'dir'},
        'init': None,
    }
    assert CliApp.run(Git, cli_args=['clone', 'repo', 'dir'], cli_cmd_method_name='alt_cmd').model_dump() == {
        'clone': {'repository': 'ran Clone.alt_cmd', 'directory': 'dir'},
        'init': None,
    }


def test_cli_app_async_method_no_existing_loop():
    class Command(BaseSettings):
        called: bool = False

        async def cli_cmd(self) -> None:
            self.called = True

    assert CliApp.run(Command, cli_args=[]).called


def test_cli_app_async_method_with_existing_loop():
    class Command(BaseSettings):
        called: bool = False

        async def cli_cmd(self) -> None:
            self.called = True

    async def run_as_coro():
        return CliApp.run(Command, cli_args=[])

    assert asyncio.run(run_as_coro()).called


def test_cli_app_exceptions():
    with pytest.raises(
        SettingsError, match='Error: NotPydanticModel is not subclass of BaseModel or pydantic.dataclasses.dataclass'
    ):

        class NotPydanticModel: ...

        CliApp.run(NotPydanticModel)

    with pytest.raises(
        SettingsError,
        match=re.escape('Error: `cli_args` must be list[str] or None when `cli_settings_source` is not used'),
    ):

        class Cfg(BaseModel): ...

        CliApp.run(Cfg, cli_args={'my_arg': 'hello'})

    with pytest.raises(SettingsError, match='Error: Child class is missing cli_cmd entrypoint'):

        class Child(BaseModel):
            val: str

        class Root(BaseModel):
            child: CliSubCommand[Child]

            def cli_cmd(self) -> None:
                CliApp.run_subcommand(self)

        CliApp.run(Root, cli_args=['child', '--val=hello'])


def test_cli_suppress(capsys, monkeypatch):
    class Settings(BaseSettings, cli_parse_args=True):
        field_a: CliSuppress[int] = 0
        field_b: str = Field(default=1, description=CLI_SUPPRESS)

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])

        with pytest.raises(SystemExit):
            CliApp.run(Settings)

        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h]

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help  show this help message and exit
"""
        )


def test_cli_mutually_exclusive_group(capsys, monkeypatch):
    class Circle(CliMutuallyExclusiveGroup):
        radius: Optional[float] = 21
        diameter: Optional[float] = 22
        perimeter: Optional[float] = 23

    class Settings(BaseModel):
        circle_optional: Circle = Circle(radius=None, diameter=None, perimeter=24)
        circle_required: Circle

    CliApp.run(Settings, cli_args=['--circle-required.radius=1', '--circle-optional.radius=1']).model_dump() == {
        'circle_optional': {'radius': 1, 'diameter': 22, 'perimeter': 24},
        'circle_required': {'radius': 1, 'diameter': 22, 'perimeter': 23},
    }

    with pytest.raises(SystemExit):
        CliApp.run(Settings, cli_args=['--circle-required.radius=1', '--circle-required.diameter=2'])
    assert (
        'error: argument --circle-required.diameter: not allowed with argument --circle-required.radius'
        in capsys.readouterr().err
    )

    with pytest.raises(SystemExit):
        CliApp.run(
            Settings,
            cli_args=['--circle-required.radius=1', '--circle-optional.radius=1', '--circle-optional.diameter=2'],
        )
    assert (
        'error: argument --circle-optional.diameter: not allowed with argument --circle-optional.radius'
        in capsys.readouterr().err
    )

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])
        with pytest.raises(SystemExit):
            CliApp.run(Settings)
        usage = (
            """usage: example.py [-h] [--circle-optional.radius float |
                  --circle-optional.diameter float |
                  --circle-optional.perimeter float]
                  (--circle-required.radius float |
                  --circle-required.diameter float |
                  --circle-required.perimeter float)"""
            if sys.version_info >= (3, 13)
            else """usage: example.py [-h]
                  [--circle-optional.radius float | --circle-optional.diameter float | --circle-optional.perimeter float]
                  (--circle-required.radius float | --circle-required.diameter float | --circle-required.perimeter float)"""
        )
        assert (
            capsys.readouterr().out
            == f"""{usage}

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help            show this help message and exit

circle-optional options (mutually exclusive):
  --circle-optional.radius float
                        (default: None)
  --circle-optional.diameter float
                        (default: None)
  --circle-optional.perimeter float
                        (default: 24.0)

circle-required options (mutually exclusive):
  --circle-required.radius float
                        (default: 21)
  --circle-required.diameter float
                        (default: 22)
  --circle-required.perimeter float
                        (default: 23)
"""
        )


def test_cli_mutually_exclusive_group_exceptions():
    class Circle(CliMutuallyExclusiveGroup):
        radius: Optional[float] = 21
        diameter: Optional[float] = 22
        perimeter: Optional[float] = 23

    class Settings(BaseSettings):
        circle: Circle

    parser = CliDummyParser()
    with pytest.raises(
        SettingsError,
        match='cannot connect CLI settings source root parser: group object is missing add_mutually_exclusive_group but is needed for connecting',
    ):
        CliSettingsSource(
            Settings,
            root_parser=parser,
            parse_args_method=CliDummyParser.parse_args,
            add_argument_method=CliDummyParser.add_argument,
            add_argument_group_method=CliDummyParser.add_argument_group,
            add_parser_method=CliDummySubParsers.add_parser,
            add_subparsers_method=CliDummyParser.add_subparsers,
        )

    class SubModel(BaseModel):
        pass

    class SettingsInvalidUnion(BaseSettings):
        union: Union[Circle, SubModel]

    with pytest.raises(SettingsError, match='cannot use union with CliMutuallyExclusiveGroup'):
        CliApp.run(SettingsInvalidUnion)

    class CircleInvalidSubModel(Circle):
        square: Optional[SubModel] = None

    class SettingsInvalidOptSubModel(BaseModel):
        circle: CircleInvalidSubModel = CircleInvalidSubModel()

    class SettingsInvalidReqSubModel(BaseModel):
        circle: CircleInvalidSubModel

    for settings in [SettingsInvalidOptSubModel, SettingsInvalidReqSubModel]:
        with pytest.raises(SettingsError, match='cannot have nested models in a CliMutuallyExclusiveGroup'):
            CliApp.run(settings)

    class CircleRequiredField(Circle):
        length: float

    class SettingsOptCircleReqField(BaseModel):
        circle: CircleRequiredField = CircleRequiredField(length=2)

    assert CliApp.run(SettingsOptCircleReqField, cli_args=[]).model_dump() == {
        'circle': {'diameter': 22.0, 'length': 2.0, 'perimeter': 23.0, 'radius': 21.0}
    }

    class SettingsInvalidReqCircleReqField(BaseModel):
        circle: CircleRequiredField

    with pytest.raises(ValueError, match='mutually exclusive arguments must be optional'):
        CliApp.run(SettingsInvalidReqCircleReqField)


def test_cli_invalid_abbrev():
    class MySettings(BaseSettings):
        bacon: str = ''
        badger: str = ''

    with pytest.raises(
        SettingsError,
        match='error parsing CLI: unrecognized arguments: --bac cli abbrev are invalid for internal parser',
    ):
        CliApp.run(
            MySettings, cli_args=['--bac', 'cli abbrev are invalid for internal parser'], cli_exit_on_error=False
        )


def test_cli_submodels_strip_annotated():
    class PolyA(BaseModel):
        a: int = 1
        type: Literal['a'] = 'a'

    class PolyB(BaseModel):
        b: str = '2'
        type: Literal['b'] = 'b'

    def _get_type(model: Union[BaseModel, Dict]) -> str:
        if isinstance(model, dict):
            return model.get('type', 'a')
        return model.type  # type: ignore

    Poly = Annotated[Union[Annotated[PolyA, Tag('a')], Annotated[PolyB, Tag('b')]], Discriminator(_get_type)]

    class WithUnion(BaseSettings):
        poly: Poly

    assert CliApp.run(WithUnion, ['--poly.type=a']).model_dump() == {'poly': {'a': 1, 'type': 'a'}}


def test_cli_kebab_case(capsys, monkeypatch):
    class DeepSubModel(BaseModel):
        deep_pos_arg: CliPositionalArg[str]
        deep_arg: str

    class SubModel(BaseModel):
        sub_subcmd: CliSubCommand[DeepSubModel]
        sub_arg: str

    class Root(BaseModel):
        root_subcmd: CliSubCommand[SubModel]
        root_arg: str

    assert CliApp.run(
        Root,
        cli_args=[
            '--root-arg=hi',
            'root-subcmd',
            '--sub-arg=hello',
            'sub-subcmd',
            'hey',
            '--deep-arg=bye',
        ],
    ).model_dump() == {
        'root_arg': 'hi',
        'root_subcmd': {
            'sub_arg': 'hello',
            'sub_subcmd': {'deep_pos_arg': 'hey', 'deep_arg': 'bye'},
        },
    }

    with monkeypatch.context() as m:
        m.setattr(sys, 'argv', ['example.py', '--help'])
        with pytest.raises(SystemExit):
            CliApp.run(Root)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py [-h] --root-arg str {{root-subcmd}} ...

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help      show this help message and exit
  --root-arg str  (required)

subcommands:
  {{root-subcmd}}
    root-subcmd
"""
        )

        m.setattr(sys, 'argv', ['example.py', 'root-subcmd', '--help'])
        with pytest.raises(SystemExit):
            CliApp.run(Root)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py root-subcmd [-h] --sub-arg str {{sub-subcmd}} ...

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help     show this help message and exit
  --sub-arg str  (required)

subcommands:
  {{sub-subcmd}}
    sub-subcmd
"""
        )

        m.setattr(sys, 'argv', ['example.py', 'root-subcmd', 'sub-subcmd', '--help'])
        with pytest.raises(SystemExit):
            CliApp.run(Root)
        assert (
            capsys.readouterr().out
            == f"""usage: example.py root-subcmd sub-subcmd [-h] --deep-arg str DEEP-POS-ARG

positional arguments:
  DEEP-POS-ARG

{ARGPARSE_OPTIONS_TEXT}:
  -h, --help      show this help message and exit
  --deep-arg str  (required)
"""
        )
