import logging
from typing import Any

import pytest
from pydase.observer_pattern.observable import Observable
from pydase.observer_pattern.observer import Observer

logger = logging.getLogger("pydase")


class MyObserver(Observer):
    def on_change(self, full_access_path: str, value: Any) -> None:
        logger.info("'%s' changed to '%s'", full_access_path, value)


def test_simple_instance_list_attribute(caplog: pytest.LogCaptureFixture) -> None:
    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.list_attr = [1, 2]

    instance = MyObservable()
    MyObserver(instance)
    instance.list_attr[0] = 12

    assert "'list_attr[0]' changed to '12'" in caplog.text


def test_instance_object_list_attribute(caplog: pytest.LogCaptureFixture) -> None:
    class NestedObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.name = "Hello"

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.list_attr = [NestedObservable()]

    instance = MyObservable()
    MyObserver(instance)
    instance.list_attr[0].name = "Ciao"

    assert "'list_attr[0].name' changed to 'Ciao'" in caplog.text


def test_simple_class_list_attribute(caplog: pytest.LogCaptureFixture) -> None:
    class MyObservable(Observable):
        list_attr = [1, 2]

    instance = MyObservable()
    MyObserver(instance)
    instance.list_attr[0] = 12

    assert "'list_attr[0]' changed to '12'" in caplog.text


def test_class_object_list_attribute(caplog: pytest.LogCaptureFixture) -> None:
    class NestedObservable(Observable):
        name = "Hello"

    class MyObservable(Observable):
        list_attr = [NestedObservable()]

    instance = MyObservable()
    MyObserver(instance)
    instance.list_attr[0].name = "Ciao"

    assert "'list_attr[0].name' changed to 'Ciao'" in caplog.text


def test_removed_observer_on_class_list_attr(caplog: pytest.LogCaptureFixture) -> None:
    class NestedObservable(Observable):
        name = "Hello"

    nested_instance = NestedObservable()

    class MyObservable(Observable):
        nested_attr = nested_instance
        changed_list_attr = [nested_instance]

    instance = MyObservable()
    MyObserver(instance)

    assert nested_instance._observers == {
        "[0]": [instance.changed_list_attr],
        "nested_attr": [instance],
    }

    instance.changed_list_attr[0] = "Ciao"

    assert "'changed_list_attr[0]' changed to 'Ciao'" in caplog.text
    caplog.clear()

    assert nested_instance._observers == {
        "nested_attr": [instance],
    }

    instance.nested_attr.name = "Hi"

    assert "'nested_attr.name' changed to 'Hi'" in caplog.text
    assert "'changed_list_attr[0].name' changed to 'Hi'" not in caplog.text


def test_removed_observer_on_instance_list_attr(
    caplog: pytest.LogCaptureFixture,
) -> None:
    class NestedObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.name = "Hello"

    nested_instance = NestedObservable()

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.nested_attr = nested_instance
            self.changed_list_attr = [nested_instance]

    instance = MyObservable()
    MyObserver(instance)
    instance.changed_list_attr[0] = "Ciao"

    assert "'changed_list_attr[0]' changed to 'Ciao'" in caplog.text
    caplog.clear()

    assert nested_instance._observers == {
        "nested_attr": [instance],
    }

    instance.nested_attr.name = "Hi"

    assert "'nested_attr.name' changed to 'Hi'" in caplog.text
    assert "'changed_list_attr[0].name' changed to 'Hi'" not in caplog.text


def test_list_append(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Other Observable"

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = []

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    observable_instance.my_list.append(OtherObservable())
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text
    caplog.clear()

    observable_instance.my_list.append(OtherObservable())
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text
    caplog.clear()

    observable_instance.my_list[0].greeting = "Hi"
    observable_instance.my_list[1].greeting = "Hello"

    assert observable_instance.my_list[0].greeting == "Hi"
    assert observable_instance.my_list[1].greeting == "Hello"
    assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text
    assert "'my_list[1].greeting' changed to 'Hello'" in caplog.text


def test_list_pop(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Hello there!"

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = [OtherObservable() for _ in range(2)]

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    popped_instance = observable_instance.my_list.pop(0)

    assert len(observable_instance.my_list) == 1
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text

    # checks if observer is removed
    popped_instance.greeting = "Ciao"
    assert "'my_list[0].greeting' changed to 'Ciao'" not in caplog.text
    caplog.clear()

    # checks if observer keys have been updated (index 1 moved to 0)
    observable_instance.my_list[0].greeting = "Hi"
    assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text


def test_list_clear(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Hello there!"

    other_observable_instance = OtherObservable()

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = [other_observable_instance]

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    other_observable_instance.greeting = "Hello"
    assert "'my_list[0].greeting' changed to 'Hello'" in caplog.text

    observable_instance.my_list.clear()

    assert len(observable_instance.my_list) == 0
    assert "'my_list' changed to '[]'" in caplog.text

    # checks if observer has been removed
    other_observable_instance.greeting = "Hi"
    assert "'my_list[0].greeting' changed to 'Hi'" not in caplog.text


def test_list_extend(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Hello there!"

    other_observable_instance = OtherObservable()

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = []

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    other_observable_instance.greeting = "Hello"
    assert "'my_list[0].greeting' changed to 'Hello'" not in caplog.text

    observable_instance.my_list.extend([other_observable_instance, OtherObservable()])

    assert len(observable_instance.my_list) == 2
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text

    # checks if observer has been removed
    other_observable_instance.greeting = "Hi"
    assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text
    observable_instance.my_list[1].greeting = "Ciao"
    assert "'my_list[1].greeting' changed to 'Ciao'" in caplog.text


def test_list_insert(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Hello there!"

    other_observable_instance_1 = OtherObservable()
    other_observable_instance_2 = OtherObservable()

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = [other_observable_instance_1, OtherObservable()]

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    other_observable_instance_1.greeting = "Hello"
    assert "'my_list[0].greeting' changed to 'Hello'" in caplog.text

    observable_instance.my_list.insert(0, other_observable_instance_2)

    assert len(observable_instance.my_list) == 3
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text

    # checks if observer keys have been updated
    other_observable_instance_2.greeting = "Hey"
    other_observable_instance_1.greeting = "Hi"
    observable_instance.my_list[2].greeting = "Ciao"

    assert "'my_list[0].greeting' changed to 'Hey'" in caplog.text
    assert "'my_list[1].greeting' changed to 'Hi'" in caplog.text
    assert "'my_list[2].greeting' changed to 'Ciao'" in caplog.text


def test_list_remove(caplog: pytest.LogCaptureFixture) -> None:
    class OtherObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.greeting = "Hello there!"

    other_observable_instance_1 = OtherObservable()
    other_observable_instance_2 = OtherObservable()

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.my_list = [other_observable_instance_1, other_observable_instance_2]

    observable_instance = MyObservable()
    MyObserver(observable_instance)

    other_observable_instance_1.greeting = "Hello"
    other_observable_instance_2.greeting = "Hi"
    caplog.clear()

    observable_instance.my_list.remove(other_observable_instance_1)

    assert len(observable_instance.my_list) == 1
    assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text
    caplog.clear()

    # checks if observer has been removed
    other_observable_instance_1.greeting = "Hi"
    assert "'my_list[0].greeting' changed to 'Hi'" not in caplog.text
    caplog.clear()

    # checks if observer key was updated correctly (was index 1)
    other_observable_instance_2.greeting = "Ciao"
    assert "'my_list[0].greeting' changed to 'Ciao'" in caplog.text


def test_list_garbage_collection() -> None:
    """Makes sure that the GC collects lists that are not referenced anymore."""

    import gc
    import json

    list_json = """
    [1]
    """

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.list_attr = json.loads(list_json)

    observable = MyObservable()
    list_mapping_length = len(observable._list_mapping)
    observable.list_attr = json.loads(list_json)

    gc.collect()
    assert len(observable._list_mapping) <= list_mapping_length


def test_dict_garbage_collection() -> None:
    """Makes sure that the GC collects dicts that are not referenced anymore."""

    import gc
    import json

    dict_json = """
    {
        "foo": "bar"
    }
    """

    class MyObservable(Observable):
        def __init__(self) -> None:
            super().__init__()
            self.dict_attr = json.loads(dict_json)

    observable = MyObservable()
    dict_mapping_length = len(observable._dict_mapping)
    observable.dict_attr = json.loads(dict_json)

    gc.collect()
    assert len(observable._dict_mapping) <= dict_mapping_length