"""The processing lifecycle a processor goes through once an event/message is
routed to it: parse the input, call business, format a response - or, on
failure, format the appropriate error response. Callbacks observe each stage.

Everything is driven through ``handle_event`` / ``handle_message`` so the real
executor and the real default factory participate.
"""
from messagebus import Event, Message

from testutil import BaseProcessor

TOKEN = 0x1A2B


# --- the happy path -------------------------------------------------------

def test_runs_parse_then_business_then_format_response(build_handler, message_bus):
    class TransformingProcessor(BaseProcessor):
        def parse_data(self, data=None):
            return data.decode("utf-8")

        def call_business(self, text):
            return text.upper()

        def format_response(self, message_bus, message, event, input_obj, result):
            message_bus.publish_event(Event("result", data=result.encode("utf-8")))

    handler = build_handler(event_processors={"orders": TransformingProcessor()})

    handler.handle_event(message_bus, Event("orders", None, b"hello"))

    assert message_bus.published_events[0].data == b"HELLO"


# --- the parameter-injection contract -------------------------------------
# "Implementations can choose what they want to receive": the executor inspects
# parse_data's signature and passes only the parameters it declares.

def test_injects_only_the_data_parameter_when_requested(build_handler, message_bus):
    class DataCapturer(BaseProcessor):
        received = "unset"

        def parse_data(self, data=None):
            DataCapturer.received = data
            return data

    handler = build_handler(event_processors={"orders": DataCapturer()})

    handler.handle_event(message_bus, Event("orders", "created", b"payload"))

    assert DataCapturer.received == b"payload"


def test_injects_the_event_object_when_requested(build_handler, message_bus):
    class EventCapturer(BaseProcessor):
        received = None

        def parse_data(self, event):
            EventCapturer.received = event
            return event

    event = Event("orders", "created", b"")
    handler = build_handler(event_processors={"orders": EventCapturer()})

    handler.handle_event(message_bus, event)

    assert EventCapturer.received is event


def test_injects_the_event_type_when_requested(build_handler, message_bus):
    class TypeCapturer(BaseProcessor):
        received = "unset"

        def parse_data(self, event_type):
            TypeCapturer.received = event_type
            return event_type

    handler = build_handler(event_processors={"orders": TypeCapturer()})

    handler.handle_event(message_bus, Event("orders", "created", b""))

    assert TypeCapturer.received == "created"


def test_injects_all_parameters_when_the_processor_accepts_kwargs(build_handler, message_bus):
    class KwargsCapturer(BaseProcessor):
        received = None

        def parse_data(self, **kwargs):
            KwargsCapturer.received = kwargs
            return kwargs

    event = Event("orders", "created", b"payload")
    handler = build_handler(event_processors={"orders": KwargsCapturer()})

    handler.handle_event(message_bus, event)

    assert KwargsCapturer.received == {
        "data": b"payload",
        "event_type": "created",
        "event": event,
        "message": None,
    }


def test_injects_the_message_object_when_processing_a_message(build_handler, message_bus):
    class MessageCapturer(BaseProcessor):
        received = None

        def parse_data(self, message):
            MessageCapturer.received = message
            return message

    message = Message(token=TOKEN, data=b"payload")
    handler = build_handler(message_processors={TOKEN: MessageCapturer()})

    handler.handle_message(message_bus, message)

    assert MessageCapturer.received is message


# --- failure paths --------------------------------------------------------

def test_business_failure_is_routed_to_format_exception(build_handler, message_bus):
    class BusinessFailure(BaseProcessor):
        def call_business(self, obj):
            raise ValueError("boom")

        def format_response(self, message_bus, message, event, input_obj, result):
            message_bus.publish_event(Event("success", data=b""))

        def format_exception(self, message_bus, message, event, input_obj, exception):
            message_bus.publish_event(Event("business-error", data=str(exception).encode()))

    handler = build_handler(event_processors={"orders": BusinessFailure()})

    # No exception escapes the handler; the processor formats the error itself.
    handler.handle_event(message_bus, Event("orders", None, b""))

    assert [e.subject for e in message_bus.published_events] == ["business-error"]
    assert message_bus.published_events[0].data == b"boom"


def test_parse_failure_is_routed_to_format_parse_exception_with_raw_data(build_handler, message_bus):
    class ParseFailure(BaseProcessor):
        def parse_data(self, data=None):
            raise ValueError("cannot parse")

        def format_response(self, message_bus, message, event, input_obj, result):
            message_bus.publish_event(Event("success", data=b""))

        def format_exception(self, message_bus, message, event, input_obj, exception):
            message_bus.publish_event(Event("business-error", data=b""))

        def format_parse_exception(self, message_bus, message, event, data, exception):
            message_bus.publish_event(Event("parse-error", data=data))

    handler = build_handler(event_processors={"orders": ParseFailure()})

    handler.handle_event(message_bus, Event("orders", None, b"raw-bytes"))

    # Only the parse-error path ran: business was never called, so neither the
    # success nor the (post-parse) business-error response was produced.
    assert [e.subject for e in message_bus.published_events] == ["parse-error"]
    assert message_bus.published_events[0].data == b"raw-bytes"


# --- the callback observability contract ----------------------------------

def test_callbacks_fire_in_order_for_a_successful_run(build_handler, message_bus, recording_callback):
    handler = build_handler(
        event_processors={"orders": BaseProcessor("orders-processor", "request-7")},
        callbacks=[recording_callback],
    )

    handler.handle_event(message_bus, Event("orders", None, b""))

    assert recording_callback.stages == [
        "process_begun",
        "input_parsed",
        "business_called",
        "process_finished",
    ]
    assert recording_callback.event_names[0] == "orders-processor"
    assert recording_callback.request_ids[0] == "request-7"


def test_callbacks_report_a_parse_failure(build_handler, message_bus, recording_callback):
    class ParseFailure(BaseProcessor):
        def parse_data(self, data=None):
            raise ValueError("nope")

    handler = build_handler(
        event_processors={"orders": ParseFailure("orders-processor")},
        callbacks=[recording_callback],
    )

    handler.handle_event(message_bus, Event("orders", None, b""))

    assert recording_callback.stages == ["process_begun", "parse_exception"]
    assert isinstance(recording_callback.exceptions[0], ValueError)


def test_callbacks_report_a_business_failure(build_handler, message_bus, recording_callback):
    class BusinessFailure(BaseProcessor):
        def call_business(self, obj):
            raise ValueError("nope")

    handler = build_handler(
        event_processors={"orders": BusinessFailure("orders-processor")},
        callbacks=[recording_callback],
    )

    handler.handle_event(message_bus, Event("orders", None, b""))

    assert recording_callback.stages == ["process_begun", "input_parsed", "unhandled_exception"]
    assert isinstance(recording_callback.exceptions[0], ValueError)
