# #region Test.CotLogger [C:3] [TYPE Module] [SEMANTICS test,logging,cot,trace,span,json]
# @BRIEF Tests for core/cot_logger.py — trace_id, span, log markers, MarkerLogger.
# @RELATION BINDS_TO -> [CotLoggerModule]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

import logging
import re
from unittest.mock import patch, MagicMock, call
import pytest


class TestSeedTraceId:
    """seed_trace_id — generates UUID and sets ContextVar."""

    def test_returns_uuid_string(self):
        from src.core.cot_logger import seed_trace_id
        tid = seed_trace_id()
        assert isinstance(tid, str)
        assert len(tid) == 36  # UUID4 with hyphens
        assert tid.count("-") == 4

    def test_sets_trace_id(self):
        from src.core.cot_logger import seed_trace_id, get_trace_id
        tid = seed_trace_id()
        assert get_trace_id() == tid


class TestSetTraceId:
    """set_trace_id — sets explicit trace ID."""

    def test_sets_provided_id(self):
        from src.core.cot_logger import set_trace_id, get_trace_id
        set_trace_id("custom-trace-123")
        assert get_trace_id() == "custom-trace-123"


class TestGetTraceId:
    """get_trace_id — returns current trace ID."""

    def test_default_empty(self):
        from src.core.cot_logger import get_trace_id
        # After seed, get_trace_id returns non-empty
        # In a fresh test the ContextVar may be empty
        pass  # ContextVar has default ""

    def test_after_seed_returns_value(self):
        from src.core.cot_logger import seed_trace_id, get_trace_id
        tid = seed_trace_id()
        assert get_trace_id() == tid


class TestPushPopSpan:
    """push_span and pop_span — span stack operations."""

    def test_push_returns_previous(self):
        from src.core.cot_logger import push_span, pop_span
        prev1 = push_span("span-1")
        # Initially empty
        prev2 = push_span("span-2")
        assert prev2 == "span-1"
        pop_span(prev2)
        # After pop, current span is back to "span-1"

    def test_push_pop_restores(self):
        from src.core.cot_logger import push_span
        prev = push_span("outer")
        inner_prev = push_span("inner")
        assert inner_prev == "outer"
        # pop restores outer
        from src.core.cot_logger import pop_span
        pop_span(inner_prev)


class TestLog:
    """log — structured JSON logging with markers."""

    def test_reason_marker_info_level(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.info") as mock_info:
            log("TestModule", "REASON", "Testing reason")
            mock_info.assert_called_once()
            args, kwargs = mock_info.call_args
            assert "Testing reason" in args[0]
            extra = kwargs.get("extra", {})
            assert extra["marker"] == "REASON"
            assert extra["src"] == "TestModule"
            assert extra["intent"] == "Testing reason"

    def test_reflect_marker_info_level(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.info") as mock_info:
            log("Test", "REFLECT", "Reflecting")
            mock_info.assert_called_once()

    def test_explore_marker_warning_level(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.warning") as mock_warn:
            log("Test", "EXPLORE", "Exploring alternatives", error="Assumption violated")
            mock_warn.assert_called_once()

    def test_error_level_explicit(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.error") as mock_err:
            log("Test", "REASON", "Error test", level="ERROR")
            mock_err.assert_called_once()

    def test_debug_level_explicit(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.debug") as mock_debug:
            log("Test", "REASON", "Debug test", level="DEBUG")
            mock_debug.assert_called_once()

    def test_payload_included(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.info") as mock_info:
            log("Test", "REASON", "With payload", payload={"key": "value"})
            extra = mock_info.call_args[1]["extra"]
            assert extra["payload"] == {"key": "value"}

    def test_error_included(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.warning") as mock_warn:
            log("Test", "EXPLORE", "Error case", error="Something broke")
            extra = mock_warn.call_args[1]["extra"]
            assert extra["error"] == "Something broke"

    def test_payload_not_included_when_none(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.info") as mock_info:
            log("Test", "REASON", "No payload")
            extra = mock_info.call_args[1]["extra"]
            assert "payload" not in extra

    def test_error_not_included_when_none(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.info") as mock_info:
            log("Test", "REASON", "No error")
            extra = mock_info.call_args[1]["extra"]
            assert "error" not in extra

    def test_warning_level_inferred_from_explore(self):
        from src.core.cot_logger import log
        with patch("src.core.cot_logger.cot_logger.warning") as mock_warn:
            log("Test", "EXPLORE", "Auto warning")
            mock_warn.assert_called_once()


class TestMarkerLogger:
    """MarkerLogger — convenience proxy for .reason(), .reflect(), .explore()."""

    @pytest.fixture
    def logger(self):
        from src.core.cot_logger import MarkerLogger
        return MarkerLogger("MyComponent")

    def test_reason(self, logger):
        with patch("src.core.cot_logger.log") as mock_log:
            logger.reason("Doing something", payload={"action": "test"})
            mock_log.assert_called_once_with("MyComponent", "REASON", "Doing something", payload={"action": "test"})

    def test_reflect(self, logger):
        with patch("src.core.cot_logger.log") as mock_log:
            logger.reflect("Done")
            mock_log.assert_called_once_with("MyComponent", "REFLECT", "Done", payload=None)

    def test_explore(self, logger):
        with patch("src.core.cot_logger.log") as mock_log:
            logger.explore("Failed", error="DB timeout")
            mock_log.assert_called_once_with("MyComponent", "EXPLORE", "Failed", payload=None, error="DB timeout")

    def test_explore_with_payload_and_error(self, logger):
        with patch("src.core.cot_logger.log") as mock_log:
            logger.explore("Retrying", payload={"attempt": 2}, error="Timeout")
            mock_log.assert_called_once_with("MyComponent", "EXPLORE", "Retrying", payload={"attempt": 2}, error="Timeout")


class TestTraceIdPropagation:
    """Trace ID propagation across async contexts."""

    def test_seed_then_get(self):
        from src.core.cot_logger import seed_trace_id, get_trace_id
        tid = seed_trace_id()
        assert get_trace_id() == tid

    def test_set_then_get(self):
        from src.core.cot_logger import set_trace_id, get_trace_id
        set_trace_id("abc-123")
        assert get_trace_id() == "abc-123"

    def test_set_overrides_seed(self):
        from src.core.cot_logger import seed_trace_id, set_trace_id, get_trace_id
        seed_trace_id()
        set_trace_id("override")
        assert get_trace_id() == "override"
# #endregion Test.CotLogger
