#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Скіл 3, §3.6 — крок 1: перевірка кирилиці в полях форми PDF.

Генерує самостійний тестовий PDF з полями ВСІХ типів зі специфікації §3.2
(char / text / area / choice / multi / bool / done), половина полів заповнена
проблемним кириличним рядком, половина порожня — для ручного вводу.

Сторінки 1–2  — варіант "як у специфікації": базовий шрифт Helv + need_appearances.
Сторінка 3    — ПЛАН Б: Arial вбудований в AcroForm/DR як Type0/Identity-H
                (те, що дає page.insert_font), /DA полів посилається на нього.
Сторінка 4    — ПЛАН В: Arial вбудований в AcroForm/DR як ПРОСТИЙ TrueType
                з однобайтовим /Encoding /Differences (uniXXXX). Найсумісніший
                варіант; MuPDF уміє ним перегенерувати вигляд, тому його можна
                перевірити ще до ручного тесту.
На сторінках 3–4 /AP видалено навмисно — переглядач ЗОБОВ'ЯЗАНИЙ перемалювати
поля сам зі шрифту в /DR, і ми бачимо його справжню поведінку.

Ніяких submit-кнопок і ніяких полів на боксах балів (§3.0).

Запуск:  .venv/bin/python ua_build/formtest.py
"""
import os
import re
import sys

import fitz

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FORMS = os.path.join(ROOT, "forms")
ARIAL = "/System/Library/Fonts/Supplemental/Arial.ttf"
ARIAL_B = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"

# ---- вигляд полів (§3.5) --------------------------------------------------
HILITE = (1, 0.97, 0.72)      # блідо-жовта заливка
BORDER = (0.80, 0.74, 0.45)   # рамка
TXTCOL = (0, 0, 0.55)         # темно-синій введений текст

# ---- проблемний тестовий рядок -------------------------------------------
TEST = "Іван ЄЇҐ їжак ґудзик 'апостроф «лапки»"
TEST2 = "Ґанок ґречний ґуля — Ґ ґ Є є Ї ї І і’"

PAGE_W, PAGE_H = fitz.paper_size("a4")
MARGIN = 40.0
COLW = PAGE_W - 2 * MARGIN

# кожне поле, яке ми створили: (сторінка, ім'я, тип, "заповнене"/"порожнє")
INVENTORY = []


# ===========================================================================
#  дрібні помічники
# ===========================================================================
def next_version(prefix="formtest_ua"):
    """forms/formtest_ua_vNN.pdf — кожна збірка новий файл (§2.7.0.1)."""
    os.makedirs(FORMS, exist_ok=True)
    used = []
    for fn in os.listdir(FORMS):
        m = re.match(rf"^{re.escape(prefix)}_v(\d+)\.pdf$", fn)
        if m:
            used.append(int(m.group(1)))
    n = max(used) + 1 if used else 1
    return os.path.join(FORMS, f"{prefix}_v{n:02d}.pdf")


def pdf_name(s):
    """Кириличний рядок -> PDF-ім'я з #XX-екрануванням UTF-8 байтів."""
    out = "/"
    for b in s.encode("utf-8"):
        c = chr(b)
        if b < 0x21 or b > 0x7E or c in "#()<>[]{}/%":
            out += "#%02X" % b
        else:
            out += c
    return out


def label(page, x, y, text, size=8.5, bold=False, color=(0, 0, 0), w=None):
    """Підпис справжнім шрифтом з гліфами (не Helvetica!). Повертає нижній y."""
    fname = "uab" if bold else "ua"
    ffile = ARIAL_B if bold else ARIAL
    w = w if w else COLW
    box = fitz.Rect(x, y, x + w, y + 400)
    # спершу міряємо, скільки треба висоти
    rc = page.insert_textbox(box, text, fontname=fname, fontfile=ffile,
                             fontsize=size, color=color, align=0)
    if rc < 0:  # не влізло — розширюємо (страховка)
        box = fitz.Rect(x, y, x + w, y + 800)
        page.insert_textbox(box, text, fontname=fname, fontfile=ffile,
                            fontsize=size, color=color, align=0)
    nlines = text.count("\n") + 1 + int(len(text) / max(1, int(w / (size * 0.5))))
    return y + nlines * (size * 1.25)


def base_widget(name, rect, ftype):
    w = fitz.Widget()
    w.field_name = name
    w.field_type = ftype
    w.rect = fitz.Rect(rect)
    w.fill_color = HILITE
    w.border_color = BORDER
    w.border_width = 0.5
    w.text_color = TXTCOL
    return w


def add_char(page, name, rect, value=None):
    w = base_widget(name, rect, fitz.PDF_WIDGET_TYPE_TEXT)
    w.text_maxlen = 1
    w.text_align = fitz.TEXT_ALIGN_CENTER
    w.text_fontsize = 0            # автопідбір кегля
    if value:
        w.field_value = value
    a = page.add_widget(w)
    INVENTORY.append((page.number, name, "char", "filled" if value else "empty", value or ""))
    return a


def add_text(page, name, rect, value=None, size=11):
    w = base_widget(name, rect, fitz.PDF_WIDGET_TYPE_TEXT)
    w.text_align = fitz.TEXT_ALIGN_LEFT
    w.text_fontsize = size
    if value:
        w.field_value = value
    a = page.add_widget(w)
    INVENTORY.append((page.number, name, "text", "filled" if value else "empty", value or ""))
    return a


def add_area(page, name, rect, value=None, size=10):
    w = base_widget(name, rect, fitz.PDF_WIDGET_TYPE_TEXT)
    w.field_flags = fitz.PDF_TX_FIELD_IS_MULTILINE
    w.text_align = fitz.TEXT_ALIGN_LEFT
    w.text_fontsize = size
    if value:
        w.field_value = value
    a = page.add_widget(w)
    INVENTORY.append((page.number, name, "area", "filled" if value else "empty", value or ""))
    return a


def add_check(page, name, rect, on=False, kind="multi"):
    w = base_widget(name, rect, fitz.PDF_WIDGET_TYPE_CHECKBOX)
    if kind == "done":                 # §3.5: без заливки, лише рамка
        w.fill_color = None
        w.border_color = (0.35, 0.35, 0.35)
    w.text_font = "ZaDb"               # стиль «галочка»
    w.field_value = bool(on)
    a = page.add_widget(w)
    INVENTORY.append((page.number, name, kind, "filled" if on else "empty",
                      "Yes" if on else "Off"))
    return a


def add_combo(page, name, rect, values, value=None, size=10):
    w = base_widget(name, rect, fitz.PDF_WIDGET_TYPE_COMBOBOX)
    w.choice_values = list(values)
    w.text_fontsize = size
    if value:
        w.field_value = value
    a = page.add_widget(w)
    INVENTORY.append((page.number, name, "bool", "filled" if value else "empty", value or ""))
    return a


# ---------------------------------------------------------------------------
#  radio-група з кириличними on-значеннями (§3.4: on-значення = текст варіанта)
# ---------------------------------------------------------------------------
def add_radio_group(doc, page, group, opts, rects, selected=None):
    """PyMuPDF робить із однойменних radio окремі поля без /Parent-/Kids —
    для Acrobat це неправильно. Тому додаємо віджети, а потім власноруч
    збираємо канонічну структуру: одне батьківське поле + /Kids, а on-стани
    перейменовуємо на самі українські слова."""
    kids = []
    for opt, r in zip(opts, rects):
        w = base_widget(group, r, fitz.PDF_WIDGET_TYPE_RADIOBUTTON)
        w.field_value = False          # інакше _checker() падає на bad xref
        a = page.add_widget(w)
        kids.append((a.xref, opt))
        INVENTORY.append((page.number, f"{group}[{opt}]", "choice",
                          "filled" if opt == selected else "empty", opt))

    # 1. перейменувати ключ /Yes у /AP/N на кириличне значення
    for xref, opt in kids:
        typ, val = doc.xref_get_key(xref, "AP/N")
        refs = dict(re.findall(r"/(\w+)\s+(\d+)\s+0\s+R", val))
        off = refs.get("Off")
        on = refs.get("Yes") or refs.get("On")
        newn = pdf_name(opt)
        doc.xref_set_key(xref, "AP/N", f"<</Off {off} 0 R{newn} {on} 0 R>>")

    # 2. батьківське поле
    parent = doc.get_new_xref()
    kidrefs = " ".join(f"{x} 0 R" for x, _ in kids)
    v = pdf_name(selected) if selected else "/Off"
    doc.update_object(parent,
                      f"<</FT/Btn/Ff 49152/T({group})/V{v}/DV{v}"
                      f"/DA(/Helv 0 Tf 0 0 0.55 rg)/Kids[{kidrefs}]>>")

    # 3. діти: без власних /T, /FT, /Ff — тільки /Parent і /AS
    for xref, opt in kids:
        for k in ("T", "FT", "Ff", "V", "DV"):
            doc.xref_set_key(xref, k, "null")
        doc.xref_set_key(xref, "Parent", f"{parent} 0 R")
        doc.xref_set_key(xref, "AS", pdf_name(opt) if opt == selected else "/Off")

    # 4. AcroForm/Fields: діти геть, батько всередину
    acro = doc.pdf_catalog()
    typ, val = doc.xref_get_key(acro, "AcroForm/Fields")
    have = [int(x) for x in re.findall(r"(\d+)\s+0\s+R", val or "")]
    kidset = {x for x, _ in kids}
    keep = [x for x in have if x not in kidset] + [parent]
    doc.xref_set_key(acro, "AcroForm/Fields",
                     "[" + " ".join(f"{x} 0 R" for x in keep) + "]")
    return parent


# ---------------------------------------------------------------------------
#  вбудований кириличний шрифт у AcroForm/DR (план Б)
# ---------------------------------------------------------------------------
def embed_dr_font(doc, page, resname="UAF"):
    """Кладемо Arial в /AcroForm/DR/Font під іменем UAF. Повертає xref."""
    xref = page.insert_font(fontname=resname, fontfile=ARIAL)
    acro = doc.pdf_catalog()
    doc.xref_set_key(acro, f"AcroForm/DR/Font/{resname}", f"{xref} 0 R")
    return xref


def set_da(doc, xref, resname="UAF", size=11):
    """Переписати /DA віджета на вбудований шрифт (PyMuPDF дозволяє лише
    Helv/Cour/TiRo/ZaDb через text_font, тому правимо об'єкт напряму)."""
    doc.xref_set_key(xref, "DA",
                     f"({'/%s %g Tf 0 0 0.55 rg' % (resname, size)})")


def drop_ap(doc, xref):
    """Прибрати готовий вигляд — хай переглядач малює сам зі шрифту в /DR."""
    doc.xref_set_key(xref, "AP", "null")


# коди 128..255 віддаємо українській кирилиці й типографіці
CYR_CODES = (
    "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ"
    "абвгґдеєжзиіїйклмнопрстуфхцчшщьюя"
    "ЪЫЭЁъыэё’‘“”«»—–…№"
)


def embed_simple_dr_font(doc, resname="UAS"):
    """Вбудувати Arial у /AcroForm/DR/Font як ПРОСТИЙ TrueType-шрифт з
    однобайтовим кодуванням: /BaseEncoding /WinAnsiEncoding + /Differences,
    де 128..255 — українські літери (імена гліфів у формі uniXXXX).

    Це найсумісніший спосіб дати переглядачу кирилицю для полів форми:
    генератор вигляду мапить символ -> код через /Differences, а код -> гліф
    через post/cmap шрифту. Працює і в MuPDF, і в Acrobat, і в Preview."""
    data = open(ARIAL, "rb").read()
    f = fitz.Font(fontfile=ARIAL)

    # 1) потік FontFile2
    ff = doc.get_new_xref()
    doc.update_object(ff, "<<>>")
    doc.update_stream(ff, data, compress=True)
    doc.xref_set_key(ff, "Length1", str(len(data)))

    # 2) FontDescriptor
    fd = doc.get_new_xref()
    asc = int(f.ascender * 1000)
    dsc = int(f.descender * 1000)
    doc.update_object(fd, (
        "<</Type/FontDescriptor/FontName/ArialMT/Flags 32"
        "/FontBBox[-665 -325 2000 1006]/ItalicAngle 0"
        f"/Ascent {asc}/Descent {dsc}/CapHeight 716/StemV 88"
        f"/FontFile2 {ff} 0 R>>"))

    # 3) /Differences 128..255
    diffs, widths = [], []
    for i, ch in enumerate(CYR_CODES[:128]):
        diffs.append(f"/uni{ord(ch):04X}")
    diff_str = "128 " + " ".join(diffs)

    enc = doc.get_new_xref()
    doc.update_object(enc, ("<</Type/Encoding/BaseEncoding/WinAnsiEncoding"
                            f"/Differences[{diff_str}]>>"))

    # 4) /Widths для 32..255
    code2ch = {c: chr(c) for c in range(32, 127)}
    for i, ch in enumerate(CYR_CODES[:128]):
        code2ch[128 + i] = ch
    for c in range(32, 256):
        ch = code2ch.get(c)
        w = f.glyph_advance(ord(ch)) * 1000 if ch else 0
        widths.append(str(int(round(w))))

    fo = doc.get_new_xref()
    doc.update_object(fo, (
        "<</Type/Font/Subtype/TrueType/BaseFont/ArialMT"
        f"/FirstChar 32/LastChar 255/Widths[{' '.join(widths)}]"
        f"/Encoding {enc} 0 R/FontDescriptor {fd} 0 R>>"))

    acro = doc.pdf_catalog()
    doc.xref_set_key(acro, f"AcroForm/DR/Font/{resname}", f"{fo} 0 R")
    return fo


# ===========================================================================
#  збірка
# ===========================================================================
def build(out):
    doc = fitz.open()

    # ---------------- сторінка 1: char / text / area --------------------
    p1 = doc.new_page(width=PAGE_W, height=PAGE_H)
    y = MARGIN
    label(p1, MARGIN, y, "ТЕСТ КИРИЛИЦІ В ПОЛЯХ ФОРМИ — сторінка 1/3", 14, bold=True)
    y += 22
    label(p1, MARGIN, y,
          "Сторінки 1–2: базовий шрифт (Helv) + need_appearances — так, як описано в специфікації.\n"
          "Що робити: впиши текст у ПОРОЖНІ поля, натисни Cmd+S (зберегти), закрий файл, відкрий знову.\n"
          "Перевіряй: (1) чи вводяться літери, (2) чи видно Ґ ґ Є Ї і ї ’ « », (3) чи текст не зник після збереження.", 9)
    y += 46

    # --- A. char --------------------------------------------------------
    label(p1, MARGIN, y, "A. char — односимвольні клітинки (центрування, автопідбір кегля)", 11, bold=True)
    y += 16
    label(p1, MARGIN, y, "A1 (ЗАПОВНЕНО програмно): має читатися  Ґ ґ Є Ї і ї ’ «  — особливо дивись на Ґ і ґ.", 8.5)
    y += 14
    cw, ch, gap = 26, 28, 4
    chars = ["Ґ", "ґ", "Є", "Ї", "і", "ї", "’", "«"]
    for i, c in enumerate(chars):
        x = MARGIN + i * (cw + gap)
        add_char(p1, f"p01_cellA{i+1}", (x, y, x + cw, y + ch), value=c)
    y += ch + 10
    label(p1, MARGIN, y, "A2 (ПОРОЖНІ — впиши сам): набери по літері  Ґ ґ Є Ї і ї ’ « — по одній у клітинку.", 8.5)
    y += 14
    for i in range(8):
        x = MARGIN + i * (cw + gap)
        add_char(p1, f"p01_cellB{i+1}", (x, y, x + cw, y + ch))
    y += ch + 18

    # --- B. text --------------------------------------------------------
    label(p1, MARGIN, y, "B. text — однорядкове поле", 11, bold=True)
    y += 16
    label(p1, MARGIN, y, "B1 (ЗАПОВНЕНО): має бути видно весь рядок, без квадратиків і без обрізання праворуч.", 8.5)
    y += 13
    add_text(p1, "p01_text1", (MARGIN, y, MARGIN + COLW, y + 20), value=TEST)
    y += 26
    label(p1, MARGIN, y, "B2 (ПОРОЖНЄ — впиши): Іван ЄЇҐ їжак ґудзик 'апостроф «лапки» — потім збережи і відкрий знову.", 8.5)
    y += 13
    add_text(p1, "p01_text2", (MARGIN, y, MARGIN + COLW, y + 20))
    y += 26
    label(p1, MARGIN, y, "B3 (ЗАПОВНЕНО, вузьке поле): перевір, чи не обрізає текст у короткому полі.", 8.5)
    y += 13
    add_text(p1, "p01_text3", (MARGIN, y, MARGIN + 170, y + 18), value="Ґанок ґречний", size=10)
    y += 26

    # --- C. area --------------------------------------------------------
    label(p1, MARGIN, y, "C. area — багаторядкове поле", 11, bold=True)
    y += 16
    label(p1, MARGIN, y, "C1 (ЗАПОВНЕНО, 3 рядки): перевір перенос рядків і всі проблемні літери.", 8.5)
    y += 13
    add_area(p1, "p01_area1", (MARGIN, y, MARGIN + COLW, y + 60),
             value=TEST + "\n" + TEST2 + "\nдругий рядок: Ґудзик ґанок ґрунт")
    y += 66
    label(p1, MARGIN, y, "C2 (ПОРОЖНЄ — впиши 2–3 рядки українською з Ґ, ґ, апострофом і «лапками»).", 8.5)
    y += 13
    add_area(p1, "p01_area2", (MARGIN, y, MARGIN + COLW, y + 60))
    y += 70

    label(p1, MARGIN, y,
          "ПІСЛЯ ЗБЕРЕЖЕННЯ: закрий файл і відкрий знову — усе введене має лишитися на місці.", 9, bold=True)

    # ---------------- сторінка 2: choice / multi / bool / done ----------
    p2 = doc.new_page(width=PAGE_W, height=PAGE_H)
    y = MARGIN
    label(p2, MARGIN, y, "ТЕСТ КИРИЛИЦІ В ПОЛЯХ ФОРМИ — сторінка 2/3", 14, bold=True)
    y += 24

    # --- D. choice (radio) ---------------------------------------------
    label(p2, MARGIN, y, "D. choice — radio-група; значення кнопок = самі українські слова", 11, bold=True)
    y += 16
    label(p2, MARGIN, y,
          "D1 (ПЕРЕДВИБРАНО «ґанок»): перевір, що позначку видно; потім перемкни на «хмара», збережи,\n"
          "відкрий знову — має лишитися «хмара». Кнопки мають бути взаємовиключні (одна за раз).", 8.5)
    y += 26
    opts = ["сонце", "хмара", "ґанок"]
    rects, lx = [], MARGIN
    for opt in opts:
        rects.append((lx, y, lx + 13, y + 13))
        label(p2, lx + 18, y - 1, opt, 10)
        lx += 110
    add_radio_group(doc, p2, "p02_q1", opts, rects, selected="ґанок")
    y += 30

    # --- E. multi (checkbox) --------------------------------------------
    label(p2, MARGIN, y, "E. multi — незалежні чекбокси", 11, bold=True)
    y += 16
    label(p2, MARGIN, y,
          "E1 позначено програмно, E2/E3 порожні. Познач E2 і E3, збережи, відкрий — мають лишитися позначеними.", 8.5)
    y += 16
    for i, (word, on) in enumerate([("їжак", True), ("ґудзик", False), ("Єва", False)]):
        x = MARGIN + i * 110
        add_check(p2, f"p02_q2_opt{i+1}", (x, y, x + 13, y + 13), on=on, kind="multi")
        label(p2, x + 18, y - 1, word, 10)
    y += 32

    # --- F. bool (combobox) ---------------------------------------------
    label(p2, MARGIN, y, "F. bool — випадний список ПРАВДА / НЕПРАВДА", 11, bold=True)
    y += 16
    label(p2, MARGIN, y, "F1 (ЗАПОВНЕНО «ПРАВДА»): чи видно значення в закритому списку?", 8.5)
    y += 14
    add_combo(p2, "p02_tf1", (MARGIN, y, MARGIN + 130, y + 18),
              ["ПРАВДА", "НЕПРАВДА"], value="ПРАВДА")
    y += 26
    label(p2, MARGIN, y, "F2 (ПОРОЖНЄ): розкрий список — чи читаються обидва варіанти? Обери НЕПРАВДА, збережи, відкрий.", 8.5)
    y += 14
    add_combo(p2, "p02_tf2", (MARGIN, y, MARGIN + 130, y + 18), ["ПРАВДА", "НЕПРАВДА"])
    y += 32

    # --- G. done ---------------------------------------------------------
    label(p2, MARGIN, y, "G. done — чекбокс «виконано» (без заливки, лише рамка)", 11, bold=True)
    y += 16
    label(p2, MARGIN, y, "G1 позначено програмно, G2 порожній — познач його, збережи, відкрий.", 8.5)
    y += 16
    label(p2, MARGIN, y, "Розфарбуй малюнок  (виконано)", 9)
    add_check(p2, "p02_done1", (MARGIN + 190, y - 1, MARGIN + 201, y + 10), on=True, kind="done")
    y += 20
    label(p2, MARGIN, y, "З'єднай лінією  (виконано)", 9)
    add_check(p2, "p02_done2", (MARGIN + 190, y - 1, MARGIN + 201, y + 10), on=False, kind="done")
    y += 34

    label(p2, MARGIN, y,
          "ЩЕ ОДНА ПЕРЕВІРКА: відкрий цей самий файл у другому переглядачі (Preview / Acrobat / телефон)\n"
          "і подивись, чи видно там текст, який ти ввів у першому.", 9, bold=True)

    # ---------------- сторінки 3 і 4: варіанти з шрифтом у DR -----------
    def dr_page(no, title, subtitle, prefix, resname):
        pg = doc.new_page(width=PAGE_W, height=PAGE_H)
        yy = MARGIN
        label(pg, MARGIN, yy, title, 12.5, bold=True)
        yy += 20
        label(pg, MARGIN, yy, subtitle, 9)
        yy += 46
        ws = []
        label(pg, MARGIN, yy, "H. char — заповнено програмно + порожній ряд нижче", 11, bold=True)
        yy += 16
        for i, c in enumerate(chars):
            x = MARGIN + i * (cw + gap)
            ws.append((add_char(pg, f"{prefix}_cellA{i+1}", (x, yy, x + cw, yy + ch), value=c), 0))
        yy += ch + 8
        for i in range(8):
            x = MARGIN + i * (cw + gap)
            ws.append((add_char(pg, f"{prefix}_cellB{i+1}", (x, yy, x + cw, yy + ch)), 0))
        yy += ch + 18

        label(pg, MARGIN, yy, "I. text", 11, bold=True)
        yy += 16
        ws.append((add_text(pg, f"{prefix}_text1", (MARGIN, yy, MARGIN + COLW, yy + 20), value=TEST), 11))
        yy += 26
        label(pg, MARGIN, yy, "порожнє — впиши те саме:", 8.5)
        yy += 13
        ws.append((add_text(pg, f"{prefix}_text2", (MARGIN, yy, MARGIN + COLW, yy + 20)), 11))
        yy += 30

        label(pg, MARGIN, yy, "J. area", 11, bold=True)
        yy += 16
        ws.append((add_area(pg, f"{prefix}_area1", (MARGIN, yy, MARGIN + COLW, yy + 55),
                            value=TEST + "\n" + TEST2), 10))
        yy += 62
        ws.append((add_area(pg, f"{prefix}_area2", (MARGIN, yy, MARGIN + COLW, yy + 55)), 10))
        yy += 66

        label(pg, MARGIN, yy, "K. bool", 11, bold=True)
        yy += 16
        ws.append((add_combo(pg, f"{prefix}_tf1", (MARGIN, yy, MARGIN + 130, yy + 18),
                             ["ПРАВДА", "НЕПРАВДА"], value="НЕПРАВДА"), 10))
        yy += 24
        ws.append((add_combo(pg, f"{prefix}_tf2", (MARGIN, yy, MARGIN + 130, yy + 18),
                             ["ПРАВДА", "НЕПРАВДА"]), 10))
        yy += 30
        return pg.number, ws

    p3n, w3 = dr_page(
        3, "ПЛАН Б — сторінка 3/4: Arial у DR як Type0 / Identity-H",
        "Ці поля посилаються не на базовий Helv, а на вбудований Arial (/UAF у /AcroForm/DR/Font),\n"
        "і в них НЕМАЄ готового вигляду (/AP) — переглядач мусить намалювати їх сам.\n"
        "Якщо тут порожньо, а на стор. 1 текст видно — цей варіант переглядач не тягне.",
        "p03", "UAF")
    p4n, w4 = dr_page(
        4, "ПЛАН В — сторінка 4/4: Arial у DR як простий TrueType (1-байтове кодування)",
        "Той самий Arial, але як звичайний TrueType з /Encoding /Differences (uniXXXX) — 128..255 віддані\n"
        "українській кирилиці. /AP теж видалено. Це найсумісніший варіант; порівняй його зі стор. 1 і 3.",
        "p04", "UAS")

    label(doc[p4n], MARGIN, MARGIN + 640,
          "ПІДСУМОК ДЛЯ ТЕБЕ: запам'ятай, на якій зі сторінок 1 / 3 / 4 текст виглядає правильно\n"
          "(особливо Ґ ґ Ї « » ’) І залишається після збереження — саме той варіант підемо робити на 120 уроків.",
          9, bold=True)

    # вбудувати шрифти й перепризначити /DA + прибрати /AP
    embed_dr_font(doc, doc[p3n], "UAF")
    embed_simple_dr_font(doc, "UAS")
    for ws, res in ((w3, "UAF"), (w4, "UAS")):
        for annot, size in ws:
            set_da(doc, annot.xref, res, size if size else 0)
            drop_ap(doc, annot.xref)

    # ---------------- збереження ---------------------------------------
    doc.need_appearances(True)
    doc.save(out, deflate=True, garbage=3)
    doc.close()
    return out


# ===========================================================================
#  автоматичні перевірки
# ===========================================================================
def render(path, outdir, dpi=110):
    os.makedirs(outdir, exist_ok=True)
    doc = fitz.open(path)
    files = []
    for i, page in enumerate(doc):
        pm = page.get_pixmap(dpi=dpi)
        fn = os.path.join(outdir, f"formtest_p{i+1}.png")
        pm.save(fn)
        files.append(fn)
    doc.close()
    return files


def roundtrip(path, outdir):
    """Заповнити ВСІ поля кирилицею, зберегти, відкрити заново, звірити."""
    os.makedirs(outdir, exist_ok=True)
    tmp = os.path.join(outdir, "roundtrip.pdf")
    doc = fitz.open(path)
    written = {}
    for page in doc:
        for w in page.widgets():
            ft = w.field_type
            name = w.field_name
            if ft == fitz.PDF_WIDGET_TYPE_TEXT:
                val = "Ґ" if w.text_maxlen == 1 else TEST
                w.field_value = val
                w.update()
                written[name] = val
            elif ft == fitz.PDF_WIDGET_TYPE_COMBOBOX:
                w.field_value = "НЕПРАВДА"
                w.update()
                written[name] = "НЕПРАВДА"
            elif ft == fitz.PDF_WIDGET_TYPE_CHECKBOX:
                w.field_value = True
                w.update()
                written[name] = "Yes"
            elif ft == fitz.PDF_WIDGET_TYPE_RADIOBUTTON:
                pass  # обробляємо групою нижче (див. select_radio)

    # radio: перемкнути групу на «сонце» — правимо /V батька і /AS дітей.
    # (Widget.update() для radio у PyMuPDF не вміє кириличних on-станів.)
    pick = "сонце"
    acro = doc.pdf_catalog()
    fields = [int(x) for x in
              re.findall(r"(\d+) 0 R", doc.xref_get_key(acro, "AcroForm/Fields")[1] or "")]
    for x in fields:
        obj = doc.xref_object(x)
        if "/T (p02_q1)" not in obj:
            continue
        doc.xref_set_key(x, "V", pdf_name(pick))
        for k in re.findall(r"(\d+) 0 R", doc.xref_get_key(x, "Kids")[1] or ""):
            k = int(k)
            doc.xref_set_key(k, "AS",
                             pdf_name(pick) if pdf_name(pick)[1:] in doc.xref_get_key(k, "AP/N")[1]
                             else "/Off")
        written["p02_q1"] = pick

    doc.need_appearances(True)
    doc.save(tmp, deflate=True, garbage=3)
    doc.close()

    doc2 = fitz.open(tmp)
    got = {}
    for page in doc2:
        for w in page.widgets():
            # у radio-групі всі кнопки мають одне ім'я: цікавить увімкнена
            if got.get(w.field_name) not in (None, "Off"):
                continue
            got[w.field_name] = w.field_value
    fonts = {}
    for page in doc2:
        for w in page.widgets():
            fonts[w.field_name] = (w.text_font, w.text_fontsize)
    doc2.close()
    return tmp, written, got, fonts


def report_fields(path):
    doc = fitz.open(path)
    rows = []
    for page in doc:
        for w in page.widgets():
            rows.append((page.number + 1, w.field_name, w.field_type_string,
                         repr(w.field_value), w.text_font, w.text_fontsize,
                         doc.xref_get_key(w.xref, "DA")[1]))
    acro = doc.pdf_catalog()
    dr = doc.xref_get_key(acro, "AcroForm/DR/Font")
    na = doc.xref_get_key(acro, "AcroForm/NeedAppearances")
    doc.close()
    return rows, dr, na


if __name__ == "__main__":
    out = next_version()
    build(out)
    size = os.path.getsize(out)
    print(f"збережено: {out}  ({size/1024:.0f} КБ)")

    scratch = os.environ.get("FORMTEST_OUT", os.path.join(FORMS, "_formtest_render"))
    pngs = render(out, scratch)
    print("рендер:", *pngs, sep="\n  ")

    rows, dr, na = report_fields(out)
    print(f"\nполів: {len(rows)}   AcroForm/DR/Font={dr}   NeedAppearances={na}")
    print(f"{'стор':>4} {'ім\'я':<16} {'тип':<12} {'шрифт':<6} {'кегль':>5}  значення / DA")
    for r in rows:
        print(f"{r[0]:>4} {r[1]:<16} {r[2]:<12} {r[4]:<6} {r[5]:>5}  {r[3]}  DA={r[6]}")

    tmp, written, got, fonts = roundtrip(out, scratch)
    print(f"\n--- КРУГОВИЙ ТЕСТ ({tmp}) ---")
    ok = bad = 0
    for name in sorted(written):
        exp, act = written[name], got.get(name)
        good = (exp == act)
        ok, bad = (ok + 1, bad) if good else (ok, bad + 1)
        print(f"{'OK ' if good else 'FAIL'} {name:<16} записано={exp!r} прочитано={act!r} шрифт={fonts.get(name)}")
    print(f"разом: OK={ok} FAIL={bad}")
