"""
Tests for realistic LLM output patterns in JSON parsing that are not covered
by the existing 75 tests across 3 files.

Source: src/local_deep_research/utilities/json_utils.py
"""

from local_deep_research.utilities.json_utils import (
    extract_json,
    get_llm_response_text,
    _extract_by_brackets,
    _strip_code_fences,
)


# ---------------------------------------------------------------------------
# Think tags wrapping code fences (real DeepSeek pattern)
# ---------------------------------------------------------------------------


class TestThinkTagsWithCodeFences:
    """Real LLM patterns where think tags wrap JSON code fences."""

    def test_think_tags_wrapping_json_code_fence(self):
        """<think>...</think> followed by ```json ... ``` — real DeepSeek pattern."""
        text = '<think>Let me analyze this...</think>\n```json\n{"key": "value"}\n```'
        result = extract_json(text)
        assert result == {"key": "value"}

    def test_think_tags_wrapping_list_code_fence(self):
        """Think tags before a JSON list in code fence."""
        text = (
            '<think>Planning response...</think>\n```json\n["a", "b", "c"]\n```'
        )
        result = extract_json(text)
        assert result == ["a", "b", "c"]

    def test_nested_think_content_with_json(self):
        """Think tags with JSON-like content inside, real JSON outside."""
        text = '<think>Maybe {"wrong": true}?</think>\n{"correct": true}'
        result = extract_json(text)
        assert result == {"correct": True}

    def test_unclosed_think_tag_before_json(self):
        """<think> without closing </think> before JSON."""
        text = '<think>some reasoning\n{"key": "value"}'
        result = extract_json(text)
        assert result == {"key": "value"}


# ---------------------------------------------------------------------------
# Code fences with non-json language tags
# ---------------------------------------------------------------------------


class TestNonJsonCodeFences:
    """JSON inside code fences tagged with non-json language."""

    def test_typescript_tagged_code_fence(self):
        """```typescript wrapping JSON."""
        text = '```typescript\n{"type": "config"}\n```'
        # _strip_code_fences looks for ```json or plain ``` (3+ backticks)
        # For non-json tags, it falls to the plain ``` path
        result = extract_json(text)
        # The stripping gets "typescript\n{...}" which isn't valid JSON
        # but bracket extraction should still find the JSON
        assert result == {"type": "config"}

    def test_plain_code_fence(self):
        """Plain ``` without language tag."""
        text = '```\n{"plain": true}\n```'
        result = extract_json(text)
        assert result == {"plain": True}


# ---------------------------------------------------------------------------
# JSON with preamble/trailing text (common LLM pattern)
# ---------------------------------------------------------------------------


class TestPreambleAndTrailing:
    """JSON surrounded by conversational text."""

    def test_apologize_preamble(self):
        """'I apologize...' preamble before JSON."""
        text = 'I apologize for the confusion. Here is the corrected JSON:\n{"fixed": true}'
        result = extract_json(text)
        assert result == {"fixed": True}

    def test_trailing_explanation(self):
        """JSON followed by explanation text."""
        text = '{"result": 42}\n\nThis represents the answer to the question.'
        result = extract_json(text)
        assert result == {"result": 42}

    def test_preamble_and_trailing(self):
        """Both preamble and trailing text around JSON."""
        text = 'Sure! Here it is:\n{"data": [1, 2, 3]}\nHope this helps!'
        result = extract_json(text)
        assert result == {"data": [1, 2, 3]}


# ---------------------------------------------------------------------------
# get_llm_response_text edge cases
# ---------------------------------------------------------------------------


class TestGetLlmResponseText:
    """Edge cases for extracting text from LLM response objects."""

    def test_none_returns_empty(self):
        """None input returns empty string."""
        assert get_llm_response_text(None) == ""

    def test_content_attribute_used(self):
        """Object with .content attribute uses it."""
        obj = type("Resp", (), {"content": "hello"})()
        assert get_llm_response_text(obj) == "hello"

    def test_text_attribute_used(self):
        """Object with .text but no .content uses .text."""
        obj = type("Resp", (), {"text": "world", "content": None})()
        assert get_llm_response_text(obj) == "world"

    def test_both_none_falls_to_str(self):
        """.content and .text both None -> falls to str()."""
        obj = type("Resp", (), {"content": None, "text": None})()
        result = get_llm_response_text(obj)
        # str() of the object
        assert isinstance(result, str)

    def test_empty_string_content_used(self):
        """Empty string .content is used as-is (not None)."""
        obj = type("Resp", (), {"content": ""})()
        assert get_llm_response_text(obj) == ""

    def test_think_tags_removed(self):
        """Think tags in content are removed."""
        obj = type("Resp", (), {"content": "<think>reasoning</think>answer"})()
        assert get_llm_response_text(obj) == "answer"

    def test_non_string_content_converted(self):
        """Non-string .content is converted to string."""
        obj = type("Resp", (), {"content": 42})()
        result = get_llm_response_text(obj)
        assert result == "42"


# ---------------------------------------------------------------------------
# _extract_by_brackets edge cases
# ---------------------------------------------------------------------------


class TestExtractByBrackets:
    """Edge cases for bracket extraction."""

    def test_adjacent_braces(self):
        """Adjacent {} objects — extracts outermost."""
        result = _extract_by_brackets('{"a": 1}{"b": 2}', "{", "}")
        # find("{")=0, rfind("}")=17 -> extracts whole string
        assert '"a"' in result
        assert '"b"' in result

    def test_only_open_bracket(self):
        """Only open bracket, no close -> returns None."""
        result = _extract_by_brackets("{no close", "{", "}")
        assert result is None

    def test_empty_string(self):
        """Empty string -> returns None."""
        result = _extract_by_brackets("", "{", "}")
        assert result is None

    def test_close_before_open(self):
        """Close bracket before open -> returns None."""
        result = _extract_by_brackets("} then {", "{", "}")
        # find("{")=7, rfind("}")=0, 0 < 7 -> None
        assert result is None


# ---------------------------------------------------------------------------
# _strip_code_fences edge cases
# ---------------------------------------------------------------------------


class TestStripCodeFences:
    """Edge cases for code fence stripping."""

    def test_no_code_fences(self):
        """Text without code fences returned as-is."""
        text = '{"key": "value"}'
        assert _strip_code_fences(text) == text

    def test_json_code_fence(self):
        """```json ... ``` stripped correctly."""
        text = '```json\n{"key": "value"}\n```'
        result = _strip_code_fences(text)
        assert result == '{"key": "value"}'

    def test_multiple_code_fences_first_extracted(self):
        """Multiple code fences — first one extracted."""
        text = '```json\n{"first": 1}\n```\n\n```json\n{"second": 2}\n```'
        result = _strip_code_fences(text)
        assert '{"first": 1}' in result


# ---------------------------------------------------------------------------
# extract_json with expected_type
# ---------------------------------------------------------------------------


class TestExtractJsonExpectedType:
    """Test expected_type parameter behavior."""

    def test_expect_list_gets_list(self):
        """expected_type=list prefers list extraction."""
        text = '{"dict": true}\n[1, 2, 3]'
        result = extract_json(text, expected_type=list)
        assert result == [1, 2, 3]

    def test_expect_dict_gets_dict(self):
        """expected_type=dict prefers dict extraction."""
        text = '[1, 2]\n{"dict": true}'
        result = extract_json(text, expected_type=dict)
        assert result == {"dict": True}

    def test_none_type_accepts_either(self):
        """expected_type=None accepts first valid JSON."""
        text = '{"key": "value"}'
        result = extract_json(text, expected_type=None)
        assert result == {"key": "value"}

    def test_empty_input_returns_none(self):
        """Empty string returns None."""
        assert extract_json("") is None
        assert extract_json("   ") is None

    def test_no_valid_json_returns_none(self):
        """Text without valid JSON returns None."""
        assert extract_json("just some text without json") is None
