#!/usr/bin/env python3
"""Locale-whitelist, unicode-range, and sliced-font regression tests."""

from __future__ import annotations

import hashlib
from functools import lru_cache
from io import BytesIO
from pathlib import Path

import pytest
import uharfbuzz as hb
from fontTools.ttLib import TTFont

ROOT = Path(__file__).resolve().parent.parent
DIST = ROOT / "dist"
SUBSET_DIR = DIST / "subsets"
WHITELIST_DIR = ROOT / "subsets"
SUBSET_NAMES = ("latin-core", "latin-ext", "vietnamese")
STYLES = ("upright", "italic")
MONEY_GLYPHS = "0123456789$£¥₹€"
MATH_TEXT = "‰≈≠≤≥"
SS02_FORMS = (
    ("l", "l", "l.ss02"),
    ("ĺ", "lacute", "lacute.ss02"),
    ("ļ", "uni013C", "uni013C.ss02"),
    ("ľ", "lcaron", "lcaron.ss02"),
    ("ŀ", "ldot", "ldot.ss02"),
    ("ł", "lslash", "lslash.ss02"),
    ("ḷ", "uni1E37", "uni1E37.ss02"),
    ("ḻ", "uni1E3B", "uni1E3B.ss02"),
)
WHITELIST_SHA256 = {
    "latin-core": (
        "3d1f311e74cd075285f8750a308d6084"
        "dbb4091529f619d34da21cd5820e95bc"
    ),
    "latin-ext": (
        "5db64e5fe670f5aaede08024fcb040183"
        "69102f30037102aa426ab074206ce98"
    ),
    "vietnamese": (
        "0ecc5e3b7d96db9a8922e668dddc43f"
        "74006bfdf4084d1279660102879bfc0a3"
    ),
}


def parse_unicode_ranges(path: Path) -> set[int]:
    codepoints: set[int] = set()
    for line in path.read_text().splitlines():
        line = line.split("#", 1)[0]
        for token in line.replace(",", " ").split():
            value = token.removeprefix("U+")
            if "-" in value:
                start, end = value.split("-", 1)
                codepoints.update(range(int(start, 16), int(end, 16) + 1))
            else:
                codepoints.add(int(value, 16))
    return codepoints


def range_tokens(path: Path) -> list[str]:
    tokens = []
    for line in path.read_text().splitlines():
        line = line.split("#", 1)[0]
        tokens.extend(token for token in line.replace(",", " ").split() if token)
    return tokens


def variable_path(style: str) -> Path:
    infix = "-Italic" if style == "italic" else ""
    return DIST / f"PivotSans{infix}[wght].ttf"


def subset_path(style: str, subset_name: str) -> Path:
    infix = "-Italic" if style == "italic" else ""
    return SUBSET_DIR / f"PivotSans{infix}-{subset_name}[wght].woff2"


@lru_cache(maxsize=None)
def sfnt_bytes(path: Path) -> bytes:
    font_file = TTFont(path)
    font_file.flavor = None
    stream = BytesIO()
    font_file.save(stream)
    return stream.getvalue()


def shape(
    path: Path,
    text: str,
    weight: int = 400,
    features=None,
    language: str | None = None,
):
    face = hb.Face(sfnt_bytes(path))
    font = hb.Font(face)
    font.scale = (face.upem, face.upem)
    hb.ot_font_set_funcs(font)
    font.set_variations({"wght": weight})
    buffer = hb.Buffer()
    buffer.add_str(text)
    buffer.guess_segment_properties()
    if language is not None:
        buffer.language = language
    hb.shape(font, buffer, features or {})
    return (
        [info.codepoint for info in buffer.glyph_infos],
        [position.x_advance for position in buffer.glyph_positions],
    )


def shaped_names(
    path: Path,
    text: str,
    weight: int = 400,
    features=None,
    language: str | None = None,
):
    glyph_ids, advances = shape(
        path,
        text,
        weight,
        features,
        language,
    )
    font = TTFont(path)
    return [font.getGlyphName(glyph_id) for glyph_id in glyph_ids], advances


def feature_tags(font: TTFont) -> set[str]:
    return {
        record.FeatureTag
        for record in font["GSUB"].table.FeatureList.FeatureRecord
    }


def test_whitelists_are_disjoint_and_cover_the_complete_pivot_cmap():
    targets = {
        name: parse_unicode_ranges(WHITELIST_DIR / f"{name}.txt")
        for name in SUBSET_NAMES
    }
    for index, first in enumerate(SUBSET_NAMES):
        for second in SUBSET_NAMES[index + 1 :]:
            assert not targets[first] & targets[second], (first, second)

    union = set().union(*targets.values())
    for style in STYLES:
        cmap = set(TTFont(variable_path(style)).getBestCmap())
        assert cmap <= union
        assert not cmap - union


def test_locale_whitelists_are_frozen_for_the_1_200_release():
    for subset_name, expected in WHITELIST_SHA256.items():
        data = (WHITELIST_DIR / f"{subset_name}.txt").read_bytes()
        assert hashlib.sha256(data).hexdigest() == expected


@pytest.mark.parametrize("style", STYLES)
@pytest.mark.parametrize("subset_name", SUBSET_NAMES)
def test_slice_cmap_is_exact_intersection_of_full_font_and_whitelist(
    style, subset_name
):
    full_cmap = set(TTFont(variable_path(style)).getBestCmap())
    whitelist = parse_unicode_ranges(
        WHITELIST_DIR / f"{subset_name}.txt"
    )
    sliced = TTFont(subset_path(style, subset_name))
    assert set(sliced.getBestCmap()) == full_cmap & whitelist


@pytest.mark.parametrize("style", STYLES)
@pytest.mark.parametrize("subset_name", SUBSET_NAMES)
def test_slices_preserve_variable_metadata_and_authorship(style, subset_name):
    path = subset_path(style, subset_name)
    font = TTFont(path)
    assert font.flavor == "woff2"
    assert font["head"].modified == 3_868_128_000
    assert font["name"].getDebugName(1) == "Pivot Sans"
    assert font["name"].getDebugName(5) == "Version 1.200"
    assert font["name"].getDebugName(8) == "Beacon Bay Technologies, Inc."
    assert font["name"].getDebugName(11) == "https://pivotsans.com"
    assert font["hhea"].advanceWidthMax == max(
        advance for advance, _ in font["hmtx"].metrics.values()
    )
    axis = font["fvar"].axes[0]
    assert (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue) == (
        "wght",
        300,
        400,
        700,
    )


@pytest.mark.parametrize("style", STYLES)
def test_latin_core_slice_retains_money_math_ss01_and_ss02(style):
    path = subset_path(style, "latin-core")
    font = TTFont(path)
    assert {"ss01", "ss02", "tnum"} <= feature_tags(font)
    for weight, slot in (
        (300, 668),
        (400, 660),
        (500, 666),
        (600, 671),
        (700, 677),
    ):
        glyph_ids, advances = shape(
            path, MONEY_GLYPHS, weight, {"tnum": True}
        )
        assert advances == [slot] * 15
        if weight >= 600:
            assert font.getGlyphName(glyph_ids[10]) == (
                "dollar.BRACKET.varAlt01.tf"
            )

    math_ids, math_advances = shape(path, MATH_TEXT)
    assert [font.getGlyphName(glyph_id) for glyph_id in math_ids] == [
        "perthousand",
        "approxequal",
        "notequal",
        "lessequal",
        "greaterequal",
    ]
    assert all(advance > 0 for advance in math_advances)
    assert shape(path, "a")[0] != shape(path, "a", features={"ss01": True})[0]

    for weight in (300, 400, 500, 600, 700):
        default = shaped_names(path, "l", weight)
        explicit_off = shaped_names(path, "l", weight, {"ss02": False})
        enabled = shaped_names(path, "l", weight, {"ss02": True})
        assert default == explicit_off
        assert default[0] == ["l"]
        assert enabled[0] == ["l.ss02"]
        assert enabled[1] == default[1]


@pytest.mark.parametrize("style", STYLES)
def test_latin_ext_slice_retains_every_encoded_ss02_l_derivative(style):
    path = subset_path(style, "latin-ext")
    font = TTFont(path)
    assert {"ss01", "ss02"} <= feature_tags(font)
    assert "tnum" not in feature_tags(font)
    assert shaped_names(path, "ā")[0] == ["amacron"]
    assert shaped_names(path, "ā", features={"ss01": True})[0] == [
        "amacron.ss01"
    ]

    # ASCII l belongs only to latin-core. Every encoded accented/stroked l
    # derivative belongs only to latin-ext, including the two U+1E3x forms
    # assembled from l plus below-mark components in the source.
    assert 0x006C not in font.getBestCmap()
    for character, source, alternate in SS02_FORMS[1:]:
        assert font.getBestCmap()[ord(character)] == source
        for weight in (300, 400, 500, 600, 700):
            default = shaped_names(path, character, weight)
            explicit_off = shaped_names(
                path,
                character,
                weight,
                {"ss02": False},
            )
            enabled = shaped_names(
                path,
                character,
                weight,
                {"ss02": True},
            )
            assert default == explicit_off
            assert default[0] == [source]
            assert enabled[0] == [alternate]
            assert enabled[1] == default[1]


@pytest.mark.parametrize("style", STYLES)
def test_latin_ext_ss02_follows_marshallese_l_cedilla_localization(style):
    path = subset_path(style, "latin-ext")
    for weight in (300, 400, 500, 600, 700):
        default = shaped_names(path, "ļ", weight, language="mh")
        explicit_off = shaped_names(
            path,
            "ļ",
            weight,
            {"ss02": False},
            language="mh",
        )
        enabled = shaped_names(
            path,
            "ļ",
            weight,
            {"ss02": True},
            language="mh",
        )
        assert default == explicit_off
        assert default[0] == ["uni013C.loclMAH"]
        assert enabled[0] == ["uni013C.loclMAH.ss02"]
        assert enabled[1] == default[1]


@pytest.mark.parametrize("style", STYLES)
def test_extension_slices_contain_representative_locale_characters(style):
    latin_ext = TTFont(subset_path(style, "latin-ext")).getBestCmap()
    vietnamese = TTFont(subset_path(style, "vietnamese")).getBestCmap()
    assert latin_ext[0x0100] == "Amacron"
    assert latin_ext[0x0142] == "lslash"
    assert 0x1EA0 in vietnamese  # LATIN CAPITAL LETTER A WITH DOT BELOW
    assert 0x1EF9 in vietnamese  # LATIN SMALL LETTER Y WITH TILDE


@pytest.mark.parametrize("style", STYLES)
def test_vietnamese_slice_does_not_gain_out_of_scope_ss02_coverage(style):
    font = TTFont(subset_path(style, "vietnamese"))
    assert "ss01" in feature_tags(font)
    assert "ss02" not in feature_tags(font)
    assert "tnum" not in feature_tags(font)
    assert not {
        ord(character) for character, _source, _alternate in SS02_FORMS
    } & set(font.getBestCmap())


def test_subset_css_declares_all_six_faces_and_exact_source_ranges():
    css = (DIST / "pivot-sans-subsets.css").read_text()
    assert css.count("@font-face") == 6
    assert css.count('font-family: "Pivot Sans"') == 6
    assert css.count("font-weight: 300 700") == 6
    assert css.count("font-style: normal") == 3
    assert css.count("font-style: italic") == 3
    for style in STYLES:
        for subset_name in SUBSET_NAMES:
            path = subset_path(style, subset_name)
            assert path.name in css
            tokens = range_tokens(
                WHITELIST_DIR / f"{subset_name}.txt"
            )
            assert f"unicode-range: {', '.join(tokens)};" in css


def test_subset_sizes_remain_within_release_budgets():
    limits = {
        "latin-core": 40 * 1024,
        "latin-ext": 20 * 1024,
        "vietnamese": 10 * 1024,
    }
    for style in STYLES:
        for subset_name, limit in limits.items():
            path = subset_path(style, subset_name)
            assert path.stat().st_size <= limit, (
                path.name,
                path.stat().st_size,
                limit,
            )
