import time
from threading import (
    Condition,
)
from typing import (
    Any,
    Dict,
    List,
)

import pytest
from assertpy import assert_that
from contextmessagehandler import AutoupdateContextMessageHandlerBuilder
from messagebus import (
    Message,
    MessageBus,
)
from messagehandler import MessageHandler


def test_retrieves_the_context_ttl():
    builder = MyAutoUpdate()
    builder.build_singletons()
    assert_that(builder.build_context_config_called).is_true()


def test_calls_build_context_dict_when_build_singletons_is_called():
    builder = MyAutoUpdate()
    builder.build_singletons()
    assert_that(builder.build_context_config_called).is_true()


def test_calls_build_context_dict_when_context_is_expired():
    builder = MyAutoUpdate(ttl=0.01)
    builder.build_singletons()
    time.sleep(0.2)
    assert_that(builder.build_context_dict_call_count).is_greater_than(9)


@pytest.mark.parametrize(
    argnames=("ttl",),
    argvalues=(
        (0,),
        (-1,),
    ),
    ids=(
        "zero",
        "negative"
    )
)
def test_does_not_update_the_context_when_the_returned_ttl_is_lower_or_equal_to_zero(ttl):
    builder = MyAutoUpdate(ttl=ttl)
    builder.build_singletons()
    time.sleep(0.1)
    assert_that(builder.build_context_dict_call_count).is_equal_to(1)


def test_calls_build_singletons_for_context_for_each_context():
    contexts = ["1", "2"]
    builder = MyAutoUpdate(
        contexts=[contexts],
    )
    builder.build_singletons()
    for context in contexts:
        assert_that(builder.parts_built).contains_key(context)


def test_calls_context_will_update_before_and_context_updated_after_creating_the_context():
    initial_contexts = ["1", "2"]
    new_contexts = ["3"]
    builder = MyAutoUpdate(
        contexts=[initial_contexts, initial_contexts + new_contexts],
    )
    builder.build_singletons()

    assert_that(builder.calls[0]).is_equal_to("context_will_update")
    assert_that(builder.calls[-1]).is_equal_to("context_updated")


def test_calls_context_will_update_before_and_context_updated_after_updating_the_context():
    initial_contexts = ["1", "2"]
    new_contexts = ["3"]
    builder = MyAutoUpdate(
        contexts=[initial_contexts, initial_contexts + new_contexts],
        ttl=0.1
    )
    builder.build_singletons()

    builder.calls.clear()
    builder.wait_context_update()

    assert_that(builder.calls[0]).is_equal_to("context_will_update")
    assert_that(builder.calls[-1]).is_equal_to("context_updated")


def test_calls_build_context_a_context_when_a_new_context_appears():
    initial_contexts = ["1", "2"]
    new_contexts = ["3"]
    builder = MyAutoUpdate(
        contexts=[initial_contexts, initial_contexts + new_contexts],
        ttl=0.1
    )
    builder.build_singletons()

    builder.wait_context_update()

    for context in new_contexts:
        assert_that(builder.parts_built).contains_key(context)


def test_calls_destroy_a_context_part_when_a_context_disappears():
    initial_contexts = ["1", "2", "3"]
    disappearing_contexts = ["3"]

    end_context = initial_contexts[:]
    for context in disappearing_contexts:
        end_context.remove(context)

    builder = MyAutoUpdate(
        contexts=[initial_contexts, end_context],
        ttl=0.1
    )
    builder.build_singletons()

    builder.wait_context_update()

    for context in disappearing_contexts:
        assert_that(builder.parts_built).does_not_contain_key(context)


def test_preserves_the_new_context_when_a_second_one_is_added():
    initial_contexts = ["1", "2"]
    first_new_context = ["3"]
    second_new_context = ["4"]
    intermediary_context = initial_contexts + first_new_context
    final_context = intermediary_context + second_new_context
    builder = MyAutoUpdate(
        contexts=[
            initial_contexts,
            intermediary_context,
            final_context,
        ],
        ttl=0.1,
    )
    builder.build_singletons()

    builder.wait_context_update()
    builder.wait_context_update()

    for context in final_context:
        assert_that(builder.parts_built).contains_key(context)


def test_does_not_call_context_will_update_and_context_updated_when_the_context_is_not_changed():
    builder = MyAutoUpdate(
        contexts=[["1", "2"]],
        ttl=0.01,
    )
    builder.build_singletons()

    builder.calls.clear()
    time.sleep(0.1)
    assert_that(builder.calls).is_empty()


def test_new_context_are_found_on_the_internal_context_dictionary():
    initial_contexts = ["1", "2"]
    new_contexts = ["3"]
    final_contexts = initial_contexts + new_contexts
    builder = MyAutoUpdate(
        contexts=[
            initial_contexts,
            final_contexts,
        ],
        ttl=0.1,
    )
    builder.build_singletons()

    builder.wait_context_update()

    for context in final_contexts:
        build_message_handler = builder.build_message_handler(
            message=Message(
                token=1,
                headers={"context": context},
            ),
            event=None,
        )

        assert_that(build_message_handler.built_for_context).is_equal_to(context)


def test_old_context_is_removed_from_the_internal_context_dictionary():
    initial_contexts = ["1", "2", "3"]
    disappearing_contexts = ["3"]
    final_contexts = initial_contexts[:]
    for context in disappearing_contexts:
        final_contexts.remove(context)

    builder = MyAutoUpdate(
        contexts=[
            initial_contexts,
            final_contexts,
        ],
        ttl=0.1,
    )
    builder.build_singletons()

    builder.wait_context_update()

    build_message_handler = builder.build_message_handler(
        message=Message(
            token=1,
            headers={"context": "3"},
        ),
        event=None,
    )

    assert_that(build_message_handler.built_for_context).is_none()


def test_calls_build_context_independent_singletons_once():
    builder = MyAutoUpdate(contexts=[["1"]])
    builder.build_singletons()

    assert_that(builder.build_context_independent_singletons_called).is_true()


def test_calls_destroy_context_independent_singletons_when_destroyed():
    builder = MyAutoUpdate(contexts=[["1"]])
    builder.build_singletons()
    builder.destroy_singletons()

    assert_that(builder.destroy_context_independent_singletons_called).is_true()


class MyAutoUpdate(AutoupdateContextMessageHandlerBuilder):
    def __init__(
        self,
        ttl: float = 100,
        contexts: List[List[str]] = None,
    ):
        super().__init__(message_bus=DummyMessageBus())
        self.ttl = ttl
        self.contexts = contexts or []
        self.calls = []

        self.build_context_config_called = False
        self.build_context_independent_singletons_called = False
        self.destroy_context_independent_singletons_called = False
        self.build_context_dict_call_count = 0
        self.context_will_update_count = 0
        self.context_updated_count = 0
        self.parts_built = {}
        self.parts_destroyed = {}
        self.current_context = 0
        self.context_will_update_condition = Condition()
        self.context_updated_condition = Condition()

    def build_context_independent_singletons(self):
        self.build_context_independent_singletons_called = True

    def get_context_ttl(self) -> float:
        self.build_context_config_called = True
        return self.ttl

    def build_context_dict(self) -> Dict[str, Any]:
        self.build_context_dict_call_count += 1
        if len(self.contexts) == 0:
            return {}

        ret = {}
        for context in self.contexts[self.current_context]:
            ret[context] = {}

        self.current_context += 1
        if self.current_context >= len(self.contexts):
            self.current_context = len(self.contexts) - 1

        return ret

    def build_context_singletons(self, context_name, context_dict: Dict[str, Any]) -> Any:
        if context_name in self.parts_built:
            raise RuntimeError(f"Context {context_name} was already built")

        self.calls.append("build_context_part")
        self.parts_built[context_name] = context_dict
        return {}

    def destroy_context_singletons(self, context_name) -> Any:
        del self.parts_built[context_name]

    def context_will_update(self):
        self.calls.append("context_will_update")

        self.context_will_update_count += 1
        if self.context_will_update_count >= 1:
            with self.context_will_update_condition:
                self.context_will_update_condition.notify()

    def context_updated(self):
        self.calls.append("context_updated")

        self.context_updated_count += 1
        if self.context_updated_count >= 1:
            with self.context_updated_condition:
                self.context_updated_condition.notify()

    def wait_context_update(self, timeout: int = 1) -> bool:
        with self.context_updated_condition:
            return self.context_updated_condition.wait(timeout=timeout)

    def build_message_handler_with_context(self, context):
        if context is None:
            context_name = None
        else:
            context_name = context.context_name

        return ContextMessageHandler(context_name=context_name)

    def destroy_message_handler_with_context(self, context, message_handler):
        return

    def destroy_context_independent_singletons(self):
        self.destroy_context_independent_singletons_called = True


class ContextMessageHandler(MessageHandler):
    def __init__(self, context_name: str):
        self.built_for_context = context_name

    def handle_event(self, message_bus, event):
        return

    def handle_message(self, message_bus, message):
        return


class DummyMessageBus(MessageBus):
    def publish_event(self, event):
        pass

    def send_message(self, component, message):
        pass

    def subscribe(self, subscriptions):
        pass

    def reply_message(self, message, reply_message):
        pass

    def reply_event(self, event, reply_message):
        pass
