# #region Test.LLMParse [C:3] [TYPE Module] [SEMANTICS test, translate, llm, parse, json, recovery]
# @BRIEF Tests for _llm_parse.py — parse_llm_response, markdown recovery, truncated row recovery.
# @RELATION BINDS_TO -> [_llm_parse]

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

import json
import pytest

from src.plugins.translate._llm_parse import (
    parse_llm_response,
    _recover_from_markdown,
    _recover_truncated_rows,
)


class TestRecoverFromMarkdown:
    """_recover_from_markdown — extract JSON from markdown code blocks."""

    def test_json_code_block(self):
        """Standard ```json code block."""
        text = "```json\n{\"rows\": [{\"row_id\": 1, \"translation\": \"hola\"}]}\n```"
        result = _recover_from_markdown(text)
        assert result is not None
        assert result["rows"][0]["row_id"] == 1

    def test_no_marker_code_block(self):
        """Code block without json marker."""
        text = "```\n{\"rows\": [{\"row_id\": 1}]}\n```"
        result = _recover_from_markdown(text)
        assert result is not None
        assert len(result["rows"]) == 1

    def test_no_code_block(self):
        """Plain text without code block."""
        result = _recover_from_markdown("just plain text")
        assert result is None

    def test_invalid_json_in_block(self):
        """Code block with invalid JSON returns None."""
        text = "```json\n{invalid json}\n```"
        result = _recover_from_markdown(text)
        assert result is None

    def test_empty_block(self):
        """Empty code block returns None."""
        text = "```json\n```"
        result = _recover_from_markdown(text)
        assert result is None

    def test_inline_text_before_after(self):
        """Text before and after code block still extracts JSON."""
        text = "some text\n```json\n{\"rows\": []}\n```\nmore text"
        result = _recover_from_markdown(text)
        assert result is not None


class TestRecoverTruncatedRows:
    """_recover_truncated_rows — extract complete rows from truncated JSON."""

    def test_recover_single_row(self):
        """Single complete row extracted from truncated text."""
        text = 'some text {"row_id": 1, "translation": "hola"} more text'
        result = _recover_truncated_rows(text, 1, "length")
        assert result is not None
        assert len(result["rows"]) == 1

    def test_recover_multiple_rows(self):
        """Multiple rows extracted."""
        text = '{"row_id": 1, "translation": "hola"} {"row_id": 2, "translation": "adios"}'
        result = _recover_truncated_rows(text, 2, "length")
        assert result is not None
        assert len(result["rows"]) == 2

    def test_no_rows_found(self):
        """No row-like patterns found."""
        result = _recover_truncated_rows("completely invalid text", 1, "length")
        assert result is None

    def test_invalid_row_patterns_skipped(self):
        """Invalid JSON within row-like patterns is skipped."""
        text = '{"row_id": 1, invalid} {"row_id": 2, "translation": "ok"}'
        result = _recover_truncated_rows(text, 1, "length")
        assert result is not None
        assert len(result["rows"]) == 1

    def test_empty_text(self):
        """Empty text returns None."""
        result = _recover_truncated_rows("", 1, "length")
        assert result is None

    def test_no_finish_reason(self):
        """Works without finish_reason."""
        text = '{"row_id": 1, "translation": "hola"}'
        result = _recover_truncated_rows(text, 1, None)
        assert result is not None


class TestParseLLMResponse:
    """parse_llm_response — full LLM response parsing pipeline."""

    def test_valid_json_direct(self):
        """Direct valid JSON response."""
        response = json.dumps({
            "rows": [
                {"row_id": 1, "ru": "привет", "detected_source_language": "en"},
                {"row_id": 2, "ru": "мир", "detected_source_language": "en"},
            ]
        })
        result = parse_llm_response(response, expected_count=2, target_languages=["ru"])
        assert len(result) == 2
        assert result["1"]["ru"] == "привет"
        assert result["2"]["ru"] == "мир"

    def test_missing_rows_key(self):
        """No 'rows' key returns empty dict (defaults to empty list)."""
        result = parse_llm_response("{}", expected_count=1)
        assert result == {}

    def test_rows_not_a_list(self):
        """Rows key is not a list raises ValueError."""
        with pytest.raises(ValueError, match="rows"):
            parse_llm_response('{"rows": "not_a_list"}', expected_count=1)

    def test_markdown_code_block(self):
        """Response wrapped in markdown code block."""
        text = "```json\n{\"rows\": [{\"row_id\": 1, \"ru\": \"привет\"}]}\n```"
        result = parse_llm_response(text, expected_count=1, target_languages=["ru"])
        assert len(result) == 1
        assert result["1"]["ru"] == "привет"

    def test_truncated_recovery(self):
        """Truncated JSON recovers complete rows."""
        text = '{"row_id": 1, "ru": "привет"} {"row_id": 2, "ru": "мир"'
        result = parse_llm_response(text, expected_count=2, target_languages=["ru"])
        assert len(result) >= 1

    def test_invalid_json_fallback_to_markdown(self):
        """Invalid JSON falls through to markdown recovery."""
        text = "some text\n```json\n{\"rows\": [{\"row_id\": 1, \"ru\": \"hola\"}]}\n```"
        result = parse_llm_response(text, expected_count=1, target_languages=["ru"])
        assert len(result) == 1

    def test_row_without_id_skipped(self):
        """Row without row_id is skipped."""
        response = json.dumps({
            "rows": [
                {"translation": "no_id"},
                {"row_id": 2, "ru": "val", "detected_source_language": "en"},
            ]
        })
        result = parse_llm_response(response, expected_count=2, target_languages=["ru"])
        assert len(result) == 1
        assert "2" in result

    def test_no_language_data_falls_back_to_translation(self):
        """Row without language key falls back to 'translation' key."""
        response = json.dumps({
            "rows": [
                {"row_id": 1, "translation": "hola", "detected_source_language": "en"},
            ]
        })
        result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
        assert result["1"]["translation"] == "hola"

    def test_no_language_or_translation_skipped(self):
        """Row with neither language nor translation is skipped."""
        response = json.dumps({
            "rows": [
                {"row_id": 1, "detected_source_language": "en"},
            ]
        })
        result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
        assert len(result) == 0

    def test_detected_language_defaults(self):
        """Missing detected_source_language defaults to 'und'."""
        response = json.dumps({
            "rows": [
                {"row_id": 1, "ru": "привет"},
            ]
        })
        result = parse_llm_response(response, expected_count=1, target_languages=["ru"])
        assert result["1"]["detected_source_language"] == "und"

    def test_fully_failed_parse_raises(self):
        """Completely unparseable response raises ValueError."""
        with pytest.raises(ValueError, match="not valid JSON"):
            parse_llm_response("!@#$%^&*", expected_count=1)

    def test_empty_target_languages(self):
        """Empty target_languages uses 'translation' fallback."""
        response = json.dumps({
            "rows": [
                {"row_id": 1, "translation": "hola", "detected_source_language": "en"},
            ]
        })
        result = parse_llm_response(response, expected_count=1, target_languages=[])
        assert result["1"]["translation"] == "hola"

    def test_empty_rows(self):
        """Empty rows array returns empty dict."""
        response = json.dumps({"rows": []})
        result = parse_llm_response(response, expected_count=1)
        assert result == {}
