"""Tests documenting critical behavioral semantics of thread_context.py.

These tests document non-obvious behaviors that are important for correctness
when using ThreadPoolExecutor with research context propagation.
"""

from local_deep_research.utilities.thread_context import (
    clear_search_context,
    get_search_context,
    preserve_research_context,
    search_context,
    set_search_context,
)


class TestPreserveResearchContextDecorator:
    """Tests for @preserve_research_context decorator semantics."""

    def test_preserve_research_context_captures_at_decoration_time(self):
        """Context is captured when @preserve_research_context is applied,
        NOT when the decorated function is called. This is the most important
        behavioral property — it's what makes thread context propagation work."""
        # Set context BEFORE decorating
        set_search_context({"research_id": "captured-at-decoration"})

        @preserve_research_context
        def task():
            return get_search_context()

        # Clear context AFTER decorating but BEFORE calling
        clear_search_context()

        # The decorated function should still see the context from decoration time
        result = task()
        assert result is not None
        assert result["research_id"] == "captured-at-decoration"

    def test_multiple_decorated_functions_carry_independent_snapshots(self):
        """Two functions decorated at different times carry their own
        independent context snapshots."""
        set_search_context({"research_id": "snapshot-1", "phase": "first"})

        @preserve_research_context
        def task_1():
            return get_search_context()

        # Change context before decorating second function
        set_search_context({"research_id": "snapshot-2", "phase": "second"})

        @preserve_research_context
        def task_2():
            return get_search_context()

        clear_search_context()

        result_1 = task_1()
        result_2 = task_2()

        assert result_1["research_id"] == "snapshot-1"
        assert result_1["phase"] == "first"
        assert result_2["research_id"] == "snapshot-2"
        assert result_2["phase"] == "second"

    def test_decorator_with_no_context_is_noop(self):
        """When no context is set at decoration time, the decorator
        doesn't fail — it simply doesn't set/clear any context."""
        clear_search_context()

        @preserve_research_context
        def task():
            return get_search_context()

        result = task()
        # No context was set at decoration time, so context remains None
        assert result is None

    def test_decorator_clears_context_in_finally(self):
        """The decorator clears context after the function runs,
        even if the context was set. This prevents leakage in thread pools."""
        set_search_context({"research_id": "will-be-cleared"})

        @preserve_research_context
        def task():
            ctx = get_search_context()
            return ctx

        clear_search_context()

        # Run the decorated function
        task()

        # After the decorated function returns, context should be cleared
        # because the finally block calls clear_search_context()
        assert get_search_context() is None


class TestSearchContextManager:
    """Tests for the search_context context manager."""

    def test_context_manager_destroys_pre_existing_context(self):
        """with search_context(...) clears any previously-set context in finally.
        This is destructive — the pre-existing context is NOT restored."""
        set_search_context({"research_id": "pre-existing"})

        with search_context({"research_id": "temporary"}):
            ctx = get_search_context()
            assert ctx["research_id"] == "temporary"

        # After exiting, context is CLEARED (not restored to pre-existing)
        assert get_search_context() is None

    def test_context_manager_clears_on_exception(self):
        """Context is cleared even if an exception occurs inside the block."""
        try:
            with search_context({"research_id": "will-fail"}):
                raise ValueError("test error")
        except ValueError:
            pass

        assert get_search_context() is None


class TestShallowCopySemantics:
    """Tests documenting shallow copy behavior of get_search_context."""

    def test_shallow_copy_allows_nested_mutation_leakage(self):
        """get_search_context() returns a shallow copy — top-level keys
        are independent, but nested dict mutations leak to the original."""
        original = {
            "research_id": "test",
            "metadata": {"depth": 1, "tags": ["a"]},
        }
        set_search_context(original)

        retrieved = get_search_context()

        # Top-level mutation doesn't leak
        retrieved["research_id"] = "modified"
        assert get_search_context()["research_id"] == "test"

        # But nested mutation DOES leak (shallow copy)
        retrieved["metadata"]["depth"] = 999
        assert get_search_context()["metadata"]["depth"] == 999

        clear_search_context()
