from abc import abstractmethod, ABC

import pytest
from traitlets import Float, TraitError, Int
from traitlets.config.loader import Config
import astropy.units as u
import warnings

from ctapipe.core import Component


def test_non_abstract_children():
    """ check that we can find all constructable children """
    from ctapipe.core import non_abstract_children

    class AbstractBase(ABC):
        @abstractmethod
        def method(self):
            pass

    class Child1(AbstractBase):
        def method(self):
            print("method of Child1")

    class Child2(AbstractBase):
        def method(self):
            print("method of Child2")

    class GrandChild(Child2):
        def method(self):
            print("method of GrandChild")

    class AbstractChild(AbstractBase):
        pass

    kids = non_abstract_children(AbstractBase)
    assert Child1 in kids
    assert Child2 in kids
    assert GrandChild in kids
    assert AbstractChild not in kids


def test_get_config_from_hierarchy():
    from ctapipe.core.component import find_config_in_hierarchy

    class Bottom(Component):
        val = Float(0).tag(config=True)

    class Middle(Component):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.bottom = Bottom(parent=self)

    class Top(Component):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            self.middle = Middle(parent=self)

    # test with root present
    c = Config({"Top": {"Middle": {"Bottom": {"val": 5}}}})
    t = Top(config=c)

    val = find_config_in_hierarchy(t.middle, "Bottom", "val")
    assert val == 5

    # test with root not present
    c = Config({"Middle": {"Bottom": {"val": 5}}})
    t = Top(config=c)

    val = find_config_in_hierarchy(t.middle, "Bottom", "val")
    assert val == 5


class SubComponent(Component):
    """ An Example Component, this is the help text"""

    value = Float(default_value=-1.0, help="float parameter").tag(config=True)


class ExampleComponent(Component):
    """ An Example Component, this is the help text"""

    param = Float(default_value=1.0, help="float parameter").tag(config=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.sub = SubComponent(parent=self)


class ExampleSubclass1(ExampleComponent):
    """ a subclass of ExampleComponent"""

    pass


class ExampleSubclass2(ExampleComponent):
    """ Another ExampleComponent """

    description = "A shorter description"
    param = Float(default_value=3.0, help="float parameter").tag(config=True)
    extra = Float(default_value=5.0, help="float parameter").tag(config=True)


def test_component_is_abstract():
    """ check that we can make an abstract component """

    class AbstractComponent(Component):
        @abstractmethod
        def test(self):
            pass

    with pytest.raises(TypeError):
        AbstractComponent()


def test_component_simple():
    """
    very basic test to construct a component and check
    that it's traits work correctly
    """
    comp = ExampleComponent()

    assert comp.has_trait("param") is True
    comp.param = 1.2

    with pytest.raises(TraitError):
        comp.param = "badvalue"


def test_component_kwarg_setting():
    """ check that we can construct a component by setting traits via kwargs """
    comp = ExampleComponent(param=3)
    assert comp.param == 3

    # Invalid type
    with pytest.raises(TraitError):
        comp = ExampleComponent(param="badvalue")

    # Invalid traitlet
    with pytest.raises(TraitError):
        # the current traitlets version already warns about this
        # will be raising an error in the future, but we want the error
        # now, filter the warning here to not clutter the log
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=DeprecationWarning)
            comp = ExampleComponent(incorrect="wrong")


def test_help():
    """ check that component help strings are generated correctly """
    help_msg = ExampleComponent.class_get_help()
    assert "Default: 1.0" in help_msg


def test_config():
    """ check that components can be constructed by config dict """
    config = Config()
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    comp = ExampleComponent(config=config)
    assert comp.param == 199.0


def test_config_baseclass():
    """ check that parent and subclass configuration works """
    config = Config()
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    comp1 = ExampleSubclass1(config=config)
    assert comp1.param == 199.0
    comp2 = ExampleSubclass2(config=config)
    assert comp2.param == 199.0


def test_config_subclass1():
    """check sub-class config"""
    config = Config()
    config["ExampleSubclass1"] = Config()
    config["ExampleSubclass1"]["param"] = 199.0
    comp = ExampleComponent(config=config)
    assert comp.param == 1.0


def test_config_subclass2():
    """check another sub-class config"""
    config = Config()
    config["ExampleSubclass2"] = Config()
    config["ExampleSubclass2"]["param"] = 199.0
    comp = ExampleComponent(config=config)
    assert comp.param == 1.0


def test_config_sibling1():
    """ check sibling config """
    config = Config()
    config["ExampleSubclass1"] = Config()
    config["ExampleSubclass1"]["param"] = 199.0
    comp1 = ExampleSubclass1(config=config)
    assert comp1.param == 199.0
    comp2 = ExampleSubclass2(config=config)
    assert comp2.param == 3.0


def test_config_sibling2():
    """ check sibling config """
    config = Config()
    config["ExampleSubclass2"] = Config()
    config["ExampleSubclass2"]["param"] = 199.0
    comp1 = ExampleSubclass1(config=config)
    assert comp1.param == 1.0
    comp2 = ExampleSubclass2(config=config)
    assert comp2.param == 199.0


def test_config_baseclass_then_subclass():
    """ check base and subclass config """
    config = Config()
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    config["ExampleSubclass1"] = Config()
    config["ExampleSubclass1"]["param"] = 229.0
    comp = ExampleSubclass1(config=config)
    assert comp.param == 229.0


def test_config_subclass_then_baseclass():
    """ check subclass and base config """
    config = Config()
    config["ExampleSubclass1"] = Config()
    config["ExampleSubclass1"]["param"] = 229.0
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    comp = ExampleSubclass1(config=config)
    assert comp.param == 229.0


def test_config_override():
    """ check that we can override a trait set in the config """
    config = Config()
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    comp = ExampleComponent(config=config, param=229.0)
    assert comp.param == 229.0


def test_config_override_subclass():
    """ check that we can override a trait set in the config """
    config = Config()
    config["ExampleComponent"] = Config()
    config["ExampleComponent"]["param"] = 199.0
    comp = ExampleSubclass1(config=config, param=229.0)
    assert comp.param == 229.0


def test_extra():
    """ check that traits are settable """
    comp = ExampleSubclass2(extra=229.0)
    assert comp.has_trait("extra") is True
    assert comp.extra == 229.0


def test_extra_config():
    """ check setting trait via config """
    config = Config()
    config["ExampleSubclass2"] = Config()
    config["ExampleSubclass2"]["extra"] = 229.0
    comp = ExampleSubclass2(config=config)
    assert comp.extra == 229.0


def test_unknown_traitlet_raises():
    """ check that setting an incorrect trait raises an exception """
    with pytest.raises(TraitError):
        # the current traitlets version already warns about this
        # will be raising an error in the future, but we want the error
        # now, filter the warning here to not clutter the log
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=DeprecationWarning)

            ExampleSubclass1(extra=229.0)


def test_extra_config_missing():
    """
    check that setting an incorrect trait via config also raises
    an exception
    """
    config = Config()
    config["ExampleSubclass1"] = Config()
    config["ExampleSubclass1"]["extra"] = 199.0
    with pytest.warns(UserWarning):
        comp = ExampleSubclass1(config=config)
    assert comp.has_trait("extra") is False
    with pytest.raises(AttributeError):
        assert comp.extra == 229.0


def test_default():
    """ check default values work"""
    comp = ExampleComponent()
    assert comp.param == 1.0


def test_default_subclass():
    """ check default values work in subclasses"""
    comp = ExampleSubclass1()
    assert comp.param == 1.0


def test_default_subclass_override():
    """ check overrides work in subclasses"""
    comp = ExampleSubclass2()
    assert comp.param == 3.0


def test_change_default():
    """ check we can change a default value"""
    old_default = ExampleComponent.param.default_value
    ExampleComponent.param.default_value = 199.0
    comp = ExampleComponent()
    assert comp.param == 199.0
    ExampleComponent.param.default_value = old_default


def test_change_default_subclass():
    """ check we can change a default value in subclass """
    old_default = ExampleComponent.param.default_value
    ExampleComponent.param.default_value = 199.0
    comp = ExampleSubclass1()
    assert comp.param == 199.0
    ExampleComponent.param.default_value = old_default


def test_change_default_subclass_override():
    """ check override default value  """
    old_default = ExampleComponent.param.default_value
    ExampleComponent.param.default_value = 199.0
    comp = ExampleSubclass2()
    assert comp.param == 3.0  # No change as it is a seperate traitlet object
    ExampleComponent.param.default_value = old_default


def test_help_changed_default():
    """ check that the help text is updated if the default is changed """
    old_default = ExampleComponent.param.default_value
    ExampleComponent.param.default_value = 199.0
    help_msg = ExampleComponent.class_get_help()
    assert "Default: 199.0" in help_msg
    ExampleComponent.param.default_value = old_default


def test_non_abstract_subclasses():
    """non_abstract_subclasses() is a helper function:
    it might just be nice, in case a person wants to see the subclasses
    in a python session.

    Can also be helpful in parametrized tests, to make sure all
    sublcasses are being tested.
    """
    assert "ExampleSubclass1" in ExampleComponent.non_abstract_subclasses()


def test_from_name():
    """ Make sure one can construct a Component subclass by name"""
    subclass = ExampleComponent.from_name("ExampleSubclass1")
    assert isinstance(subclass, ExampleSubclass1)
    subclass = ExampleComponent.from_name("ExampleSubclass2")
    assert isinstance(subclass, ExampleSubclass2)


def test_from_name_config():
    """ make sure one can construct a Component subclass by name + config"""
    config = Config({"ExampleComponent": {"param": 229.0}})
    subclass = ExampleComponent.from_name("ExampleSubclass1", config=config)
    assert subclass.param == 229.0


def test_component_current_config():
    """ make sure one can get the full current configuration"""
    comp = ExampleComponent()
    full_config = comp.get_current_config()
    assert "ExampleComponent" in full_config
    assert "param" in full_config["ExampleComponent"]
    assert full_config["ExampleComponent"]["param"] == 1.0


def test_component_html_repr():
    """ check the HTML repr for Jupyter notebooks """
    comp = ExampleComponent()
    html = comp._repr_html_()
    assert len(html) > 10


def test_telescope_component():
    from ctapipe.core import TelescopeComponent
    from ctapipe.instrument import SubarrayDescription, TelescopeDescription

    subarray = SubarrayDescription(
        "test",
        tel_positions={1: [0, 0, 0] * u.m},
        tel_descriptions={1: TelescopeDescription.from_name("LST", "LSTCam")},
    )

    class Base(TelescopeComponent):
        pass

    class Sub(Base):
        pass

    assert isinstance(Base.from_name("Sub", subarray=subarray), Sub)


def test_full_config():
    class SubComponent(Component):
        param = Int(default_value=3).tag(config=True)

    class MyComponent(Component):
        val = Int(default_value=42).tag(config=True)

        def __init__(self, config=None, parent=None):
            super().__init__(config=config, parent=parent)
            self.sub = SubComponent(parent=self)

    comp = MyComponent()
    assert comp.get_current_config() == {
        "MyComponent": {"val": 42, "SubComponent": {"param": 3}}
    }

    # test round tripping
    comp = MyComponent()
    comp.val = 10
    comp.sub.param = -1

    dict_config = comp.get_current_config()
    config = Config(dict_config)
    comp_from_config = MyComponent(config=config)

    assert dict_config == comp_from_config.get_current_config()
