"""Tests for LogBuffer with trace/span ID support."""

from datetime import UTC, datetime

from noteflow.infrastructure.logging.log_buffer import LogBuffer, LogBufferHandler, LogEntry

# Test constants
SAMPLE_TRACE_ID = "abc123"
SAMPLE_SPAN_ID = "def456"
FULL_TRACE_ID = "00000000000000000000000000000001"
FULL_SPAN_ID = "0000000000000001"
DEFAULT_CAPACITY = 100
SMALL_CAPACITY = 3
DEFAULT_LIMIT = 10


class TestLogEntryWithTracing:
    """Tests for LogEntry trace/span ID fields."""

    def test_log_entry_has_trace_id_field(self) -> None:
        """LogEntry has trace_id field."""
        entry = LogEntry(
            timestamp=datetime.now(tz=UTC),
            level="info",
            source="app",
            message="test message",
            trace_id=SAMPLE_TRACE_ID,
        )
        assert (
            entry.trace_id == SAMPLE_TRACE_ID
        ), f"trace_id should be '{SAMPLE_TRACE_ID}'"

    def test_log_entry_has_span_id_field(self) -> None:
        """LogEntry has span_id field."""
        entry = LogEntry(
            timestamp=datetime.now(tz=UTC),
            level="info",
            source="app",
            message="test message",
            span_id=SAMPLE_SPAN_ID,
        )
        assert entry.span_id == SAMPLE_SPAN_ID, f"span_id should be '{SAMPLE_SPAN_ID}'"

    def test_log_entry_trace_ids_default_to_none(self) -> None:
        """LogEntry trace/span IDs default to None."""
        entry = LogEntry(
            timestamp=datetime.now(tz=UTC),
            level="info",
            source="app",
            message="test message",
        )
        assert entry.trace_id is None, "trace_id should default to None"
        assert entry.span_id is None, "span_id should default to None"

    def test_log_entry_with_full_context(self) -> None:
        """LogEntry stores all tracing context."""
        expected_details = {"error": "timeout"}
        entry = LogEntry(
            timestamp=datetime.now(tz=UTC),
            level="error",
            source="api",
            message="Request failed",
            details=expected_details,
            trace_id=FULL_TRACE_ID,
            span_id=FULL_SPAN_ID,
        )
        assert entry.trace_id == FULL_TRACE_ID, "trace_id should match full format"
        assert entry.span_id == FULL_SPAN_ID, "span_id should match full format"
        assert entry.details == expected_details, "details should store error info"


class TestLogBuffer:
    """Tests for LogBuffer."""

    def test_buffer_stores_entries(self) -> None:
        """LogBuffer stores log entries."""
        buffer = LogBuffer(capacity=DEFAULT_CAPACITY)
        expected_message = "Test message"
        buffer.log("info", "app", expected_message)

        entries = buffer.get_recent(limit=DEFAULT_LIMIT)
        assert len(entries) == 1, "buffer should contain one entry"
        assert (
            entries[0].message == expected_message
        ), f"message should be '{expected_message}'"

    def test_buffer_respects_capacity(self) -> None:
        """LogBuffer evicts oldest entries when capacity reached."""
        buffer = LogBuffer(capacity=SMALL_CAPACITY)
        buffer.log("info", "app", "Message 1")
        buffer.log("info", "app", "Message 2")
        buffer.log("info", "app", "Message 3")
        buffer.log("info", "app", "Message 4")

        entries = buffer.get_recent(limit=DEFAULT_LIMIT)
        assert (
            len(entries) == SMALL_CAPACITY
        ), f"buffer should not exceed capacity of {SMALL_CAPACITY}"
        assert (
            entries[0].message == "Message 4"
        ), "most recent entry should be first (Message 4)"
        assert entries[1].message == "Message 3", "second entry should be Message 3"
        assert (
            entries[2].message == "Message 2"
        ), "oldest entry should be Message 2 (Message 1 evicted)"

    def test_buffer_filters_by_level(self) -> None:
        """LogBuffer filters entries by level."""
        buffer = LogBuffer()
        buffer.log("info", "app", "Info message")
        buffer.log("error", "app", "Error message")
        buffer.log("info", "app", "Another info")

        entries = buffer.get_recent(limit=DEFAULT_LIMIT, level="error")
        assert len(entries) == 1, "filter should return only error entries"
        assert (
            entries[0].message == "Error message"
        ), "filtered entry should be error message"

    def test_buffer_filters_by_source(self) -> None:
        """LogBuffer filters entries by source."""
        buffer = LogBuffer()
        buffer.log("info", "app", "App message")
        buffer.log("info", "api", "API message")
        buffer.log("info", "app", "Another app")

        entries = buffer.get_recent(limit=DEFAULT_LIMIT, source="api")
        assert len(entries) == 1, "filter should return only api entries"
        assert (
            entries[0].message == "API message"
        ), "filtered entry should be API message"


class TestLogBufferHandler:
    """Tests for LogBufferHandler."""

    def test_handler_writes_to_buffer(self) -> None:
        """LogBufferHandler writes log records to buffer."""
        import logging

        buffer = LogBuffer()
        handler = LogBufferHandler(buffer=buffer)

        logger = logging.getLogger("test.handler")
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

        try:
            expected_message = "Test log message"
            logger.info(expected_message)

            entries = buffer.get_recent(limit=DEFAULT_LIMIT)
            assert len(entries) == 1, "handler should write one entry to buffer"
            assert (
                expected_message in entries[0].message
            ), f"entry should contain '{expected_message}'"
            assert entries[0].level == "info", "entry level should be 'info'"
        finally:
            logger.removeHandler(handler)

    def test_handler_captures_level_name(self) -> None:
        """LogBufferHandler captures log level name."""
        import logging

        buffer = LogBuffer()
        handler = LogBufferHandler(buffer=buffer)

        logger = logging.getLogger("test.levels")
        logger.addHandler(handler)
        logger.setLevel(logging.DEBUG)

        try:
            logger.debug("Debug message")
            logger.warning("Warning message")
            logger.error("Error message")

            entries = buffer.get_recent(limit=DEFAULT_LIMIT)
            levels = {e.level for e in entries}
            assert "debug" in levels, "buffer should contain debug level entry"
            assert "warning" in levels, "buffer should contain warning level entry"
            assert "error" in levels, "buffer should contain error level entry"
        finally:
            logger.removeHandler(handler)

    def test_handler_formats_message_with_args(self) -> None:
        """LogBufferHandler formats message with arguments."""
        import logging

        buffer = LogBuffer()
        handler = LogBufferHandler(buffer=buffer)

        logger = logging.getLogger("test.formatting")
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

        try:
            logger.info("User %s logged in from %s", "alice", "192.168.1.1")

            entries = buffer.get_recent(limit=1)
            expected = "User alice logged in from 192.168.1.1"
            assert (
                entries[0].message == expected
            ), f"message should be formatted as '{expected}'"
        finally:
            logger.removeHandler(handler)

    def test_handler_includes_trace_context_when_none(self) -> None:
        """LogBufferHandler includes None trace context when OTel not active."""
        import logging

        buffer = LogBuffer()
        handler = LogBufferHandler(buffer=buffer)

        logger = logging.getLogger("test.no_trace")
        logger.addHandler(handler)
        logger.setLevel(logging.INFO)

        try:
            logger.info("No trace context")

            entries = buffer.get_recent(limit=1)
            assert (
                entries[0].trace_id is None
            ), "trace_id should be None without OTel context"
            assert (
                entries[0].span_id is None
            ), "span_id should be None without OTel context"
        finally:
            logger.removeHandler(handler)
