"""New feature tests: formality/regional prompts, TM, bilingual output,
QA report, OpenAI JSON batching, font hints."""

import pytest
from docx import Document
from openpyxl import Workbook

from services.glossary_service import build_full_prompt
from services.quality.qa_report import (
    _number_fidelity,
    _untranslated_ratio,
    run_qa_report,
)
from services.translation_tm import TMScope, translate_with_tm
from services.translation_cache import reset_cache_for_tests
from translators.bilingual import make_bilingual_docx
from translators.word_translator import WordTranslator, _font_hints_for_target


# ===========================================================================
# Formality + regional variant in prompts
# ===========================================================================
class TestFormalityPrompt:
    def test_formal_adds_tone_directive(self):
        prompt = build_full_prompt(None, None, "fr", "en", formality="formal")
        assert "TONE:" in prompt and "formal" in prompt

    def test_informal_adds_tone_directive(self):
        prompt = build_full_prompt(None, None, "fr", "en", formality="informal")
        assert "TONE:" in prompt and "informal" in prompt

    def test_no_formality_no_tone(self):
        assert "TONE:" not in build_full_prompt(None, None, "fr", "en")

    def test_regional_variant_auto(self):
        prompt = build_full_prompt(None, None, "fr", "pt-BR")
        assert "REGIONAL VARIANT" in prompt
        assert "Portuguese" in prompt

    def test_no_region_no_variant(self):
        assert "REGIONAL VARIANT" not in build_full_prompt(None, None, "fr", "en")


# ===========================================================================
# Translation memory (per-user scoping)
# ===========================================================================
class TestTranslationMemory:
    @pytest.fixture(autouse=True)
    def clean_cache(self):
        reset_cache_for_tests()
        yield
        reset_cache_for_tests()

    def test_no_scope_passthrough(self):
        calls = []

        def fake_translate(texts):
            calls.extend(texts)
            return [t.upper() for t in texts]

        out = translate_with_tm(
            ["a", "b"], "en", "fr", "fake", None, fake_translate
        )
        assert out == ["A", "B"]
        assert calls == ["a", "b"]

    def test_reuse_on_second_call_same_user(self):
        from services.translation_cache import get_cache

        get_cache()  # init LRU backend

        calls = []

        def fake_translate(texts):
            calls.extend(texts)
            return [f"T:{t}" for t in texts]

        scope = TMScope.from_prompt("user-1", "ctx")
        first = translate_with_tm(["hello", "world"], "en", "fr", "fake", scope, fake_translate)
        assert first == ["T:hello", "T:world"]
        assert len(calls) == 2

        second = translate_with_tm(["hello", "world"], "en", "fr", "fake", scope, fake_translate)
        assert second == ["T:hello", "T:world"]
        # Everything came from the TM — no new provider calls
        assert len(calls) == 2

    def test_users_are_isolated(self):
        from services.translation_cache import get_cache

        get_cache()
        out = translate_with_tm(
            ["x"], "en", "fr", "fake",
            TMScope.from_prompt("user-A", None), lambda ts: [f"A:{ts[0]}"],
        )
        assert out == ["A:x"]
        out_b = translate_with_tm(
            ["x"], "en", "fr", "fake",
            TMScope.from_prompt("user-B", None), lambda ts: [f"B:{ts[0]}"],
        )
        assert out_b == ["B:x"]

    def test_identity_translations_not_stored(self):
        from services.translation_cache import get_cache

        get_cache()
        scope = TMScope.from_prompt("user-1", None)
        # identity provider: returns input unchanged (a provider failure)
        translate_with_tm(["same"], "en", "fr", "fake", scope, lambda ts: ts)
        out = translate_with_tm(
            ["same"], "en", "fr", "fake", scope, lambda ts: ["CALLED"]
        )
        # Not poisoned by the identity entry: the provider was consulted
        assert out == ["CALLED"]

    def test_different_prompt_context_misses(self):
        from services.translation_cache import get_cache

        get_cache()
        s1 = TMScope.from_prompt("u", "prompt-A")
        s2 = TMScope.from_prompt("u", "prompt-B")
        translate_with_tm(["k"], "en", "fr", "fake", s1, lambda ts: ["v1"])
        out = translate_with_tm(["k"], "en", "fr", "fake", s2, lambda ts: ["v2"])
        assert out == ["v2"]


# ===========================================================================
# Bilingual output
# ===========================================================================
class TestBilingual:
    def test_source_interleaved_above_translation(self, tmp_path):
        src = Document()
        src.add_paragraph("Bonjour le monde")
        src.add_paragraph("")
        src.add_paragraph("Deuxième paragraphe")
        src_in = tmp_path / "src.docx"
        src.save(str(src_in))

        tr = Document()
        tr.add_paragraph("Hello world")
        tr.add_paragraph("")
        tr.add_paragraph("Second paragraph")
        tr_out = tmp_path / "tr.docx"
        tr.save(str(tr_out))

        result = make_bilingual_docx(src_in, tr_out, tmp_path / "bi.docx")
        assert result is not None

        doc = Document(str(result))
        texts = [p.text for p in doc.paragraphs]
        # Source paragraph precedes its translation
        assert texts[0] == "Bonjour le monde"
        assert texts[1] == "Hello world"
        assert texts[3] == "Deuxième paragraphe"
        assert texts[4] == "Second paragraph"

    def test_structure_mismatch_returns_none(self, tmp_path):
        a = Document(); a.add_paragraph("x"); a.save(str(tmp_path / "a.docx"))
        b = Document(); b.add_paragraph("x"); b.add_paragraph("y")
        b.save(str(tmp_path / "b.docx"))
        assert make_bilingual_docx(
            tmp_path / "a.docx", tmp_path / "b.docx", tmp_path / "bi.docx"
        ) is None


# ===========================================================================
# QA report
# ===========================================================================
class TestQAReport:
    def test_number_fidelity_full(self):
        r = _number_fidelity("Prix: 12,50 € sur 3 pages", "Price: 12.50 € on 3 pages")
        assert r["fidelity"] >= 0.9

    def test_number_fidelity_missing(self):
        r = _number_fidelity("Ref 123 and 456", "Ref 123 only")
        assert r["fidelity"] < 1.0

    def test_untranslated_ratio_high_when_identity(self):
        identity_source = (
            "This document contains substantial english text about pricing, "
            "delivery schedules and quarterly reporting obligations for the "
            "regional sales team and their managers."
        )
        assert _untranslated_ratio(identity_source, identity_source) > 0.8

    def test_untranslated_ratio_low_when_translated(self):
        assert _untranslated_ratio(
            "Ce document contient beaucoup de mots différents sur la facturation, "
            "les échéances de livraison et les obligations trimestrielles.",
            "This document holds many different words about invoicing, "
            "delivery deadlines and quarterly obligations.",
        ) < 0.35

    def test_run_qa_report_on_docx(self, tmp_path):
        src = Document()
        src.add_paragraph("Le total est de 42 euros pour la livraison.")
        src.add_paragraph("Merci de votre confiance renouvelée.")
        src_in = tmp_path / "in.docx"
        src.save(str(src_in))

        out_doc = Document()
        out_doc.add_paragraph("The total is 42 euros for the delivery.")
        out_doc.add_paragraph("Thank you for your renewed trust.")
        out_p = tmp_path / "out.docx"
        out_doc.save(str(out_p))

        report = run_qa_report(src_in, out_p, "en", ".docx")
        assert report is not None
        assert report["score"] >= 80
        assert report["numbers"]["fidelity"] == 1.0

    def test_run_qa_report_scores_untranslated_low(self, tmp_path):
        long_fr = (
            "Ce paragraphe contient suffisamment de mots différents pour "
            "satisfaire l'heuristique du rapport qualité automatisé, avec "
            "des notions de facturation, livraison et obligations."
        )
        src = Document()
        src.add_paragraph(long_fr)
        src_in = tmp_path / "in.docx"
        src.save(str(src_in))
        import shutil

        out_p = tmp_path / "out.docx"
        shutil.copy(str(src_in), str(out_p))  # "translation" = original

        report = run_qa_report(src_in, out_p, "en", ".docx")
        assert report is not None
        assert report["score"] < 50
        assert report["untranslated_ratio"] > 0.5


# ===========================================================================
# OpenAI JSON batching
# ===========================================================================
class TestOpenAIBatch:
    def _provider(self):
        from services.providers.openai_provider import OpenAITranslationProvider

        return OpenAITranslationProvider(api_key="test-key", model="gpt-test")

    def _requests(self, texts):
        from services.providers.schemas import TranslationRequest

        return [TranslationRequest(text=t, target_language="fr") for t in texts]

    def test_batch_single_request_success(self, monkeypatch):
        provider = self._provider()
        seen = {}

        def fake_api(text, system_prompt):
            import json

            seen["text"] = text
            items = json.loads(text)
            reply = json.dumps(
                [{"id": it["id"], "translation": f"FR:{it['text']}"} for it in items]
            )
            return reply, {}

        monkeypatch.setattr(provider, "_make_api_request", fake_api)
        out = provider.translate_batch(self._requests(["one", "two", "three"]))
        assert [r.translated_text for r in out] == [
            "FR:one", "FR:two", "FR:three",
        ]
        # One API call for the whole batch
        assert isinstance(seen.get("text"), str) and '"id"' in seen["text"]

    def test_batch_falls_back_on_bad_json(self, monkeypatch):
        provider = self._provider()
        calls = {"batch": 0, "single": 0}

        def fake_api(text, system_prompt):
            if text.startswith("["):
                calls["batch"] += 1
                return "not valid json at all", {}
            calls["single"] += 1
            return f"FR:{text}", {}

        monkeypatch.setattr(provider, "_make_api_request", fake_api)
        out = provider.translate_batch(self._requests(["alpha", "beta"]))
        assert [r.translated_text for r in out] == ["FR:alpha", "FR:beta"]
        assert calls["batch"] == 1
        assert calls["single"] == 2

    def test_batch_falls_back_on_wrong_length(self, monkeypatch):
        import json

        provider = self._provider()

        def fake_api(text, system_prompt):
            return json.dumps([{"id": 0, "translation": "only one"}]), {}

        monkeypatch.setattr(provider, "_make_api_request", fake_api)

        def fail_single(req):
            from services.providers.schemas import TranslationResponse

            return TranslationResponse(
                translated_text=f"S:{req.text}", provider_name="openai"
            )

        monkeypatch.setattr(provider, "translate_text", fail_single)
        out = provider.translate_batch(self._requests(["a", "b"]))
        assert [r.translated_text for r in out] == ["S:a", "S:b"]


# ===========================================================================
# CJK font hints (Word)
# ===========================================================================
class TestFontHints:
    def test_hint_mapping(self):
        assert _font_hints_for_target("zh-CN")[0] == "SimSun"
        assert _font_hints_for_target("ja")[0] == "Yu Mincho"
        assert _font_hints_for_target("ar")[1] == "Arial"
        assert _font_hints_for_target("en") == (None, None)

    def test_applied_on_translate(self, tmp_path):
        class _CJK:
            def get_name(self):
                return "mock"

            def is_available(self):
                return True

            def translate_batch(self, texts, target_language, source_language="auto"):
                return [f"译:{t}" for t in texts]

        from docx.oxml.ns import qn as _qn

        doc = Document()
        doc.add_paragraph("Hello")
        src = tmp_path / "in.docx"
        doc.save(str(src))

        t = WordTranslator(provider=_CJK())
        out = tmp_path / "out.docx"
        t.translate_file(src, out, "zh-CN", "en")

        result = Document(str(out))
        para = result.paragraphs[0]
        rFonts = para.runs[0]._r.find(_qn("w:rPr")).find(_qn("w:rFonts"))
        assert rFonts is not None
        assert rFonts.get(_qn("w:eastAsia")) == "SimSun"
