# region InlineCorrectionTests [TYPE Module]
# @SEMANTICS: test, translate, correction, inline, bulk
# @PURPOSE: Tests for inline correction (T109-T116, T117): single correction, dictionary submission, bulk replace.
# @LAYER Domain
# @RELATION  BINDS_TO -> [InlineCorrectionService]
# @RELATION  BINDS_TO -> [BulkFindReplaceService]
# @TEST_CONTRACT  InlineCorrectionService -> submit_correction, duplicate detection
# @TEST_CONTRACT  BulkFindReplaceService -> preview, apply, atomic
# @TEST_EDGE  single_correction -> Creates dictionary entry with origin tracking
# @TEST_EDGE  duplicate_detection -> Conflict detected for existing term
# @TEST_EDGE  bulk_replace_preview -> Returns accurate affected records
# @TEST_EDGE  bulk_replace_apply -> Updates values and optionally submits to dictionary

from unittest.mock import MagicMock, patch

from src.models.translate import TranslationLanguage, TranslationRecord
from src.plugins.translate.service import BulkFindReplaceService, InlineCorrectionService


# region TestInlineCorrectionService [TYPE Class]
# @PURPOSE: Tests for InlineCorrectionService: single correction, duplicate detection, origin tracking.
class TestInlineCorrectionService:

    # region test_single_correction_to_dict [TYPE Function]
    # @BRIEF Single inline correction creates a dictionary entry with origin tracking.
    def test_single_correction_to_dict(self):
        db = MagicMock()

        # Mock language entry
        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old translation"
        lang_entry.final_value = None
        lang_entry.record_id = "rec-1"

        # Mock record
        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "original source text"
        record.source_object_name = ""
        record.source_object_id = "0"
        record.languages = [lang_entry]

        # Mock queries: lang_entry, record, then DictionaryEntry query (None = no conflict)
        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new translation",
            current_user="test-user",
        )

        assert result is not None
        assert result.get("action") == "created"
    # endregion test_single_correction_to_dict

    # region test_duplicate_detection [TYPE Function]
    # @PURPOSE: Duplicate source term in same dictionary raises conflict.
    def test_duplicate_detection(self):
        db = MagicMock()

        # Mock language entry
        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old translation"
        lang_entry.record_id = "rec-1"

        # Mock record
        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source text"
        record.source_object_name = ""
        record.source_object_id = "0"
        record.languages = [lang_entry]

        # Mock queries: lang_entry, record, then DictionaryEntry query returns existing (conflict)
        existing_entry = MagicMock()
        existing_entry.id = "entry-existing"
        existing_entry.source_term = "source text"
        existing_entry.target_term = "existing translation"

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, existing_entry]

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new translation",
            current_user="test-user",
        )

        assert result is not None
        assert result.get("action") == "conflict_detected"
    # endregion test_duplicate_detection

    # region test_correction_with_context_capture [TYPE Function]
    # @PURPOSE: Correction accepts context_data_override and usage_notes.
    def test_correction_with_context_capture(self):
        db = MagicMock()

        # Mock language entry
        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        # Mock record
        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source"
        record.source_object_name = ""
        record.source_object_id = "0"
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new",
            current_user="test-user",
            context_data_override={"category": "electronics"},
            usage_notes="Use for product descriptions",
        )

        assert result is not None
        assert result.get("action") == "created"
    # endregion test_correction_with_context_capture

# endregion TestInlineCorrectionService


# region TestBulkFindReplaceService [TYPE Class]
# @PURPOSE: Tests for BulkFindReplaceService: preview accuracy, atomic apply.
class TestBulkFindReplaceService:

    # region test_bulk_replace_preview_accuracy [TYPE Function]
    # @BRIEF Preview returns accurate list of affected records.
    def test_bulk_replace_preview_accuracy(self):
        db = MagicMock()

        # Mock language entries — need source_text key for preview matching
        def make_lang(lang_code, val):
            entry = MagicMock(spec=TranslationLanguage)
            entry.language_code = lang_code
            entry.final_value = val or ""
            entry.translated_value = val or ""
            entry.id = f"lang-{lang_code}"
            entry.record_id = f"rec-{lang_code}"
            return entry

        lang_ru = make_lang("ru", "Hello world")
        lang_en = make_lang("en", "not matched")

        rec = MagicMock(spec=TranslationRecord)
        rec.id = "rec-1"
        rec.source_sql = "Hello world"
        rec.languages = [lang_ru, lang_en]

        db.query.return_value.options.return_value.filter.return_value.all.return_value = [rec]

        # Mock run exists
        db.query.return_value.filter.return_value.first.return_value = MagicMock(id="run-1")

        result = BulkFindReplaceService.preview(
            db=db,
            run_id="run-1",
            pattern="Hello",
            is_regex=False,
            replacement_text="Hi",
            target_language="ru",
        )

        # Should find records with matching pattern
        assert isinstance(result, list)
    # endregion test_bulk_replace_preview_accuracy

    # region test_bulk_replace_apply_atomic [TYPE Function]
    # @BRIEF Apply updates values atomically.
    def test_bulk_replace_apply_atomic(self):
        db = MagicMock()

        def make_lang(lang_code, val):
            entry = MagicMock(spec=TranslationLanguage)
            entry.language_code = lang_code
            entry.final_value = val or ""
            entry.translated_value = val or ""
            entry.id = f"lang-{lang_code}"
            entry.record_id = "rec-1"
            return entry

        lang_ru = make_lang("ru", "Hello world")
        lang_en = make_lang("en", "not touched")

        rec = MagicMock(spec=TranslationRecord)
        rec.id = "rec-1"
        rec.source_sql = "Hello world"
        rec.languages = [lang_ru, lang_en]
        rec.target_sql = "Hello world"

        db.query.return_value.options.return_value.filter.return_value.all.return_value = [rec]
        db.query.return_value.filter.return_value.first.return_value = MagicMock(id="run-1")

        result = BulkFindReplaceService.apply(
            db=db,
            run_id="run-1",
            pattern="Hello",
            is_regex=False,
            replacement_text="Hi",
            target_language="ru",
            submit_to_dictionary=False,
            current_user="test-user",
        )

        assert result is not None
    # endregion test_bulk_replace_apply_atomic

    # region test_bulk_replace_submit_to_dict [TYPE Function]
    # @PURPOSE: Bulk replace with submit_to_dictionary creates dictionary entries.
    @patch("src.plugins.translate.service.InlineCorrectionService.submit_correction_to_dict")
    def test_bulk_replace_submit_to_dict(self, mock_submit_correction: MagicMock):
        db = MagicMock()

        def make_lang(lang_code, val):
            entry = MagicMock(spec=TranslationLanguage)
            entry.language_code = lang_code
            entry.final_value = val or ""
            entry.translated_value = val or ""
            entry.id = f"lang-{lang_code}"
            entry.record_id = "rec-1"
            return entry

        lang = make_lang("ru", "replace me")

        rec = MagicMock(spec=TranslationRecord)
        rec.id = "rec-1"
        rec.source_sql = "original"
        rec.languages = [lang]
        rec.target_sql = "replace me"

        db.query.return_value.options.return_value.filter.return_value.all.return_value = [rec]
        db.query.return_value.filter.return_value.first.return_value = MagicMock(id="run-1")

        mock_submit_correction.return_value = {"entry_id": "new-entry"}

        result = BulkFindReplaceService.apply(
            db=db,
            run_id="run-1",
            pattern="replace me",
            is_regex=False,
            replacement_text="replaced",
            target_language="ru",
            submit_to_dictionary=True,
            dictionary_id="dict-1",
            current_user="test-user",
        )

        assert result is not None
    # endregion test_bulk_replace_submit_to_dict

# endregion TestBulkFindReplaceService


# region TestContextAwareCorrection [TYPE Module]
# @SEMANTICS: test, translate, correction, context, jaccard, priority
# @PURPOSE: Tests for context-aware correction (T132): context capture, Jaccard similarity, priority flagging, truncation, context_source tagging.
# @LAYER Test
# @RELATION  BINDS_TO -> [InlineCorrectionService]
# @RELATION  BINDS_TO -> [ContextAwarePromptBuilder]
# @TEST_CONTRACT  InlineCorrectionService -> context capture from source row, context editing/removal
# @TEST_CONTRACT  ContextAwarePromptBuilder -> Jaccard similarity, priority flagging, truncation, render_entry
# @TEST_EDGE  context_capture -> Auto-populates context_data from source row columns
# @TEST_EDGE  context_removal -> keep_context=false clears context_data and sets has_context=False
# @TEST_EDGE  jaccard_zero -> 0% overlap returns 0.0
# @TEST_EDGE  jaccard_half -> 50% overlap returns 0.5
# @TEST_EDGE  jaccard_full -> 100% overlap returns 1.0
# @TEST_EDGE  priority_flagging -> Similarity >=0.5 triggers priority prefix
# @TEST_EDGE  truncation_500 -> Context rendering capped at ~500 tokens with annotation
# @TEST_EDGE  context_source_tagging -> auto/bulk/manual tags set correctly

from unittest.mock import MagicMock

from src.plugins.translate.prompt_builder import ContextAwarePromptBuilder


# region TestContextCapture [TYPE Class]
# @PURPOSE: Tests for auto-capture and editing/removal of context_data on correction submission.
class TestContextCapture:

    # region test_context_auto_captured_from_source_row [TYPE Function]
    # @BRIEF Submitting correction auto-populates context_data from source record columns.
    def test_context_auto_captured_from_source_row(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source text"
        record.source_object_name = "chart-42"
        record.source_object_id = "42"
        record.source_object_type = "chart"
        record.source_data = {"key": "dashboard_name", "value": "Sales Report"}
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        # Capture the DictionaryEntry created via db.add
        created_entries = []
        def capture_add(obj):
            created_entries.append(obj)
        db.add.side_effect = capture_add

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new translation",
            current_user="test-user",
        )

        assert result["action"] == "created"
        # Verify context was auto-captured from the record
        entry = created_entries[0]
        assert entry.has_context is True
        assert entry.context_source == "auto"
        assert entry.context_data is not None
        assert "source_object_type" in entry.context_data
        assert entry.context_data["source_object_type"] == "chart"
        assert entry.context_data["source_object_name"] == "chart-42"
    # endregion test_context_auto_captured_from_source_row

    # region test_context_removal_keep_context_false [TYPE Function]
    # @BRIEF keep_context=False clears context_data and sets has_context=False.
    def test_context_removal_keep_context_false(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source text"
        record.source_object_name = ""
        record.source_object_id = ""
        record.source_object_type = ""
        record.source_data = {"category": "electronics"}
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        created_entries = []
        db.add.side_effect = lambda obj: created_entries.append(obj)

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new",
            current_user="test-user",
            keep_context=False,
        )

        assert result["action"] == "created"
        entry = created_entries[0]
        assert entry.context_data is None
        assert entry.has_context is False
        assert entry.context_source == "manual"
    # endregion test_context_removal_keep_context_false

    # region test_context_override_replaces_auto [TYPE Function]
    # @BRIEF context_data_override replaces auto-captured context and sets source=manual.
    def test_context_override_replaces_auto(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source"
        record.source_object_name = "chart-1"
        record.source_object_id = "1"
        record.source_object_type = "chart"
        record.source_data = {"auto": "captured"}
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        created_entries = []
        db.add.side_effect = lambda obj: created_entries.append(obj)

        result = InlineCorrectionService.submit_correction_to_dict(
            db=db,
            record_id="rec-1",
            language_code="ru",
            dictionary_id="dict-1",
            corrected_value="new",
            current_user="test-user",
            context_data_override={"category": "manual_override", "region": "EU"},
        )

        assert result["action"] == "created"
        entry = created_entries[0]
        assert entry.context_source == "manual"
        assert entry.context_data == {"category": "manual_override", "region": "EU"}
    # endregion test_context_override_replaces_auto

# endregion TestContextCapture


# region TestJaccardSimilarity [TYPE Class]
# @PURPOSE: Tests for ContextAwarePromptBuilder Jaccard similarity and priority flagging.
class TestJaccardSimilarity:

    # region test_jaccard_zero_overlap [TYPE Function]
    # @BRIEF Disjoint contexts return 0.0 similarity.
    def test_jaccard_zero_overlap(self):
        a = {"domain": "electronics"}
        b = {"domain": "healthcare"}
        assert ContextAwarePromptBuilder.compute_context_similarity(a, b) == 0.0
    # endregion test_jaccard_zero_overlap

    # region test_jaccard_half_overlap [TYPE Function]
    # @BRIEF 50% overlap returns 0.5.
    def test_jaccard_half_overlap(self):
        # 1 shared out of 3 unique = 1/3 ≈ 0.333; construct exact 50%
        # {a, b} vs {b, c} -> intersection={b}, union={a,b,c} = 1/3
        # To get 0.5: {a,b} vs {b,c} -> nope
        # {a,b} vs {a,c} -> 1/3. {a,b} vs {a,b,c} -> 2/3
        # Need intersection/union = 0.5 -> union size = 2 * intersection size
        # {a, b} vs {a, c}: intersection={a}=1, union={a,b,c}=3 -> 0.333
        # {a, b} vs {a}: intersection={a}=1, union={a,b}=2 -> 0.5
        a = {"cat": "electronics", "region": "US"}
        b = {"cat": "electronics"}
        assert ContextAwarePromptBuilder.compute_context_similarity(a, b) == 0.5
    # endregion test_jaccard_half_overlap

    # region test_jaccard_full_overlap [TYPE Function]
    # @BRIEF Identical contexts return 1.0.
    def test_jaccard_full_overlap(self):
        a = {"domain": "electronics", "region": "US"}
        b = {"domain": "electronics", "region": "US"}
        assert ContextAwarePromptBuilder.compute_context_similarity(a, b) == 1.0
    # endregion test_jaccard_full_overlap

    # region test_jaccard_missing_context [TYPE Function]
    # @BRIEF Missing or empty context returns 0.0.
    def test_jaccard_missing_context(self):
        assert ContextAwarePromptBuilder.compute_context_similarity(None, {"a": "1"}) == 0.0
        assert ContextAwarePromptBuilder.compute_context_similarity({"a": "1"}, None) == 0.0
        assert ContextAwarePromptBuilder.compute_context_similarity(None, None) == 0.0
        assert ContextAwarePromptBuilder.compute_context_similarity({}, {"a": "1"}) == 0.0
        assert ContextAwarePromptBuilder.compute_context_similarity({"a": "1"}, {}) == 0.0
    # endregion test_jaccard_missing_context

    # region test_jaccard_case_insensitive [TYPE Function]
    # @BRIEF Similarity is case-insensitive.
    def test_jaccard_case_insensitive(self):
        a = {"domain": "Electronics"}
        b = {"domain": "electronics"}
        assert ContextAwarePromptBuilder.compute_context_similarity(a, b) == 1.0
    # endregion test_jaccard_case_insensitive

# endregion TestJaccardSimilarity


# region TestPriorityFlagging [TYPE Class]
# @PURPOSE: Tests that context similarity >= 0.5 triggers priority flagging in prompt rendering.
class TestPriorityFlagging:

    # region test_priority_flag_on_high_similarity [TYPE Function]
    # @BRIEF Entries with Jaccard >= 0.5 get priority prefix in rendered output.
    def test_priority_flag_on_high_similarity(self):
        entries = [
            {"source_term": "laptop", "target_term": "ноутбук", "has_context": True,
             "context_data": {"domain": "electronics", "region": "US"}},
            {"source_term": "laptop", "target_term": "ноутбук", "has_context": False,
             "context_data": None},
        ]
        # row_context has 1 of 2 entry values: {electronics} vs {electronics, US}
        # intersection={electronics}=1, union={electronics,US}=2 -> similarity=0.5
        row_context = {"domain": "electronics"}

        result = ContextAwarePromptBuilder.build_context_entries(entries, row_context)

        # First entry: similarity = 1/2 = 0.5 -> priority
        assert result[0].startswith("# PRIORITY")
        # Second entry: no context -> not priority
        assert not result[1].startswith("# PRIORITY")
    # endregion test_priority_flag_on_high_similarity

    # region test_no_priority_below_threshold [TYPE Function]
    # @BRIEF Entries with Jaccard < 0.5 do not get priority prefix.
    def test_no_priority_below_threshold(self):
        entries = [
            {"source_term": "laptop", "target_term": "ноутбук", "has_context": True,
             "context_data": {"domain": "electronics"}},
        ]
        row_context = {"domain": "healthcare"}

        result = ContextAwarePromptBuilder.build_context_entries(entries, row_context)

        # Similarity = 0.0 -> no priority
        assert not result[0].startswith("# PRIORITY")
    # endregion test_no_priority_below_threshold

    # region test_priority_entries_sorted_first [TYPE Function]
    # @BRIEF Priority entries appear before non-priority in the output list.
    def test_priority_entries_sorted_first(self):
        entries = [
            {"source_term": "phone", "target_term": "телефон", "has_context": True,
             "context_data": {"domain": "telecom"}},
            {"source_term": "laptop", "target_term": "ноутбук", "has_context": True,
             "context_data": {"domain": "electronics"}},
        ]
        row_context = {"domain": "telecom"}

        result = ContextAwarePromptBuilder.build_context_entries(entries, row_context)

        # phone has context match -> priority; laptop has no match -> not priority
        # Priority entries should come first
        assert result[0].startswith("# PRIORITY")
        assert not result[1].startswith("# PRIORITY")
    # endregion test_priority_entries_sorted_first

# endregion TestPriorityFlagging


# region TestContextTruncation [TYPE Class]
# @PURPOSE: Tests for 500-token truncation in context rendering.
class TestContextTruncation:

    # region test_context_truncated_at_500_tokens [TYPE Function]
    # @BRIEF Context rendering capped at ~500 tokens (2000 chars) with truncation annotation.
    def test_context_truncated_at_500_tokens(self):
        # Build a context string > 2000 chars
        long_context = {f"key_{i}": f"value_{i}" * 20 for i in range(30)}

        entry = {
            "source_term": "test",
            "target_term": "тест",
            "has_context": True,
            "context_data": long_context,
            "usage_notes": None,
        }

        rendered = ContextAwarePromptBuilder.render_entry(entry)

        assert "...[truncated]" in rendered
        # Verify the truncated string is still well-formed
        assert rendered.startswith('"test"')
        assert rendered.endswith('"')
    # endregion test_context_truncated_at_500_tokens

    # region test_short_context_not_truncated [TYPE Function]
    # @BRIEF Short context is rendered in full without truncation.
    def test_short_context_not_truncated(self):
        entry = {
            "source_term": "test",
            "target_term": "тест",
            "has_context": True,
            "context_data": {"domain": "tech"},
            "usage_notes": None,
        }

        rendered = ContextAwarePromptBuilder.render_entry(entry)

        assert "...[truncated]" not in rendered
        assert "domain=tech" in rendered
    # endregion test_short_context_not_truncated

# endregion TestContextTruncation


# region TestContextSourceTagging [TYPE Class]
# @PURPOSE: Tests that context_source is set to auto/bulk/manual based on submission path.
class TestContextSourceTagging:

    # region test_inline_edit_sets_auto [TYPE Function]
    # @BRIEF Inline correction without override sets context_source="auto".
    def test_inline_edit_sets_auto(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source"
        record.source_object_name = ""
        record.source_object_id = ""
        record.source_object_type = ""
        record.source_data = {"category": "electronics"}
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        created_entries = []
        db.add.side_effect = lambda obj: created_entries.append(obj)

        InlineCorrectionService.submit_correction_to_dict(
            db=db, record_id="rec-1", language_code="ru", dictionary_id="dict-1",
            corrected_value="new", current_user="test-user",
        )

        entry = created_entries[0]
        assert entry.context_source == "auto"
    # endregion test_inline_edit_sets_auto

    # region test_inline_edit_with_override_sets_manual [TYPE Function]
    # @BRIEF Inline correction with context_data_override sets context_source="manual".
    def test_inline_edit_with_override_sets_manual(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source"
        record.source_object_name = ""
        record.source_object_id = ""
        record.source_object_type = ""
        record.source_data = None
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        created_entries = []
        db.add.side_effect = lambda obj: created_entries.append(obj)

        InlineCorrectionService.submit_correction_to_dict(
            db=db, record_id="rec-1", language_code="ru", dictionary_id="dict-1",
            corrected_value="new", current_user="test-user",
            context_data_override={"manual_key": "manual_value"},
        )

        entry = created_entries[0]
        assert entry.context_source == "manual"
    # endregion test_inline_edit_with_override_sets_manual

    # region test_keep_context_false_sets_manual [TYPE Function]
    # @BRIEF keep_context=False sets context_source="manual" (explicit user action).
    def test_keep_context_false_sets_manual(self):
        db = MagicMock()

        lang_entry = MagicMock(spec=TranslationLanguage)
        lang_entry.id = "lang-ru-1"
        lang_entry.language_code = "ru"
        lang_entry.source_language_detected = "en"
        lang_entry.translated_value = "old"
        lang_entry.record_id = "rec-1"

        record = MagicMock(spec=TranslationRecord)
        record.id = "rec-1"
        record.run_id = "run-1"
        record.job_id = "job-1"
        record.source_sql = "source"
        record.source_object_name = ""
        record.source_object_id = ""
        record.source_object_type = ""
        record.source_data = {"category": "auto"}
        record.languages = [lang_entry]

        db.query.return_value.filter.return_value.first.side_effect = [lang_entry, record, None]

        created_entries = []
        db.add.side_effect = lambda obj: created_entries.append(obj)

        InlineCorrectionService.submit_correction_to_dict(
            db=db, record_id="rec-1", language_code="ru", dictionary_id="dict-1",
            corrected_value="new", current_user="test-user", keep_context=False,
        )

        entry = created_entries[0]
        assert entry.context_source == "manual"
        assert entry.has_context is False
    # endregion test_keep_context_false_sets_manual

# endregion TestContextSourceTagging


# region TestRenderEntry [TYPE Class]
# @PURPOSE: Tests for ContextAwarePromptBuilder.render_entry with various input shapes.
class TestRenderEntry:

    # region test_render_basic_entry [TYPE Function]
    # @BRIEF Basic entry renders as "source" -> "target".
    def test_render_basic_entry(self):
        entry = {"source_term": "hello", "target_term": "привет"}
        result = ContextAwarePromptBuilder.render_entry(entry)
        assert result == '"hello" -> "привет"'
    # endregion test_render_basic_entry

    # region test_render_with_context [TYPE Function]
    # @BRIEF Entry with context renders context annotation inline.
    def test_render_with_context(self):
        entry = {
            "source_term": "laptop",
            "target_term": "ноутбук",
            "has_context": True,
            "context_data": {"domain": "electronics"},
        }
        result = ContextAwarePromptBuilder.render_entry(entry)
        assert "context: domain=electronics" in result
    # endregion test_render_with_context

    # region test_render_with_usage_notes [TYPE Function]
    # @BRIEF Entry with usage_notes appends them to the rendered line.
    def test_render_with_usage_notes(self):
        entry = {
            "source_term": "laptop",
            "target_term": "ноутбук",
            "has_context": False,
            "context_data": None,
            "usage_notes": "Use for product catalogs only",
        }
        result = ContextAwarePromptBuilder.render_entry(entry)
        assert "# Usage: Use for product catalogs only" in result
    # endregion test_render_with_usage_notes

    # region test_render_object_attrs [TYPE Function]
    # @BRIEF Entry can be an object with attributes instead of a dict.
    def test_render_object_attrs(self):
        entry = MagicMock()
        entry.source_term = "mouse"
        entry.target_term = "мышь"
        entry.has_context = False
        entry.context_data = None
        entry.usage_notes = None
        result = ContextAwarePromptBuilder.render_entry(entry)
        assert result == '"mouse" -> "мышь"'
    # endregion test_render_object_attrs

# endregion TestRenderEntry

# endregion TestContextAwareCorrection
# endregion InlineCorrectionTests
