# SPDX-FileCopyrightText: 2025 James R. Barlow
# SPDX-License-Identifier: MPL-2.0

"""Unit tests for MultiFontManager and FontProvider."""

from __future__ import annotations

import logging
from pathlib import Path

import pytest

from ocrmypdf.font import BuiltinFontProvider, FontManager, MultiFontManager


@pytest.fixture
def font_dir():
    """Return path to font directory."""
    return Path(__file__).parent.parent / "src" / "ocrmypdf" / "data"


@pytest.fixture
def multi_font_manager(font_dir):
    """Create MultiFontManager instance for testing."""
    return MultiFontManager(font_dir)


def has_cjk_font(manager: MultiFontManager) -> bool:
    """Check if CJK font is available (from system)."""
    return 'NotoSansCJK-Regular' in manager.fonts


def has_arabic_font(manager: MultiFontManager) -> bool:
    """Check if Arabic font is available (from system)."""
    return 'NotoSansArabic-Regular' in manager.fonts


def has_devanagari_font(manager: MultiFontManager) -> bool:
    """Check if Devanagari font is available (from system)."""
    return 'NotoSansDevanagari-Regular' in manager.fonts


# Marker for tests that require CJK fonts
requires_cjk = pytest.mark.skipif(
    "not has_cjk_font(MultiFontManager())",
    reason="CJK font not available (not installed on system)",
)


# --- MultiFontManager Initialization Tests ---


def test_init_loads_builtin_fonts(multi_font_manager):
    """Test that initialization loads all expected builtin fonts."""
    # Only NotoSans-Regular and Occulta are bundled
    assert 'NotoSans-Regular' in multi_font_manager.fonts
    assert 'Occulta' in multi_font_manager.fonts

    # At least 2 builtin fonts should be loaded
    assert len(multi_font_manager.fonts) >= 2

    # Arabic, Devanagari, CJK are optional (system fonts)


def test_missing_font_directory():
    """Test that missing font directory raises error for fallback font."""
    with pytest.raises(FileNotFoundError):
        MultiFontManager(Path("/nonexistent/path"))


# --- Arabic Script Language Tests ---
# These tests require Arabic fonts to be installed on the system


def test_select_font_for_arabic_language(multi_font_manager):
    """Test font selection with Arabic language hint."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    font_manager = multi_font_manager.select_font_for_word("مرحبا", "ara")
    assert font_manager == multi_font_manager.fonts['NotoSansArabic-Regular']


def test_select_font_for_persian_language(multi_font_manager):
    """Test font selection with Persian language hint."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    font_manager = multi_font_manager.select_font_for_word("سلام", "per")
    assert font_manager == multi_font_manager.fonts['NotoSansArabic-Regular']


def test_select_font_for_urdu_language(multi_font_manager):
    """Test font selection with Urdu language hint."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    font_manager = multi_font_manager.select_font_for_word("ہیلو", "urd")
    assert font_manager == multi_font_manager.fonts['NotoSansArabic-Regular']


def test_farsi_language_code(multi_font_manager):
    """Test that 'fas' (Farsi alternative code) maps to Arabic font."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    font_manager = multi_font_manager.select_font_for_word("سلام", "fas")
    assert font_manager == multi_font_manager.fonts['NotoSansArabic-Regular']


# --- Devanagari Script Language Tests ---
# These tests require Devanagari fonts to be installed on the system


def test_select_font_for_hindi_language(multi_font_manager):
    """Test font selection with Hindi language hint."""
    if not has_devanagari_font(multi_font_manager):
        pytest.skip("Devanagari font not available")
    font_manager = multi_font_manager.select_font_for_word("नमस्ते", "hin")
    assert font_manager == multi_font_manager.fonts['NotoSansDevanagari-Regular']


def test_select_font_for_sanskrit_language(multi_font_manager):
    """Test font selection with Sanskrit language hint."""
    if not has_devanagari_font(multi_font_manager):
        pytest.skip("Devanagari font not available")
    font_manager = multi_font_manager.select_font_for_word("संस्कृतम्", "san")
    assert font_manager == multi_font_manager.fonts['NotoSansDevanagari-Regular']


def test_select_font_for_marathi_language(multi_font_manager):
    """Test font selection with Marathi language hint."""
    if not has_devanagari_font(multi_font_manager):
        pytest.skip("Devanagari font not available")
    font_manager = multi_font_manager.select_font_for_word("मराठी", "mar")
    assert font_manager == multi_font_manager.fonts['NotoSansDevanagari-Regular']


def test_select_font_for_nepali_language(multi_font_manager):
    """Test font selection with Nepali language hint."""
    if not has_devanagari_font(multi_font_manager):
        pytest.skip("Devanagari font not available")
    font_manager = multi_font_manager.select_font_for_word("नेपाली", "nep")
    assert font_manager == multi_font_manager.fonts['NotoSansDevanagari-Regular']


# --- CJK Language Tests ---
# These tests require CJK fonts to be installed on the system


def test_select_font_for_chinese_language(multi_font_manager):
    """Test font selection with Chinese language hint (ISO 639-3)."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("你好", "zho")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_select_font_for_chinese_generic(multi_font_manager):
    """Test font selection with generic Chinese language code."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("中文", "chi")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_select_font_for_chinese_simplified(multi_font_manager):
    """Test font selection with Tesseract's chi_sim language code."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("简体字", "chi_sim")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_select_font_for_chinese_traditional(multi_font_manager):
    """Test font selection with Tesseract's chi_tra language code."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("漢字", "chi_tra")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_select_font_for_japanese_language(multi_font_manager):
    """Test font selection with Japanese language hint."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("こんにちは", "jpn")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_select_font_for_korean_language(multi_font_manager):
    """Test font selection with Korean language hint."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("안녕하세요", "kor")
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


# --- Latin/English Tests ---


def test_select_font_for_english_text(multi_font_manager):
    """Test font selection for English text."""
    font_manager = multi_font_manager.select_font_for_word("Hello World", "eng")
    assert font_manager == multi_font_manager.fonts['NotoSans-Regular']


def test_select_font_without_language_hint(multi_font_manager):
    """Test font selection without language hint falls back to glyph checking."""
    font_manager = multi_font_manager.select_font_for_word("Hello", None)
    assert font_manager == multi_font_manager.fonts['NotoSans-Regular']


# --- Fallback Behavior Tests ---


def test_select_font_arabic_text_without_language_hint(multi_font_manager):
    """Test that Arabic text is handled via fallback without language hint."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    font_manager = multi_font_manager.select_font_for_word("مرحبا", None)
    # Should get NotoSansArabic-Regular via fallback chain glyph checking
    assert font_manager == multi_font_manager.fonts['NotoSansArabic-Regular']


def test_devanagari_text_without_language_hint(multi_font_manager):
    """Test that Devanagari text is handled via fallback without language hint."""
    # NotoSans-Regular includes Devanagari glyphs, so it's selected first in fallback
    font_manager = multi_font_manager.select_font_for_word("नमस्ते", None)
    # Could be NotoSans-Regular or NotoSansDevanagari-Regular depending on availability
    assert font_manager is not None


def test_cjk_text_without_language_hint(multi_font_manager):
    """Test that CJK text is handled via fallback without language hint."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    font_manager = multi_font_manager.select_font_for_word("你好", None)
    # A real, glyph-covering CJK font is selected (which specific family
    # depends on what is installed: pan-CJK super font or a per-language subset).
    assert font_manager.font_path.name != 'Occulta.ttf'


def test_fallback_to_occulta_font(multi_font_manager):
    """Test that unsupported characters fall back to Occulta.ttf."""
    # Use a character unlikely to be in any standard font
    font_manager = multi_font_manager.select_font_for_word("test", "xyz")
    # Should return some valid font
    assert font_manager in multi_font_manager.fonts.values()


def test_fallback_fonts_constant(multi_font_manager):
    """Test that FALLBACK_FONTS contains expected fonts."""
    # Check that core fonts are in fallback list
    assert 'NotoSans-Regular' in MultiFontManager.FALLBACK_FONTS
    assert 'NotoSansArabic-Regular' in MultiFontManager.FALLBACK_FONTS
    assert 'NotoSansDevanagari-Regular' in MultiFontManager.FALLBACK_FONTS
    assert 'NotoSansCJK-Regular' in MultiFontManager.FALLBACK_FONTS

    # Only NotoSans-Regular is bundled; other scripts are system fonts
    assert 'NotoSans-Regular' in multi_font_manager.fonts


# --- Glyph Coverage Tests ---


def test_has_all_glyphs_for_english(multi_font_manager):
    """Test glyph coverage checking for English text."""
    assert multi_font_manager.has_all_glyphs('NotoSans-Regular', "Hello World")
    assert multi_font_manager.has_all_glyphs('NotoSans-Regular', "café")


def test_has_all_glyphs_for_arabic(multi_font_manager):
    """Test glyph coverage checking for Arabic text."""
    if not has_arabic_font(multi_font_manager):
        pytest.skip("Arabic font not available")
    assert multi_font_manager.has_all_glyphs('NotoSansArabic-Regular', "مرحبا")


def test_has_all_glyphs_for_devanagari(multi_font_manager):
    """Test glyph coverage checking for Devanagari text."""
    if not has_devanagari_font(multi_font_manager):
        pytest.skip("Devanagari font not available")
    assert multi_font_manager.has_all_glyphs('NotoSansDevanagari-Regular', "नमस्ते")


def test_has_all_glyphs_for_cjk(multi_font_manager):
    """Test glyph coverage checking for CJK text."""
    if not has_cjk_font(multi_font_manager):
        pytest.skip("CJK font not available")
    assert multi_font_manager.has_all_glyphs('NotoSansCJK-Regular', "你好")


def test_empty_text_has_all_glyphs(multi_font_manager):
    """Test that empty text returns True for glyph coverage."""
    assert multi_font_manager.has_all_glyphs('NotoSans-Regular', "")


def test_has_all_glyphs_missing_font(multi_font_manager):
    """Test that has_all_glyphs returns False for non-existent font."""
    assert not multi_font_manager.has_all_glyphs('NonExistentFont', "test")


# --- Caching Tests ---


def test_font_selection_caching(multi_font_manager):
    """Test that font selection results are cached."""
    font1 = multi_font_manager.select_font_for_word("Hello", "eng")

    cache_key = ("Hello", "eng")
    assert cache_key in multi_font_manager._selection_cache

    font2 = multi_font_manager.select_font_for_word("Hello", "eng")
    assert font1 == font2


# --- Language Font Map Tests ---


def test_language_font_map_coverage():
    """Test that LANGUAGE_FONT_MAP has valid structure."""
    # Only NotoSans-Regular is bundled now
    # This test just verifies the structure is valid
    for font_name in MultiFontManager.LANGUAGE_FONT_MAP.values():
        # All font names should be valid strings
        assert isinstance(font_name, str)
        assert font_name.startswith('NotoSans')


# --- get_all_fonts Tests ---


def test_get_all_fonts(multi_font_manager):
    """Test get_all_fonts returns all loaded fonts."""
    all_fonts = multi_font_manager.get_all_fonts()
    assert isinstance(all_fonts, dict)
    # At least 2 builtin fonts should be loaded (NotoSans-Regular and Occulta)
    assert len(all_fonts) >= 2
    assert 'NotoSans-Regular' in all_fonts
    assert 'Occulta' in all_fonts
    # Arabic, Devanagari, CJK are optional (system fonts)


# --- FontProvider Tests ---


class MockFontProvider:
    """Mock FontProvider for testing missing fonts."""

    def __init__(self, available_fonts: dict[str, FontManager], fallback: FontManager):
        """Initialize mock font provider with given fonts."""
        self._fonts = available_fonts
        self._fallback = fallback

    def get_font(self, font_name: str) -> FontManager | None:
        return self._fonts.get(font_name)

    def get_available_fonts(self) -> list[str]:
        return list(self._fonts.keys())

    def get_fallback_font(self) -> FontManager:
        return self._fallback


def test_custom_font_provider(font_dir):
    """Test that custom FontProvider can be injected."""
    fonts = {
        'NotoSans-Regular': FontManager(font_dir / 'NotoSans-Regular.ttf'),
        'Occulta': FontManager(font_dir / 'Occulta.ttf'),
    }
    provider = MockFontProvider(fonts, fonts['Occulta'])

    manager = MultiFontManager(font_provider=provider)

    # Should only have the fonts we provided
    assert len(manager.fonts) == 2
    assert 'NotoSans-Regular' in manager.fonts
    assert 'Occulta' in manager.fonts


def test_missing_font_uses_fallback(font_dir):
    """Test that missing fonts gracefully fall back."""
    fonts = {
        'NotoSans-Regular': FontManager(font_dir / 'NotoSans-Regular.ttf'),
        'Occulta': FontManager(font_dir / 'Occulta.ttf'),
    }
    provider = MockFontProvider(fonts, fonts['Occulta'])

    manager = MultiFontManager(font_provider=provider)

    # Arabic text should fall back to Occulta since NotoSansArabic is missing
    font = manager.select_font_for_word("مرحبا", "ara")
    assert font == fonts['Occulta']


def test_builtin_font_provider_loads_expected_fonts(font_dir):
    """Test BuiltinFontProvider loads all expected builtin fonts."""
    provider = BuiltinFontProvider(font_dir)

    available = provider.get_available_fonts()
    assert 'NotoSans-Regular' in available
    assert 'Occulta' in available
    # Only Latin (NotoSans) and glyphless fallback (Occulta) are bundled.
    # All other scripts (Arabic, Devanagari, CJK, etc.) are discovered
    # from system fonts by SystemFontProvider to reduce package size.
    assert len(available) == 2


def test_builtin_font_provider_get_font(font_dir):
    """Test BuiltinFontProvider.get_font returns correct fonts."""
    provider = BuiltinFontProvider(font_dir)

    font = provider.get_font('NotoSans-Regular')
    assert font is not None
    assert isinstance(font, FontManager)

    missing = provider.get_font('NonExistent')
    assert missing is None


def test_builtin_font_provider_get_fallback(font_dir):
    """Test BuiltinFontProvider.get_fallback_font returns Occulta font."""
    provider = BuiltinFontProvider(font_dir)

    fallback = provider.get_fallback_font()
    assert fallback is not None
    assert fallback == provider.get_font('Occulta')


def test_builtin_font_provider_missing_font_logs_warning(tmp_path, font_dir, caplog):
    """Test that missing expected fonts log a warning."""
    # Create minimal font directory with only Occulta.ttf
    (tmp_path / 'Occulta.ttf').write_bytes((font_dir / 'Occulta.ttf').read_bytes())

    with caplog.at_level(logging.WARNING):
        provider = BuiltinFontProvider(tmp_path)

    # Should have logged warnings for missing fonts
    assert 'NotoSans-Regular' in caplog.text
    assert 'not found' in caplog.text

    # But Occulta should be loaded
    assert provider.get_fallback_font() is not None


def test_builtin_font_provider_missing_occulta_raises(tmp_path):
    """Test that missing Occulta.ttf raises FileNotFoundError."""
    with pytest.raises(FileNotFoundError, match="Required fallback font"):
        BuiltinFontProvider(tmp_path)


class _StubHbFont:
    """Minimal uharfbuzz Font stand-in with controllable glyph coverage."""

    def __init__(self, covered_codepoints: set[int]):
        self._covered = covered_codepoints

    def get_nominal_glyph(self, codepoint: int) -> int:
        return 1 if codepoint in self._covered else 0


class _FakeFontManager:
    """FontManager stand-in whose glyph coverage is fixed per test."""

    def __init__(self, name: str, covered_chars: str):
        self.font_path = Path(name)
        self._hb = _StubHbFont({ord(c) for c in covered_chars})

    def get_hb_font(self) -> _StubHbFont:
        return self._hb


class _FakeFontProvider:
    """FontProvider returning controlled fonts by logical name."""

    def __init__(self, fonts: dict[str, _FakeFontManager]):
        self._fonts = fonts
        self._fallback = _FakeFontManager('Occulta.ttf', '')

    def get_font(self, name: str) -> _FakeFontManager | None:
        return self._fonts.get(name)

    def get_available_fonts(self) -> list[str]:
        return list(self._fonts)

    def get_fallback_font(self) -> _FakeFontManager:
        return self._fallback


def test_japanese_prefers_jp_family_over_other_cjk():
    """A Japanese language hint selects NotoSansJP, not another CJK family."""
    fonts = {
        'NotoSansSC-Regular': _FakeFontManager('NotoSansSC.ttf', '中'),
        'NotoSansJP-Regular': _FakeFontManager('NotoSansJP.ttf', '中こ'),
    }
    manager = MultiFontManager(font_provider=_FakeFontProvider(fonts))
    # 'こ' (kana) is only covered by JP; both cover the kanji '中'.
    font = manager.select_font_for_word('中こ', 'jpn')
    assert font.font_path.name == 'NotoSansJP.ttf'


def test_chinese_simplified_prefers_sc_family():
    """A Simplified Chinese hint selects NotoSansSC over the pan-CJK font."""
    fonts = {
        'NotoSansSC-Regular': _FakeFontManager('NotoSansSC.ttf', '简'),
        'NotoSansCJK-Regular': _FakeFontManager('NotoSansCJK.ttc', '简'),
    }
    manager = MultiFontManager(font_provider=_FakeFontProvider(fonts))
    font = manager.select_font_for_word('简', 'chi_sim')
    assert font.font_path.name == 'NotoSansSC.ttf'


def test_cjk_fails_over_when_preferred_subset_lacks_glyph():
    """If the language's subset font lacks a glyph, another CJK family is used."""
    fonts = {
        # Simplified Chinese subset cannot render Japanese kana.
        'NotoSansSC-Regular': _FakeFontManager('NotoSansSC.ttf', '中'),
        'NotoSansJP-Regular': _FakeFontManager('NotoSansJP.ttf', '中こ'),
    }
    manager = MultiFontManager(font_provider=_FakeFontProvider(fonts))
    # Tagged Simplified Chinese, but the text needs kana only JP covers.
    font = manager.select_font_for_word('こ', 'chi_sim')
    assert font.font_path.name == 'NotoSansJP.ttf'


def test_cjk_falls_back_to_pan_cjk_super_font():
    """When only the full-coverage pan-CJK font exists, it serves any CJK lang."""
    fonts = {
        'NotoSansCJK-Regular': _FakeFontManager('NotoSansCJK.ttc', '中こ안'),
    }
    manager = MultiFontManager(font_provider=_FakeFontProvider(fonts))
    assert manager.select_font_for_word('こ', 'jpn').font_path.name == 'NotoSansCJK.ttc'


def test_missing_cjk_font_warning_names_language_font(font_dir, caplog):
    """The missing-font warning names the language-specific CJK family (#1652)."""
    manager = MultiFontManager(font_provider=BuiltinFontProvider(font_dir))
    with caplog.at_level(logging.WARNING):
        manager.select_font_for_word("こんにちは", "jpn")
    assert 'NotoSansJP' in caplog.text


def test_missing_font_warning_explains_consequences(font_dir, caplog):
    """The glyphless-fallback warning should be actionable, not cryptic (#1652).

    With only builtin fonts available, Arabic text cannot be covered, so the
    manager falls back to glyphless Occulta and must warn helpfully.
    """
    # Builtin-only provider: NotoSansArabic is never available, forcing fallback.
    manager = MultiFontManager(font_provider=BuiltinFontProvider(font_dir))

    with caplog.at_level(logging.WARNING):
        manager.select_font_for_word("سلام", "fas")

    msg = caplog.text
    # Identifies the affected language and the font family to install.
    assert 'fas' in msg
    assert 'NotoSansArabic' in msg
    # Explains the user-visible consequence so the message is not cryptic:
    # the text stays searchable but renders blank when highlighted.
    assert 'searchable' in msg.lower()
    assert 'highlight' in msg.lower() or 'select' in msg.lower()


# --- Coverage-driven last-resort font search (#1722) ---


class _FakeProviderWithSearch(_FakeFontProvider):
    """FontProvider that can also search unlisted fonts by glyph coverage."""

    def __init__(self, fonts, searchable):
        super().__init__(fonts)
        self._searchable = searchable
        self.search_calls: list[str] = []

    def find_font_with_glyphs(self, text):
        self.search_calls.append(text)
        for name, font in self._searchable.items():
            hb = font.get_hb_font()
            if all(hb.get_nominal_glyph(ord(c)) for c in text):
                self._fonts[name] = font  # discovered fonts become resolvable
                return name, font
        return None


def test_unlisted_font_found_by_coverage_search():
    """A script outside FALLBACK_FONTS is rendered if the font is installed."""
    searchable = {'NotoSansCherokee-Regular': _FakeFontManager('Cherokee.ttf', 'ᏣᎳᎩ')}
    provider = _FakeProviderWithSearch({}, searchable)
    manager = MultiFontManager(font_provider=provider)

    font = manager.select_font_for_word('ᏣᎳᎩ', None)

    assert font.font_path.name == 'Cherokee.ttf'
    assert provider.search_calls == ['ᏣᎳᎩ']


def test_coverage_search_result_is_cached():
    """The expensive coverage search runs once per distinct word."""
    searchable = {'NotoSansCherokee-Regular': _FakeFontManager('Cherokee.ttf', 'ᏣᎳᎩ')}
    provider = _FakeProviderWithSearch({}, searchable)
    manager = MultiFontManager(font_provider=provider)

    manager.select_font_for_word('ᏣᎳᎩ', None)
    manager.select_font_for_word('ᏣᎳᎩ', None)

    assert len(provider.search_calls) == 1


def test_named_fonts_take_precedence_over_coverage_search():
    """The coverage search is a last resort, not a substitute for named fonts."""
    fonts = {'NotoSans-Regular': _FakeFontManager('NotoSans.ttf', 'abc')}
    searchable = {'NotoSansMono-Regular': _FakeFontManager('NotoSansMono.ttf', 'abc')}
    provider = _FakeProviderWithSearch(fonts, searchable)
    manager = MultiFontManager(font_provider=provider)

    font = manager.select_font_for_word('abc', None)

    assert font.font_path.name == 'NotoSans.ttf'
    assert provider.search_calls == []


def test_provider_without_coverage_search_still_falls_back():
    """Providers predating find_font_with_glyphs() keep working (duck-typed)."""
    manager = MultiFontManager(font_provider=_FakeFontProvider({}))
    font = manager.select_font_for_word('ᏣᎳᎩ', None)
    assert font.font_path.name == 'Occulta.ttf'


def test_missing_font_warning_names_the_missing_characters(font_dir, caplog):
    """The warning must identify what could not be rendered (#1722).

    The user's real question is "which font package do I install?" — naming the
    offending codepoints and their Unicode names answers it even for scripts
    OCRmyPDF has no language mapping for.
    """
    manager = MultiFontManager(font_provider=BuiltinFontProvider(font_dir))

    with caplog.at_level(logging.WARNING):
        manager.select_font_for_word('ᏣᎳᎩ', None)

    msg = caplog.text
    assert 'U+13E3' in msg  # CHEROKEE LETTER TSA
    assert 'CHEROKEE' in msg.upper()


def test_missing_character_warning_ignores_covered_characters(font_dir, caplog):
    """Only the uncoverable characters are reported, not the whole word."""
    manager = MultiFontManager(font_provider=BuiltinFontProvider(font_dir))

    with caplog.at_level(logging.WARNING):
        manager.select_font_for_word('aᏣb', None)

    msg = caplog.text
    assert 'U+13E3' in msg
    assert 'U+0061' not in msg  # 'a' is covered by the builtin Latin font


def test_mixed_script_word_warning_does_not_advise_installing_fonts(caplog):
    """A word no single font covers is reported as such, not as a missing font.

    Every character here has an installed font; telling the user to install
    more would be wrong advice and is exactly the confusion behind #1722.
    """
    fonts = {'NotoSans-Regular': _FakeFontManager('NotoSans.ttf', 'ab')}
    searchable = {'NotoSansCherokee-Regular': _FakeFontManager('Cherokee.ttf', 'Ꮳ')}
    manager = MultiFontManager(font_provider=_FakeProviderWithSearch(fonts, searchable))

    with caplog.at_level(logging.WARNING):
        font = manager.select_font_for_word('aᏣb', None)

    assert font.font_path.name == 'Occulta.ttf'
    msg = caplog.text
    assert 'mixing scripts' in msg
    assert 'will not help' in msg
    assert 'U+' not in msg  # no codepoints to blame; nothing to install
