import threading
from threading import (
    Event as ThreadEvent,
    Lock,
)
from typing import (
    Any,
    Dict,
    Optional,
)

import pytest
from assertpy import assert_that
from contextmessagebus import MultiContextMessageBus
from contextmessagehandler import DynamicContextMessageHandlerBuilder
from messagebus import (
    Event,
    Message,
    MessageBus,
)
from messagehandler import MessageHandler


def test_context_is_lazily_created_on_first_message():
    builder = MyDynamicBuilder(contexts={"tenant_a": {"db": "db_a"}})

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

    assert_that(builder.build_context_call_count).is_equal_to(1)
    assert_that(builder.build_context_calls).is_equal_to(["tenant_a"])
    assert_that(handler.built_for_context).is_equal_to("tenant_a")


def test_context_is_cached_and_not_rebuilt_on_subsequent_messages():
    builder = MyDynamicBuilder(contexts={"tenant_a": {"db": "db_a"}})

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

    assert_that(builder.build_context_call_count).is_equal_to(1)


def test_each_tenant_gets_its_own_context():
    builder = MyDynamicBuilder(contexts={
        "tenant_a": {"db": "db_a"},
        "tenant_b": {"db": "db_b"},
    })

    handler_a = builder.build_message_handler(
        message=Message(token=1, headers={"context": "tenant_a"}),
        event=None,
    )
    handler_b = builder.build_message_handler(
        message=Message(token=2, headers={"context": "tenant_b"}),
        event=None,
    )

    assert_that(builder.build_context_call_count).is_equal_to(2)
    assert_that(handler_a.built_for_context).is_equal_to("tenant_a")
    assert_that(handler_b.built_for_context).is_equal_to("tenant_b")


def test_concurrent_requests_for_same_context_build_it_only_once():
    num_threads = 10
    gate = ThreadEvent()
    builder = MyDynamicBuilder(
        contexts={"tenant_a": {"db": "db_a"}},
        build_context_gate=gate,
    )

    results = []
    errors = []
    ready = ThreadEvent()
    ready_count = [0]
    ready_lock = Lock()

    def request():
        with ready_lock:
            ready_count[0] += 1
            if ready_count[0] == num_threads:
                ready.set()
        ready.wait(timeout=5)
        try:
            handler = builder.build_message_handler(
                message=Message(token=1, headers={"context": "tenant_a"}),
                event=None,
            )
            results.append(handler)
        except Exception as e:
            errors.append(e)

    threads = [threading.Thread(target=request) for _ in range(num_threads)]
    for t in threads:
        t.start()

    ready.wait(timeout=5)
    gate.set()

    for t in threads:
        t.join(timeout=5)

    assert_that(errors).is_empty()
    assert_that(builder.build_context_call_count).is_equal_to(1)
    assert_that(results).is_length(num_threads)


def test_raises_when_no_context_header_in_message():
    builder = MyDynamicBuilder()

    with pytest.raises(Exception, match="No context"):
        builder.build_message_handler(
            message=Message(token=1, headers={}),
            event=None,
        )


def test_raises_when_build_context_returns_none():
    builder = MyDynamicBuilder(build_context_returns_none=True)

    with pytest.raises(Exception, match="No context"):
        builder.build_message_handler(
            message=Message(token=1, headers={"context": "tenant_a"}),
            event=None,
        )


def test_none_context_is_not_cached_and_retries():
    builder = MyDynamicBuilder(build_context_returns_none=True)

    with pytest.raises(Exception, match="No context"):
        builder.build_message_handler(
            message=Message(token=1, headers={"context": "tenant_a"}),
            event=None,
        )

    with pytest.raises(Exception, match="No context"):
        builder.build_message_handler(
            message=Message(token=2, headers={"context": "tenant_a"}),
            event=None,
        )

    assert_that(builder.build_context_call_count).is_equal_to(2)


def test_destroy_message_handler_cleans_up():
    builder = MyDynamicBuilder(contexts={"tenant_a": {}})

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

    assert_that(threading.current_thread()).has__edp_context_name("tenant_a")

    builder.destroy_message_handler(handler)

    assert_that(hasattr(threading.current_thread(), '_edp_context_name')).is_false()
    assert_that(builder.message_handler_context).does_not_contain_key(handler)


def test_destroy_singletons_passes_only_non_none_contexts():
    builder = MyDynamicBuilder(contexts={"tenant_a": {"db": "db_a"}})

    # Create one real context and one None context
    builder.build_message_handler(
        message=Message(token=1, headers={"context": "tenant_a"}),
        event=None,
    )
    builder.context_dict["tenant_none"] = None

    builder.destroy_singletons()

    assert_that(builder.destroy_singletons_called_with).contains_key("tenant_a")
    assert_that(builder.destroy_singletons_called_with).does_not_contain_key("tenant_none")


def test_request_data_is_set_on_context():
    builder = MyDynamicBuilderWithRequestData(
        contexts={"tenant_a": {}},
        request_data={"user": "alice"},
    )

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

    context = builder.context_dict["tenant_a"]
    assert_that(context.request_data).is_equal_to({"user": "alice"})


def test_request_data_is_thread_local():
    builder = MyDynamicBuilderWithRequestData(
        contexts={"tenant_a": {}},
        request_data=None,
    )

    # Build context on main thread first
    builder.build_message_handler(
        message=Message(token=1, headers={"context": "tenant_a"}),
        event=None,
    )
    context = builder.context_dict["tenant_a"]

    results = {}
    barrier = ThreadEvent()
    ready_count = [0]
    ready_lock = Lock()

    def set_request_data_in_thread(thread_name, data):
        context.request_data = data
        with ready_lock:
            ready_count[0] += 1
            if ready_count[0] == 2:
                barrier.set()
        barrier.wait(timeout=5)
        results[thread_name] = context.request_data

    t1 = threading.Thread(target=set_request_data_in_thread, args=("t1", {"user": "alice"}))
    t2 = threading.Thread(target=set_request_data_in_thread, args=("t2", {"user": "bob"}))

    t1.start()
    t2.start()
    t1.join(timeout=5)
    t2.join(timeout=5)

    assert_that(results["t1"]).is_equal_to({"user": "alice"})
    assert_that(results["t2"]).is_equal_to({"user": "bob"})


def test_multi_context_message_bus_set_current_context_is_called():
    dummy_bus = DummyMessageBus()
    multi_bus = MultiContextMessageBus(message_bus=dummy_bus)
    builder = MyDynamicBuilder(
        message_bus=multi_bus,
        contexts={"tenant_a": {}},
    )

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

    assert_that(multi_bus.local_data.context).is_equal_to("tenant_a")


def test_get_context_name_from_message_headers():
    message = Message(token=1, headers={"context": "tenant_x"})
    result = DynamicContextMessageHandlerBuilder.get_context_name(message, None)
    assert_that(result).is_equal_to("tenant_x")


def test_get_context_name_from_event_headers():
    event = Event(
        subject="test_event",
        headers={"context": "tenant_y"},
    )
    result = DynamicContextMessageHandlerBuilder.get_context_name(None, event)
    assert_that(result).is_equal_to("tenant_y")


# --- Test helpers ---


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


class StubMessageHandler(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 MyDynamicBuilder(DynamicContextMessageHandlerBuilder):
    def __init__(
        self,
        message_bus: Optional[MessageBus] = None,
        contexts: Optional[Dict[str, Any]] = None,
        build_context_returns_none: bool = False,
        build_context_gate: Optional[ThreadEvent] = None,
    ):
        super().__init__(message_bus=message_bus or DummyMessageBus())
        self.contexts = contexts or {}
        self.build_context_returns_none = build_context_returns_none
        self.build_context_gate = build_context_gate

        self.build_context_call_count = 0
        self.build_context_calls = []
        self.destroyed_handlers = []
        self.destroy_singletons_called_with = None

    def build_context(self, context_name: str):
        if self.build_context_gate is not None:
            self.build_context_gate.wait(timeout=5)

        self.build_context_call_count += 1
        self.build_context_calls.append(context_name)

        if self.build_context_returns_none:
            return None
        return self.contexts.get(context_name, {})

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

    def destroy_message_handler_with_context(self, context, message_handler):
        self.destroyed_handlers.append(message_handler)

    def destroy_singletons_with_context(self, context_dict):
        self.destroy_singletons_called_with = context_dict


class MyDynamicBuilderWithRequestData(MyDynamicBuilder):
    def __init__(self, request_data=None, **kwargs):
        super().__init__(**kwargs)
        self._request_data = request_data

    def get_request_context_data(self, message, event):
        return self._request_data
