from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Type

import draccus
from draccus import ParsingError
from draccus.utils import DecodingError

from .testutils import TestSetup, parametrize, raises


def test_basic_required_argument(simple_attribute, silent):
    some_type, passed_value, expected_value = simple_attribute

    @dataclass
    class SomeDataclass(TestSetup):
        some_attribute: some_type  # type: ignore

    actual = SomeDataclass.setup(f"--some_attribute {passed_value}")
    assert actual.some_attribute == expected_value
    assert isinstance(actual.some_attribute, some_type)


def test_not_passing_required_argument_raises_error(simple_attribute):
    some_type, passed_value, expected_value = simple_attribute

    @dataclass
    class SomeDataclass(TestSetup):
        some_attribute: some_type  # type: ignore

    with raises(DecodingError):
        _ = SomeDataclass.setup("")


def test_basic_optional_argument(simple_attribute, silent):
    some_type, _, expected_value = simple_attribute

    @dataclass
    class SomeDataclass(TestSetup):
        some_attribute: some_type = expected_value  # type: ignore

    actual = SomeDataclass.setup("")
    assert actual.some_attribute == expected_value
    assert isinstance(actual.some_attribute, some_type)


@parametrize(
    "some_type, default_value,  arg_value",
    [
        (int, 0, 1234),
        (float, 0.0, 123.456),
        (str, "", "bobby_boots"),
        (bool, False, True),
    ],
)
def test_arg_value_is_set_when_args_are_provided(some_type: Type, default_value: Any, arg_value: Any):
    @dataclass
    class SomeClass(TestSetup):
        a: some_type = default_value  # type: ignore
        """some docstring for attribute 'a'"""

    class_a = SomeClass.setup(f"--a {arg_value}")
    assert class_a.a != default_value
    assert class_a.a == arg_value
    assert isinstance(class_a.a, some_type)


@parametrize(
    "some_type",
    [
        int,
        float,
        str,
        bool,
    ],
)
def test_not_providing_required_argument_throws_error(some_type):
    @dataclass
    class SomeClass(TestSetup):
        a: some_type  # type: ignore
        """some docstring for attribute 'a'"""

    with raises(ParsingError):
        _ = SomeClass.setup("")


@parametrize("some_type", [int, float, str])
def test_not_providing_required_argument_name_but_no_value_throws_error(some_type):
    @dataclass
    class SomeClass(TestSetup):
        a: some_type  # type: ignore
        """some docstring for attribute 'a'"""

    with raises(SystemExit):
        _ = SomeClass.setup("--a")


class Color(Enum):
    RED = "RED"
    ORANGE = "ORANGE"
    BLUE = "BLUE"


@dataclass
class Base(TestSetup):
    """A simple base-class example"""

    a: int  # TODO: finetune this
    """docstring for attribute 'a'"""
    b: float = 5.0  # inline comment on attribute 'b'
    c: str = ""


@dataclass
class Extended(Base):
    """Some extension of base-class `Base`"""

    d: int = 5
    """ docstring for 'd' in Extended. """
    e: Color = Color.BLUE


def test_parse_base_simple_works():
    b = Base.setup("--a 10 --b 3 --c Hello")
    assert b.a == 10
    assert b.b == 3
    assert b.c == "Hello"


def test_help_displays_class_docstring_text():
    assert Base.__doc__ in Base.get_help_text()


def test_enum_attributes_work():
    ext = Extended.setup("--a 5 --e RED")
    assert ext.e == Color.RED

    ext = Extended.setup("--a 5")
    assert ext.e == Color.BLUE


def test_parsing_twice():
    @dataclass
    class Foo:
        a: int = 123

    cfg = draccus.parse(config_class=Foo, args="")
    assert cfg.a == 123
    cfg = draccus.parse(config_class=Foo, args="--a 456".split())
    assert cfg.a == 456


def test_using_a_Type_type():
    @dataclass
    class Base:
        a: str = "a"

    @dataclass
    class Extended(Base):
        a: str = "extended_a"

    @dataclass
    class Foo(TestSetup):
        a_class: Type[Base] = field(default=Base, init=False)
        a: Base = field(default=None, init=False)

        def __post_init__(self):
            self.a = self.a_class()

    foo = Foo.setup("")
    from draccus.utils import contains_dataclass_type_arg

    assert not contains_dataclass_type_arg(Type[Base])
    assert foo.a_class() == Base()

    @dataclass
    class OtherFoo(Foo):
        a_class: Type[Base] = field(default=Extended, init=False)

    foo = OtherFoo.setup("")
    assert foo.a == Extended()


@dataclass(frozen=True)
class SomeClass:
    val: str


def test_argparse_with_custom_parsing():
    @dataclass
    class Config(TestSetup):
        s: SomeClass = SomeClass("bob")

    draccus.encode.register(SomeClass, lambda x: f"custom_{x.val}")
    draccus.decode.register(SomeClass, lambda x: SomeClass(x[7:]))

    Config.setup("--s custom_bob")


def test_empty_required_str():
    @dataclass
    class Args:
        a: str

    cfg = draccus.parse(Args, args=["--a="])

    assert cfg.a == ""


def test_exit_on_error():
    @dataclass
    class Args:
        a: int

    # Note: the cases that actually trigger the SystemExit are unclear.
    # Not all errors do. This could probably be improved.
    with raises(SystemExit):
        draccus.parse(Args, args=["--b=b"], exit_on_error=True)

    with raises(draccus.utils.DraccusException):
        draccus.parse(Args, args=["--b=b"], exit_on_error=False)
