"""Tests for FlightRecorder logging infrastructure."""

import logging

import pytest

from entr_adapter_core.handlers import BaseHandler
from entr_adapter_core.logging import (
    FlightRecorder,
    FlightRecorderHandler,
    execute_with_flight_recorder,
)
from entr_adapter_core.schemas import (
    AdapterResponse,
    DocumentPayload,
    StatusEnum,
)


# -- Test handlers --


class LoggingHandler(BaseHandler):
    """Handler that logs messages during processing."""

    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        logger = logging.getLogger("test.handler")
        logger.info("Processing document %d", payload.document_id)
        logger.warning("Something to note", extra={"key": "value"})
        return AdapterResponse(
            document_id=payload.document_id,
            status=StatusEnum.SUCCESS,
        )


class CrashingHandler(BaseHandler):
    """Handler that logs then raises an exception."""

    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        logger = logging.getLogger("test.crasher")
        logger.info("About to crash")
        raise ValueError("something broke")


class SilentHandler(BaseHandler):
    """Handler that produces no logs."""

    async def process(self, payload: DocumentPayload) -> AdapterResponse:
        return AdapterResponse(
            document_id=payload.document_id,
            status=StatusEnum.SUCCESS,
        )


# -- Fixtures --


@pytest.fixture
def payload():
    return DocumentPayload(
        adapter_identifier="test",
        document_id=42,
        data={"key": "value"},
    )


# -- FlightRecorderHandler tests --


class TestFlightRecorderHandler:

    def test_basic_log_capture(self):
        handler = FlightRecorderHandler()
        logger = logging.getLogger("test.basic")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            logger.info("Info message")
            logger.warning("Warning message")
            logger.error("Error message")

            logs = handler.get_logs()
            assert len(logs) == 3
            assert logs[0]["level"] == "INFO"
            assert logs[1]["level"] == "WARNING"
            assert logs[2]["level"] == "ERROR"
        finally:
            logger.removeHandler(handler)

    def test_log_entry_format(self):
        handler = FlightRecorderHandler()
        logger = logging.getLogger("test.format")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            logger.info("Test message")
            logs = handler.get_logs()
            entry = logs[0]

            assert "timestamp" in entry
            assert "level" in entry
            assert "logger_name" in entry
            assert "message" in entry
            assert entry["logger_name"] == "test.format"
            assert "Test message" in entry["message"]
        finally:
            logger.removeHandler(handler)

    def test_extra_data_capture(self):
        handler = FlightRecorderHandler()
        logger = logging.getLogger("test.extra")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            logger.info("With context", extra={"key": "val", "num": 42})
            logs = handler.get_logs()
            assert logs[0]["context"] is not None
            assert logs[0]["context"]["key"] == "val"
            assert logs[0]["context"]["num"] == 42
        finally:
            logger.removeHandler(handler)

    def test_standard_attrs_excluded_from_context(self):
        handler = FlightRecorderHandler()
        logger = logging.getLogger("test.attrs")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            logger.info("No extra")
            logs = handler.get_logs()
            context = logs[0]["context"]
            # With no extra data, context should be None
            assert context is None
        finally:
            logger.removeHandler(handler)

    def test_max_entry_limit(self):
        handler = FlightRecorderHandler(max_entries=1000)
        logger = logging.getLogger("test.limit")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            for i in range(1005):
                logger.info("Entry %d", i)

            logs = handler.get_logs()
            # 1000 entries + 1 truncation marker
            assert len(logs) == 1001
        finally:
            logger.removeHandler(handler)

    def test_truncation_marker(self):
        handler = FlightRecorderHandler(max_entries=1000)
        logger = logging.getLogger("test.truncation")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            for i in range(1005):
                logger.info("Entry %d", i)

            logs = handler.get_logs()
            marker = logs[-1]
            assert marker["truncated"] is True
            assert marker["dropped_count"] == 5
        finally:
            logger.removeHandler(handler)

    def test_no_truncation_under_limit(self):
        handler = FlightRecorderHandler(max_entries=1000)
        logger = logging.getLogger("test.notrunc")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            for i in range(10):
                logger.info("Entry %d", i)

            logs = handler.get_logs()
            assert len(logs) == 10
            # No truncation marker
            assert not any(entry.get("truncated") for entry in logs)
        finally:
            logger.removeHandler(handler)

    def test_clear_resets_state(self):
        handler = FlightRecorderHandler(max_entries=5)
        logger = logging.getLogger("test.clear")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)
        handler.setLevel(logging.DEBUG)

        try:
            for i in range(10):
                logger.info("Entry %d", i)

            handler.clear()
            logs = handler.get_logs()
            assert len(logs) == 0
        finally:
            logger.removeHandler(handler)


# -- FlightRecorder context manager tests --


class TestFlightRecorderContextManager:

    @pytest.mark.asyncio
    async def test_captures_logs_during_block(self):
        logger = logging.getLogger("test.ctx")
        logger.setLevel(logging.DEBUG)

        async with FlightRecorder(logger_name="test.ctx") as recorder:
            logger.info("Inside context")

        logs = recorder.get_logs()
        assert len(logs) == 1
        assert "Inside context" in logs[0]["message"]

    @pytest.mark.asyncio
    async def test_handler_removed_on_normal_exit(self):
        logger = logging.getLogger("test.cleanup")
        logger.setLevel(logging.DEBUG)
        initial_count = len(logger.handlers)

        async with FlightRecorder(logger_name="test.cleanup"):
            assert len(logger.handlers) == initial_count + 1

        assert len(logger.handlers) == initial_count

    @pytest.mark.asyncio
    async def test_handler_removed_on_exception(self):
        logger = logging.getLogger("test.exccleanup")
        logger.setLevel(logging.DEBUG)
        initial_count = len(logger.handlers)

        with pytest.raises(RuntimeError):
            async with FlightRecorder(logger_name="test.exccleanup"):
                raise RuntimeError("boom")

        assert len(logger.handlers) == initial_count

    @pytest.mark.asyncio
    async def test_logs_available_after_exception(self):
        logger = logging.getLogger("test.exclog")
        logger.setLevel(logging.DEBUG)

        recorder = FlightRecorder(logger_name="test.exclog")
        with pytest.raises(RuntimeError):
            async with recorder:
                logger.info("Before crash")
                raise RuntimeError("boom")

        logs = recorder.get_logs()
        assert len(logs) == 1
        assert "Before crash" in logs[0]["message"]

    @pytest.mark.asyncio
    async def test_root_logger_capture(self):
        root_logger = logging.getLogger()
        root_logger.setLevel(logging.DEBUG)

        async with FlightRecorder(logger_name=None) as recorder:
            logging.getLogger("some.module").info("Root capture test")

        logs = recorder.get_logs()
        assert any("Root capture test" in entry["message"] for entry in logs)

    @pytest.mark.asyncio
    async def test_specific_logger_capture(self):
        specific = logging.getLogger("test.specific")
        specific.setLevel(logging.DEBUG)
        other = logging.getLogger("test.other")
        other.setLevel(logging.DEBUG)

        async with FlightRecorder(logger_name="test.specific") as recorder:
            specific.info("Captured")
            other.info("Not captured directly")

        logs = recorder.get_logs()
        messages = [entry["message"] for entry in logs]
        assert any("Captured" in m for m in messages)


# -- execute_with_flight_recorder tests --


class TestExecuteWithFlightRecorder:

    @pytest.mark.asyncio
    async def test_successful_execution(self, payload):
        handler = LoggingHandler()
        response = await execute_with_flight_recorder(handler, payload)

        assert response.status == StatusEnum.SUCCESS
        assert response.document_id == 42
        assert response.execution_logs is not None
        assert len(response.execution_logs) >= 2
        messages = [log["message"] for log in response.execution_logs]
        assert any("Processing document 42" in m for m in messages)

    @pytest.mark.asyncio
    async def test_handler_exception_returns_failed(self, payload):
        handler = CrashingHandler()
        response = await execute_with_flight_recorder(handler, payload)

        assert response.status == StatusEnum.FAILED
        assert response.failure_reason == "something broke"
        assert response.document_id == 42

    @pytest.mark.asyncio
    async def test_failure_reason_contains_exception_message(self, payload):
        handler = CrashingHandler()
        response = await execute_with_flight_recorder(handler, payload)

        assert "something broke" in (response.failure_reason or "")
        assert "Traceback" not in (response.failure_reason or "")

    @pytest.mark.asyncio
    async def test_pre_crash_logs_preserved(self, payload):
        handler = CrashingHandler()
        response = await execute_with_flight_recorder(handler, payload)

        assert response.execution_logs is not None
        messages = [log["message"] for log in response.execution_logs]
        assert any("About to crash" in m for m in messages)

    @pytest.mark.asyncio
    async def test_traceback_in_execution_logs(self, payload):
        handler = CrashingHandler()
        response = await execute_with_flight_recorder(handler, payload)

        # Tracebacks should be captured in execution_logs for diagnostics
        all_text = " ".join(log.get("message", "") for log in response.execution_logs)
        assert "something broke" in all_text

    @pytest.mark.asyncio
    async def test_document_id_propagated(self, payload):
        handler = CrashingHandler()
        response = await execute_with_flight_recorder(handler, payload)
        assert response.document_id == payload.document_id

    @pytest.mark.asyncio
    async def test_silent_handler_has_empty_logs(self, payload):
        handler = SilentHandler()
        response = await execute_with_flight_recorder(handler, payload)

        assert response.status == StatusEnum.SUCCESS
        assert response.execution_logs is not None
        # May have some logs from other loggers, but handler produced none
