"""Comprehensive coverage tests for json_utils.py.

Targets edge cases and code paths not covered by existing test files:
- get_llm_response_text: text=None fallthrough, non-string .text, objects with neither attr
- extract_json: cleaning pipeline ordering, artifact cleaning after bracket extraction,
  multiple bracket pairs, expected_type interactions with cleaning
- _remove_think_tags: multiple think blocks, nested tags, adjacent tags, whitespace handling
- _strip_code_fences: multiple ```json blocks, ```json without closing, edge split behavior
- _extract_by_brackets: same-position brackets, single char between brackets
- _clean_llm_json_artifacts: combined artifacts, edge regex patterns, trailing ellipsis
"""

import json
from unittest.mock import MagicMock


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


# ---------------------------------------------------------------------------
# get_llm_response_text — coverage gaps
# ---------------------------------------------------------------------------


class TestGetLlmResponseTextCoverage:
    """Cover paths in get_llm_response_text not hit by existing tests."""

    def test_text_attribute_none_falls_to_str(self):
        """When .content is absent and .text is None, falls through to str()."""
        obj = MagicMock(spec=[])
        obj.text = None
        result = get_llm_response_text(obj)
        # Falls through both branches to str(response)
        assert isinstance(result, str)

    def test_object_without_content_or_text(self):
        """Object with neither .content nor .text uses str()."""

        class Bare:
            pass

        obj = Bare()
        result = get_llm_response_text(obj)
        assert isinstance(result, str)
        # str() of an object includes class name
        assert "Bare" in result

    def test_non_string_text_attribute_converted(self):
        """Non-string .text attribute is converted via str()."""
        obj = MagicMock(spec=[])
        obj.text = [1, 2, 3]
        result = get_llm_response_text(obj)
        assert isinstance(result, str)
        assert "1" in result

    def test_content_is_empty_list(self):
        """Content that is an empty list (truthy check: empty list is falsy but not None)."""
        obj = MagicMock()
        obj.content = []
        # [] is not None, so the content branch is taken; [] is not a str, so str([]) = "[]"
        result = get_llm_response_text(obj)
        assert result == "[]"

    def test_content_is_dict(self):
        """Content that is a dict gets stringified."""
        obj = MagicMock()
        obj.content = {"key": "value"}
        result = get_llm_response_text(obj)
        assert isinstance(result, str)
        assert "key" in result

    def test_content_is_boolean_false(self):
        """Content that is boolean False (not None) — should use it."""
        obj = MagicMock()
        obj.content = False
        result = get_llm_response_text(obj)
        assert result == "False"

    def test_content_is_zero(self):
        """Content that is integer 0 (not None) — should use it."""
        obj = MagicMock()
        obj.content = 0
        result = get_llm_response_text(obj)
        assert result == "0"

    def test_plain_integer_input(self):
        """Integer input has no .content or .text, uses str()."""
        result = get_llm_response_text(12345)
        assert result == "12345"

    def test_plain_list_input(self):
        """List input has no .content or .text, uses str()."""
        result = get_llm_response_text([1, 2])
        assert result == "[1, 2]"

    def test_multiple_think_tags_in_content(self):
        """Multiple think tag blocks in .content are all removed."""
        obj = MagicMock()
        obj.content = "<think>a</think>B<think>c</think>D"
        result = get_llm_response_text(obj)
        assert result == "BD"

    def test_think_tags_in_text_attribute(self):
        """Think tags in .text attribute are removed too."""
        obj = MagicMock(spec=[])
        obj.text = "<think>reasoning</think>answer"
        result = get_llm_response_text(obj)
        assert result == "answer"

    def test_content_with_only_think_tags(self):
        """Content that is entirely think tags results in empty string."""
        obj = MagicMock()
        obj.content = "<think>all reasoning no answer</think>"
        result = get_llm_response_text(obj)
        assert result == ""


# ---------------------------------------------------------------------------
# _remove_think_tags — coverage gaps
# ---------------------------------------------------------------------------


class TestRemoveThinkTagsCoverage:
    """Cover edge cases in _remove_think_tags regex processing."""

    def test_multiple_paired_tags(self):
        """Multiple separate paired think blocks."""
        text = "<think>a</think>X<think>b</think>Y"
        assert _remove_think_tags(text) == "XY"

    def test_nested_think_tags(self):
        """Nested think tags — outer pair consumed by DOTALL regex."""
        text = "<think>outer<think>inner</think>still outer</think>result"
        result = _remove_think_tags(text)
        # The .*? with DOTALL matches from first <think> to first </think>
        # Then orphaned tags are cleaned up
        assert "result" in result
        assert "<think>" not in result
        assert "</think>" not in result

    def test_only_closing_tag(self):
        """Only a closing </think> tag is removed."""
        assert _remove_think_tags("before</think>after") == "beforeafter"

    def test_only_opening_tag(self):
        """Only an opening <think> tag is removed."""
        assert _remove_think_tags("before<think>after") == "beforeafter"

    def test_multiple_orphaned_closing_tags(self):
        """Multiple orphaned closing tags."""
        text = "</think>A</think>B</think>C"
        result = _remove_think_tags(text)
        assert result == "ABC"

    def test_think_tags_with_whitespace_inside(self):
        """Think tags with leading/trailing whitespace inside."""
        text = "<think>  \n  reasoning  \n  </think>answer"
        result = _remove_think_tags(text)
        assert result == "answer"

    def test_empty_think_tags(self):
        """Empty <think></think> tags."""
        text = "<think></think>content"
        result = _remove_think_tags(text)
        assert result == "content"

    def test_think_tags_case_sensitive(self):
        """Think tags are case-sensitive — <THINK> is not removed."""
        text = "<THINK>loud</THINK>quiet"
        result = _remove_think_tags(text)
        assert "<THINK>" in result

    def test_think_tag_at_end_of_string(self):
        """Think tag at the very end."""
        text = "content<think>trailing"
        result = _remove_think_tags(text)
        assert "content" in result
        assert "<think>" not in result

    def test_whitespace_only_after_removal(self):
        """Result is whitespace-only after think tag removal — stripped to empty."""
        text = "<think>everything</think>   "
        result = _remove_think_tags(text)
        assert result == ""

    def test_think_tags_with_special_chars_inside(self):
        """Think tags containing JSON-like content inside."""
        text = '<think>{"reasoning": true}</think>{"answer": 42}'
        result = _remove_think_tags(text)
        assert result == '{"answer": 42}'


# ---------------------------------------------------------------------------
# _strip_code_fences — coverage gaps
# ---------------------------------------------------------------------------


class TestStripCodeFencesCoverage:
    """Cover edge cases in _strip_code_fences split logic."""

    def test_json_fence_no_closing_fence(self):
        """```json without closing ``` — split("```")[0] returns everything."""
        text = '```json\n{"key": "value"}'
        result = _strip_code_fences(text)
        # split("```json") -> ['', '\n{"key": "value"}']
        # parts[1].split("```")[0] -> '\n{"key": "value"}'
        assert '{"key": "value"}' in result

    def test_json_fence_empty_content(self):
        """```json with empty content."""
        text = "```json\n\n```"
        result = _strip_code_fences(text)
        assert result == ""

    def test_bare_fence_with_only_two_backtick_groups(self):
        """Only two ``` markers — fewer than 3 parts from split, returns original."""
        text = "some text ``` more text"
        result = _strip_code_fences(text)
        # split("```") gives ['some text ', ' more text'] — len < 3
        assert result == text

    def test_json_fence_with_extra_whitespace(self):
        """```json fence with extra whitespace around content."""
        text = '```json\n\n  {"key": "value"}  \n\n```'
        result = _strip_code_fences(text)
        assert result == '{"key": "value"}'

    def test_json_fence_preferred_over_bare(self):
        """```json fence is checked before bare ``` fence."""
        text = '```\nbare content\n```\n```json\n{"json": true}\n```'
        result = _strip_code_fences(text)
        # "```json" is found first in the elif, but actually "```json" is checked
        # first in the if branch — yes, ```json is checked first
        # The text contains "```json", so the if branch handles it
        # split("```json") -> ['```\nbare content\n```\n', '\n{"json": true}\n```']
        assert '{"json": true}' in result

    def test_code_fence_with_language_tag_not_json(self):
        """```python — no ```json match, falls to bare ``` path."""
        text = '```python\nx = {"key": "val"}\n```'
        result = _strip_code_fences(text)
        # split("```") gives ['', 'python\nx = {"key": "val"}\n', '']
        # parts[1] = 'python\nx = {"key": "val"}\n'
        assert "python" in result
        assert '"key"' in result

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

    def test_json_fence_immediately_adjacent(self):
        """```json``` with no content between markers."""
        text = "```json```"
        result = _strip_code_fences(text)
        # split("```json") -> ['', '```']
        # parts[1].split("```") -> ['', '']
        # parts[1].split("```")[0] -> ''
        assert result == ""


# ---------------------------------------------------------------------------
# _extract_by_brackets — coverage gaps
# ---------------------------------------------------------------------------


class TestExtractByBracketsCoverage:
    """Cover edge cases in _extract_by_brackets find/rfind logic."""

    def test_minimal_brackets(self):
        """Minimal content: just {} or []."""
        assert _extract_by_brackets("{}", "{", "}") == "{}"
        assert _extract_by_brackets("[]", "[", "]") == "[]"

    def test_single_char_content(self):
        """Single character between brackets."""
        assert _extract_by_brackets("{x}", "{", "}") == "{x}"

    def test_brackets_at_start_and_end(self):
        """Brackets at exact start and end of string."""
        text = '{"key": "value"}'
        assert _extract_by_brackets(text, "{", "}") == text

    def test_multiple_close_brackets_uses_last(self):
        """rfind finds the last closing bracket."""
        text = '{"a": {"b": 1}} extra }'
        result = _extract_by_brackets(text, "{", "}")
        # find("{")=0, rfind("}")=last "}" at end
        assert result == text

    def test_multiple_open_brackets_uses_first(self):
        """find finds the first opening bracket."""
        text = '{ extra {"a": 1}}'
        result = _extract_by_brackets(text, "{", "}")
        assert result == text

    def test_deeply_nested_brackets(self):
        """Deeply nested bracket structure."""
        text = '{"a": {"b": {"c": {"d": 1}}}}'
        result = _extract_by_brackets(text, "{", "}")
        assert result == text

    def test_mixed_bracket_types(self):
        """Square brackets inside curly braces."""
        text = '{"list": [1, [2, 3], 4]}'
        result = _extract_by_brackets(text, "{", "}")
        assert result == text

    def test_square_brackets_with_nested_objects(self):
        """Objects inside square brackets."""
        text = '[{"a": 1}, {"b": 2}]'
        result = _extract_by_brackets(text, "[", "]")
        assert result == text

    def test_empty_string(self):
        """Empty string input."""
        assert _extract_by_brackets("", "{", "}") is None
        assert _extract_by_brackets("", "[", "]") is None

    def test_only_open_no_close(self):
        """Only opening bracket, no closing."""
        assert _extract_by_brackets("[no close", "[", "]") is None

    def test_only_close_no_open(self):
        """Only closing bracket, no opening."""
        assert _extract_by_brackets("no open]", "[", "]") is None

    def test_adjacent_same_position(self):
        """Open and close at adjacent positions."""
        text = "prefix{}suffix"
        result = _extract_by_brackets(text, "{", "}")
        assert result == "{}"

    def test_open_equals_close_position(self):
        """When open_char == close_char scenario (degenerate)."""
        # Not a real use case, but tests the find/rfind logic
        text = "x|y|z"
        result = _extract_by_brackets(text, "|", "|")
        # find("|")=1, rfind("|")=3, 3 > 1 -> "|y|"
        assert result == "|y|"


# ---------------------------------------------------------------------------
# _clean_llm_json_artifacts — coverage gaps
# ---------------------------------------------------------------------------


class TestCleanLlmJsonArtifactsCoverage:
    """Cover edge cases in _clean_llm_json_artifacts regex patterns."""

    def test_trailing_comma_with_whitespace(self):
        """Trailing comma with various whitespace before closing bracket."""
        text = '{"a": 1 ,  \n  }'
        result = _clean_llm_json_artifacts(text)
        parsed = json.loads(result)
        assert parsed == {"a": 1}

    def test_multiple_trailing_commas(self):
        """Multiple trailing commas (nested structures)."""
        text = '{"a": [1, 2,], "b": 3,}'
        result = _clean_llm_json_artifacts(text)
        parsed = json.loads(result)
        assert parsed == {"a": [1, 2], "b": 3}

    def test_comment_at_end_of_line(self):
        """Comment at end of line with content before it."""
        text = '{"key": "value" // this is important\n}'
        result = _clean_llm_json_artifacts(text)
        assert "//" not in result
        parsed = json.loads(result)
        assert parsed == {"key": "value"}

    def test_multiple_comments_on_different_lines(self):
        """Multiple // comments on separate lines."""
        text = '{\n"a": 1, // first\n"b": 2 // second\n}'
        result = _clean_llm_json_artifacts(text)
        assert "//" not in result
        parsed = json.loads(result)
        assert parsed == {"a": 1, "b": 2}

    def test_comment_on_empty_line(self):
        """Comment on a line by itself."""
        text = '{\n// this is a comment\n"a": 1\n}'
        result = _clean_llm_json_artifacts(text)
        assert "//" not in result
        parsed = json.loads(result)
        assert parsed == {"a": 1}

    def test_ellipsis_at_end_of_array(self):
        """Trailing ellipsis at end of array."""
        text = "[1, 2, 3, ...]"
        result = _clean_llm_json_artifacts(text)
        assert "..." not in result
        parsed = json.loads(result)
        assert parsed == [1, 2, 3]

    def test_ellipsis_at_start_of_array(self):
        """Leading ellipsis at start of array — unusual but possible."""
        text = "[..., 1, 2]"
        result = _clean_llm_json_artifacts(text)
        assert "..." not in result

    def test_multiple_dots_more_than_three(self):
        """More than three dots: ....."""
        text = "[1, 2, .....]"
        result = _clean_llm_json_artifacts(text)
        # The regex \.\.\.+ matches 3 or more dots
        assert "..." not in result

    def test_combined_trailing_comma_and_comment(self):
        """Both trailing comma and comment in same JSON."""
        text = '{"a": 1, // comment\n"b": 2,}'
        result = _clean_llm_json_artifacts(text)
        parsed = json.loads(result)
        assert parsed == {"a": 1, "b": 2}

    def test_combined_all_artifacts(self):
        """All artifact types combined."""
        text = "[\n1, // first item\n2,\n...,\n]"
        result = _clean_llm_json_artifacts(text)
        assert "//" not in result
        assert "..." not in result
        parsed = json.loads(result)
        assert parsed == [1, 2]

    def test_no_artifacts_leaves_valid_json_unchanged(self):
        """Clean JSON is not modified."""
        text = '{"key": "value", "num": 42}'
        assert _clean_llm_json_artifacts(text) == text

    def test_single_slash_not_treated_as_comment(self):
        """Single / is not a comment delimiter."""
        text = '{"path": "/api/v1"}'
        result = _clean_llm_json_artifacts(text)
        assert "/api/v1" in result

    def test_empty_object(self):
        """Empty object is unchanged."""
        text = "{}"
        assert _clean_llm_json_artifacts(text) == "{}"

    def test_empty_array(self):
        """Empty array is unchanged."""
        text = "[]"
        assert _clean_llm_json_artifacts(text) == "[]"

    def test_ellipsis_between_items_uses_first_regex(self):
        """Ellipsis between commas uses the comma-preserving regex."""
        text = "[1, ..., 2]"
        result = _clean_llm_json_artifacts(text)
        assert "..." not in result
        # First regex: ',\\s*"?\\.\\.\\.+"?\\s*,' -> ','
        # Should produce [1, 2] ideally

    def test_quoted_ellipsis_between_items(self):
        """Quoted ellipsis "..." between items."""
        text = '[1, "...", 2]'
        result = _clean_llm_json_artifacts(text)
        assert "..." not in result


# ---------------------------------------------------------------------------
# extract_json — comprehensive coverage
# ---------------------------------------------------------------------------


class TestExtractJsonCoverage:
    """Cover remaining code paths in extract_json."""

    # --- Cleaning pipeline ordering ---

    def test_code_fences_stripped_before_think_tags(self):
        """Code fences are stripped, then think tags removed."""
        text = '```json\n<think>reasoning</think>{"key": "value"}\n```'
        result = extract_json(text)
        assert result == {"key": "value"}

    def test_think_tags_inside_code_fence(self):
        """Think tags inside code fence content."""
        text = '```json\n{"key": "<think>not removed</think>"}\n```'
        # After fence stripping: {"key": "<think>not removed</think>"}
        # After think tag removal: {"key": ""}
        result = extract_json(text)
        assert result is not None
        assert isinstance(result, dict)

    # --- Direct parse success paths ---

    def test_direct_parse_dict_no_expected_type(self):
        """Direct parse succeeds with dict, no expected_type."""
        result = extract_json('{"a": 1}')
        assert result == {"a": 1}

    def test_direct_parse_list_no_expected_type(self):
        """Direct parse succeeds with list, no expected_type."""
        result = extract_json("[1, 2, 3]")
        assert result == [1, 2, 3]

    def test_direct_parse_matching_expected_type(self):
        """Direct parse succeeds and matches expected_type."""
        result = extract_json('{"a": 1}', expected_type=dict)
        assert result == {"a": 1}

    def test_direct_parse_type_mismatch_falls_to_bracket_extraction(self):
        """Direct parse succeeds as dict but expected list — falls through."""
        text = '{"items": [1, 2]}'
        result = extract_json(text, expected_type=list)
        # Direct parse gives dict, but expected list
        # Bracket extraction with "[" first finds [1, 2]
        assert result == [1, 2]

    # --- Bracket extraction with artifact cleaning ---

    def test_bracket_extraction_then_artifact_cleaning(self):
        """Bracket extraction finds JSON but it has artifacts, cleaning fixes it."""
        text = 'Result: {"a": 1, "b": 2,} end'
        result = extract_json(text)
        assert result == {"a": 1, "b": 2}

    def test_bracket_extraction_with_comment_artifact(self):
        """Bracket extraction finds JSON with // comment, cleaning fixes it."""
        text = 'Output: {"a": 1 // comment\n} done'
        result = extract_json(text)
        assert result == {"a": 1}

    def test_bracket_extraction_with_ellipsis_artifact(self):
        """Bracket extraction finds array with ellipsis, cleaning fixes it."""
        text = "Items: [1, 2, 3, ...] end"
        result = extract_json(text, expected_type=list)
        assert result == [1, 2, 3]

    # --- expected_type bracket ordering ---

    def test_expected_type_list_tries_square_first(self):
        """expected_type=list makes bracket_pairs start with ('[', ']')."""
        # Both valid JSON types present
        text = 'prefix {"d": 1} middle [10, 20] suffix'
        result = extract_json(text, expected_type=list)
        assert isinstance(result, list)
        assert result == [10, 20]

    def test_expected_type_dict_tries_curly_first(self):
        """expected_type=dict makes bracket_pairs start with ('{', '}')."""
        text = 'prefix [10, 20] middle {"d": 1} suffix'
        result = extract_json(text, expected_type=dict)
        assert isinstance(result, dict)

    def test_no_expected_type_tries_curly_first(self):
        """No expected_type tries curly braces first (default ordering)."""
        text = '{"a": 1}'
        result = extract_json(text, expected_type=None)
        assert result == {"a": 1}

    # --- Edge cases for None/empty ---

    def test_none_input(self):
        assert extract_json(None) is None

    def test_empty_string(self):
        assert extract_json("") is None

    def test_whitespace_tabs_newlines(self):
        assert extract_json("\t\n  \r\n") is None

    def test_only_text_no_json(self):
        assert extract_json("This is just plain text without any JSON.") is None

    # --- Scalar JSON values ---

    def test_json_number_returns_none(self):
        """JSON number is valid JSON but not dict/list."""
        assert extract_json("42") is None

    def test_json_string_returns_none(self):
        """JSON string is valid JSON but not dict/list."""
        assert extract_json('"hello"') is None

    def test_json_boolean_returns_none(self):
        """JSON boolean is valid JSON but not dict/list."""
        assert extract_json("true") is None

    def test_json_null_returns_none(self):
        """JSON null is valid JSON but not dict/list."""
        assert extract_json("null") is None

    # --- Complex nested JSON ---

    def test_deeply_nested_dict(self):
        text = '{"a": {"b": {"c": {"d": {"e": 1}}}}}'
        result = extract_json(text)
        assert result == {"a": {"b": {"c": {"d": {"e": 1}}}}}

    def test_list_of_lists(self):
        text = "[[1, 2], [3, 4], [5, 6]]"
        result = extract_json(text)
        assert result == [[1, 2], [3, 4], [5, 6]]

    def test_mixed_nested_structure(self):
        text = '[{"name": "A", "values": [1, 2]}, {"name": "B", "values": [3]}]'
        result = extract_json(text, expected_type=list)
        assert len(result) == 2
        assert result[0]["name"] == "A"
        assert result[1]["values"] == [3]

    # --- Unicode and special characters ---

    def test_unicode_keys_and_values(self):
        text = '{"name": "日本語", "city": "Zürich"}'
        result = extract_json(text)
        assert result == {"name": "日本語", "city": "Zürich"}

    def test_escaped_quotes_in_values(self):
        text = '{"quote": "He said \\"hello\\""}'
        result = extract_json(text)
        assert result == {"quote": 'He said "hello"'}

    def test_newlines_in_values(self):
        text = '{"text": "line1\\nline2"}'
        result = extract_json(text)
        assert result == {"text": "line1\nline2"}

    # --- Real LLM output patterns ---

    def test_deepseek_think_then_code_fence(self):
        """DeepSeek-style: long think block then code fence."""
        text = (
            "<think>\nLet me analyze this carefully.\n"
            "The user wants a JSON response.\n"
            "I should structure it as follows.\n"
            "</think>\n\n"
            '```json\n{"answer": "structured response", "confidence": 0.95}\n```'
        )
        result = extract_json(text)
        assert result == {"answer": "structured response", "confidence": 0.95}

    def test_chatgpt_style_prose_then_json(self):
        """ChatGPT-style: conversational intro then JSON."""
        text = (
            "Based on my analysis, here is the structured output:\n\n"
            '{"summary": "key findings", "relevance_scores": [0.9, 0.7, 0.5]}'
        )
        result = extract_json(text)
        assert result["summary"] == "key findings"
        assert result["relevance_scores"] == [0.9, 0.7, 0.5]

    def test_llm_numbering_before_json(self):
        """LLM provides numbered reasoning then JSON."""
        text = (
            "1. First I considered X\n"
            "2. Then I evaluated Y\n"
            "3. My conclusion:\n\n"
            '{"result": "Y is better", "score": 8.5}'
        )
        result = extract_json(text)
        assert result == {"result": "Y is better", "score": 8.5}

    def test_json_with_markdown_bold_in_prose(self):
        """JSON embedded in text with markdown formatting."""
        text = '**Result**: {"key": "value"}\n\n*Note: this is important*'
        result = extract_json(text)
        assert result == {"key": "value"}

    # --- Artifact cleaning integration ---

    def test_trailing_comma_in_nested_structure(self):
        """Trailing commas in nested object."""
        text = '{"outer": {"inner": [1, 2,], "key": "val",},}'
        result = extract_json(text)
        assert result == {"outer": {"inner": [1, 2], "key": "val"}}

    def test_comments_in_multiline_json(self):
        """Comments in multiline JSON object."""
        text = '{\n  "name": "test", // the name\n  "value": 42 // the value\n}'
        result = extract_json(text)
        assert result == {"name": "test", "value": 42}

    def test_ellipsis_in_large_array_pattern(self):
        """LLM truncates array with ellipsis."""
        text = "[1, 2, 3, ...]"
        result = extract_json(text, expected_type=list)
        assert result == [1, 2, 3]

    # --- Multiple JSON objects in text ---

    def test_two_separate_dicts_in_prose(self):
        """Two separate dicts — bracket extraction spans both (may fail)."""
        text = '{"a": 1} some text {"b": 2}'
        result = extract_json(text)
        # find("{")=0, rfind("}")=end -> spans both, invalid JSON -> None
        assert result is None

    def test_dict_then_list_expected_dict(self):
        """Dict then list, expected_type=dict."""
        text = '{"a": 1}\n[1, 2, 3]'
        result = extract_json(text, expected_type=dict)
        # Bracket extraction for {} spans from first { to last }
        # which is just {"a": 1} since last } is at pos 7
        # Actually rfind("}") finds the } at position 7 (last one)
        # Direct parse fails because the whole text isn't valid JSON
        # Then bracket extraction for "{" finds {"a": 1}\n[1, 2, 3]
        # Wait — find("{")=0, rfind("}")=7, text[0:8] = '{"a": 1}'
        assert result == {"a": 1}

    # --- Completely invalid inputs ---

    def test_html_without_json(self):
        assert extract_json("<html><body>No JSON</body></html>") is None

    def test_xml_without_json(self):
        assert extract_json("<root><item>No JSON</item></root>") is None

    def test_brackets_with_non_json_content(self):
        """Brackets containing non-JSON content."""
        assert extract_json("{this is not: json: at: all}") is None

    def test_square_brackets_with_non_json(self):
        """Square brackets with non-JSON content."""
        assert extract_json("[not, valid, json, either]") is None

    # --- Whitespace handling ---

    def test_json_with_leading_trailing_whitespace(self):
        """JSON with lots of whitespace around it."""
        text = '   \n\n  {"key": "value"}  \n\n  '
        result = extract_json(text)
        assert result == {"key": "value"}

    def test_json_with_internal_whitespace(self):
        """JSON with lots of internal whitespace."""
        text = '{\n\n  "key"  :  "value"  \n\n}'
        result = extract_json(text)
        assert result == {"key": "value"}

    # --- Empty structures ---

    def test_empty_dict(self):
        result = extract_json("{}")
        assert result == {}

    def test_empty_list(self):
        result = extract_json("[]")
        assert result == []

    def test_empty_dict_expected_dict(self):
        result = extract_json("{}", expected_type=dict)
        assert result == {}

    def test_empty_list_expected_list(self):
        result = extract_json("[]", expected_type=list)
        assert result == []

    def test_empty_dict_expected_list_returns_none(self):
        """Empty dict when list expected — falls through."""
        result = extract_json("{}", expected_type=list)
        # Direct parse gives dict, type mismatch
        # Bracket extraction for "[]" finds nothing
        assert result is None

    def test_empty_list_expected_dict_returns_none(self):
        """Empty list when dict expected — falls through."""
        result = extract_json("[]", expected_type=dict)
        assert result is None

    # --- Large/complex real patterns ---

    def test_search_result_format(self):
        """Typical search result JSON format from LDR."""
        text = (
            "```json\n"
            "{\n"
            '  "queries": [\n'
            '    {"query": "machine learning basics", "priority": 1},\n'
            '    {"query": "neural network architecture", "priority": 2}\n'
            "  ],\n"
            '  "reasoning": "These queries cover the fundamentals"\n'
            "}\n"
            "```"
        )
        result = extract_json(text)
        assert result is not None
        assert len(result["queries"]) == 2
        assert result["queries"][0]["priority"] == 1

    def test_relevance_indices_format(self):
        """Typical relevance indices list from LDR."""
        text = (
            "<think>Let me rank these by relevance...</think>\n"
            "```json\n[0, 3, 1, 7, 2]\n```"
        )
        result = extract_json(text, expected_type=list)
        assert result == [0, 3, 1, 7, 2]

    def test_boolean_and_null_values(self):
        """JSON with boolean and null values."""
        text = '{"active": true, "deleted": false, "metadata": null}'
        result = extract_json(text)
        assert result == {"active": True, "deleted": False, "metadata": None}

    def test_numeric_values_int_and_float(self):
        """JSON with integer and float values."""
        text = '{"count": 42, "score": 3.14, "negative": -7}'
        result = extract_json(text)
        assert result == {"count": 42, "score": 3.14, "negative": -7}

    def test_artifact_cleaning_only_on_bracket_extracted_text(self):
        """Artifact cleaning is only attempted on bracket-extracted text,
        not on the original full text."""
        # This text has trailing comma but is surrounded by prose
        text = "Result: [1, 2, 3,] which is the answer"
        result = extract_json(text, expected_type=list)
        assert result == [1, 2, 3]

    def test_cleaning_does_not_run_when_bracket_parse_succeeds(self):
        """If bracket-extracted text parses fine, cleaning is skipped."""
        text = 'Prefix: {"a": 1, "b": 2} suffix'
        result = extract_json(text)
        assert result == {"a": 1, "b": 2}

    def test_cleaning_skipped_when_cleaned_equals_extracted(self):
        """If cleaning produces the same string, parsing is not retried."""
        # Invalid JSON that cleaning cannot fix
        text = "{key: value}"
        result = extract_json(text)
        assert result is None


# ---------------------------------------------------------------------------
# Integration: get_llm_response_text -> extract_json pipeline
# ---------------------------------------------------------------------------


class TestPipelineIntegration:
    """Test the common pattern of extracting text then parsing JSON."""

    def test_aimessage_to_json(self):
        """Simulate LangChain AIMessage -> extract_json pipeline."""
        msg = MagicMock()
        msg.content = '```json\n{"answer": "yes"}\n```'
        text = get_llm_response_text(msg)
        result = extract_json(text)
        assert result == {"answer": "yes"}

    def test_think_tags_cleaned_before_json_extraction(self):
        """Think tags removed by get_llm_response_text, then JSON extracted."""
        msg = MagicMock()
        msg.content = '<think>long reasoning</think>{"result": true}'
        text = get_llm_response_text(msg)
        result = extract_json(text)
        assert result == {"result": True}

    def test_none_response_produces_none_json(self):
        """None response -> empty string -> None JSON."""
        text = get_llm_response_text(None)
        result = extract_json(text)
        assert result is None

    def test_complex_deepseek_pipeline(self):
        """Full DeepSeek-style response through the pipeline."""
        msg = MagicMock()
        msg.content = (
            "<think>\nI need to analyze the search results.\n"
            "Result 1 is most relevant.\n"
            "Result 3 is partially relevant.\n"
            "</think>\n"
            "```json\n"
            "[0, 2]\n"
            "```"
        )
        text = get_llm_response_text(msg)
        result = extract_json(text, expected_type=list)
        assert result == [0, 2]
