# -*- coding: utf-8 -*-
"""CHO_TRONG.PY — bộ điền chỗ trống trong văn bản soạn sẵn, bằng dữ liệu SỐNG.

VÌ SAO FILE NÀY TỒN TẠI
------------------------
Chủ website soạn câu trả lời tư vấn trong phần quản trị. Luật là văn bản
soạn sẵn KHÔNG được chứa giá, link tải hay số phiên bản gõ cứng — vì giá
đổi thì câu trả lời thành sai mà không ai biết (bài học từ bot cũ: nó đọc
giá từ một tệp JSON đóng băng và trả lời sai giá cho khách suốt một thời
gian dài, xem chú thích đầu `san_pham.py`). Thay vào đó văn bản soạn sẵn
chứa CHỖ TRỐNG kiểu `{gia:dcabot}`, và module này điền chúng bằng dữ liệu
sống ngay lúc trả lời khách, tái dùng `khuon_mau.py` để ghép câu.

LUẬT SỐNG CÒN
-------------
Nếu một chỗ trống KHÔNG điền được (mã sản phẩm không có, CSDL không nối
được, giá trống), `KetQuaDien.du` PHẢI là False. Bên gọi dựa vào cờ này để
BỎ câu trả lời soạn sẵn và rơi xuống tầng dưới — tuyệt đối không được để
một câu còn nguyên `{gia:dcabot}` đi tới khách. Dù không điền được, chỗ
trống vẫn được thay bằng chữ trung tính (vd trỏ về trang bảng giá) để
`van_ban` KHÔNG BAO GIỜ còn ký tự '{', nhưng `du` vẫn là False.

HAI CÁI BẪY ĐÃ TỪNG CẮN TRONG DỰ ÁN NÀY
----------------------------------------
1. Bẫy khớp chuỗi con: 'dca' nằm trong 'broa-dca-ster'. Mọi so khớp mã sản
   phẩm ở đây đều là so khớp CHÍNH XÁC với hằng `SAN_PHAM` (qua toán tử
   `in` trên TUPLE, tức so bằng từng phần tử — không phải `in` trên chuỗi).
2. Bẫy bỏ dấu: KHÔNG bỏ dấu tiếng Việt ở đây. Chỉ chuẩn hoá chữ thường và
   khoảng trắng cho tên chỗ trống/mã sản phẩm (vốn luôn là ASCII không dấu
   theo `SAN_PHAM`), giữ nguyên phần còn lại của văn bản.

HIỆU NĂNG: một câu trả lời có thể có nhiều chỗ trống cùng trỏ một sản
phẩm — mỗi sản phẩm chỉ tra một lần cho mỗi lần gọi `dien()` (xem phần
"gom tra cứu" bên dưới). KHÔNG thêm tầng cache dài hạn nào ở đây:
`san_pham.py` đã có cache 60 giây có chủ ý, còn link tải tra thẳng CSDL
qua `csdl.py` mỗi lần gọi (không cache) — đủ nhanh cho một lần gọi
`dien()`, và tránh đúng lỗi "cache vô hạn" đã giết bot cũ.

Cách chạy tự kiểm (không cần CSDL, dùng nguồn dữ liệu giả tiêm vào):
    python congcu/cho_trong.py
"""
from __future__ import annotations

import logging
import os
import re
import sys
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Set

try:
    from ..giao_uoc import SAN_PHAM, SanPham, TELEGRAM, ZALO
    from . import csdl, khuon_mau, san_pham
except ImportError:
    # PHẢI thêm CẢ HAI thư mục, không phải một:
    #   - thư mục CHA (gốc chatbot) để `import giao_uoc` chạy được;
    #   - thư mục CỦA CHÍNH TỆP NÀY (congcu/) để `import csdl`, `khuon_mau`,
    #     `san_pham` chạy được.
    # Bản đầu chỉ thêm thư mục cha, nên `python congcu/cho_trong.py` thì chạy
    # (vì sys.path[0] tình cờ đã là congcu/) còn `from congcu.cho_trong import
    # dien` — tức đúng đường mà nao/tra_loi.py dùng — thì vỡ với
    # ModuleNotFoundError: No module named 'csdl'. Tự kiểm chạy thẳng tệp KHÔNG
    # bắt được lỗi này; lỗi y hệt đã từng xảy ra ở tim/lai.py.
    _THU_MUC_TOI = os.path.dirname(os.path.abspath(__file__))
    _THU_MUC_CHA = os.path.dirname(_THU_MUC_TOI)
    for _d in (_THU_MUC_CHA, _THU_MUC_TOI):
        if _d not in sys.path:
            sys.path.insert(0, _d)
    from giao_uoc import SAN_PHAM, SanPham, TELEGRAM, ZALO
    import csdl
    import khuon_mau
    import san_pham


# =====================================================================
# CÚ PHÁP CHỖ TRỐNG
# =====================================================================

# {ten_loai} hoặc {ten_loai:tham_so}. tham_so không được chứa '{' hay '}'
# (một chỗ trống không được lồng chỗ trống khác).
CHO_TRONG_RE = re.compile(r'\{([a-zA-Z_]+)(?::([^{}]*))?\}')

# Các loại chỗ trống BẮT BUỘC có tham số (mã sản phẩm).
CAC_LOAI_CAN_THAM_SO = ('gia', 'link_tai', 'ten')
# Các loại KHÔNG được có tham số.
CAC_LOAI_KHONG_THAM_SO = ('bang_gia', 'zalo', 'telegram')

# Tên gọi quen trong kho tư vấn -> slug sản phẩm (mã gốc của vài sản phẩm là chuỗi băm).
BI_DANH: Dict[str, str] = {
    'tradingview': 'tool-1',
}
CAC_LOAI_HOP_LE = frozenset(CAC_LOAI_CAN_THAM_SO) | frozenset(CAC_LOAI_KHONG_THAM_SO)


def _chuan_hoa(chuoi: str) -> str:
    """Chỉ chuẩn hoá chữ thường + khoảng trắng — KHÔNG bỏ dấu. Dùng cho cả
    tên loại chỗ trống lẫn mã sản phẩm (cả hai vốn là ASCII không dấu
    trong SAN_PHAM/CAC_LOAI_HOP_LE nên lower() là an toàn, không đụng chữ
    có dấu của khách)."""
    return chuoi.strip().lower()


def _hop_le(ten_loai: Optional[str], tham_so: Optional[str]) -> bool:
    """True nếu (tên loại, tham số) tạo thành một chỗ trống hợp lệ.
    KHÔNG chạm CSDL: chỉ so khớp CHÍNH XÁC với hằng SAN_PHAM tĩnh."""
    if ten_loai is None or _chuan_hoa(ten_loai) not in CAC_LOAI_HOP_LE:
        return False
    ten_loai = _chuan_hoa(ten_loai)
    co_tham_so = tham_so is not None and tham_so.strip() != ''
    if ten_loai in CAC_LOAI_CAN_THAM_SO:
        if not co_tham_so:
            return False
        # So khớp CHÍNH XÁC cả chuỗi với từng phần tử của tuple — không
        # phải `in` trên chuỗi, nên 'broadcaster' KHÔNG khớp nhầm
        # 'signalbroadcaster' (tránh đúng bẫy chuỗi con đã dặn ở đầu tệp).
        if _chuan_hoa(tham_so) not in SAN_PHAM:
            return False
    else:
        if co_tham_so:
            return False
    return True


def kiem_cu_phap(van_ban: str) -> List[str]:
    """Trả danh sách chỗ trống SAI cú pháp hoặc tên/mã không hợp lệ,
    nguyên văn như chúng xuất hiện trong văn bản. KHÔNG chạm CSDL — dùng
    cho phần quản trị lúc lưu, và cho tự kiểm."""
    loi: List[str] = []
    if not van_ban:
        return loi

    for khop in CHO_TRONG_RE.finditer(van_ban):
        if not _hop_le(khop.group(1), khop.group(2)):
            loi.append(khop.group(0))

    # Dấu ngoặc nhọn lẻ (không tạo thành một cặp {..} hợp khuôn dạng ở
    # trên) — vd thiếu dấu đóng. Không chỉ ra vị trí chi tiết, chỉ báo có.
    phan_con_lai = CHO_TRONG_RE.sub('', van_ban)
    if '{' in phan_con_lai or '}' in phan_con_lai:
        loi.append('(có dấu ngoặc { } lẻ, không đúng khuôn dạng chỗ trống)')

    return loi


# =====================================================================
# KẾT QUẢ ĐIỀN
# =====================================================================

@dataclass
class KetQuaDien:
    van_ban: str
    du: bool                    # True = điền được HẾT, an toàn để gửi khách
    thieu: List[str] = field(default_factory=list)   # chỗ trống không điền được, nguyên văn


# =====================================================================
# CHỮ TRUNG TÍNH DÙNG KHI KHÔNG ĐIỀN ĐƯỢC
# =====================================================================

def _lien_he(khong_gian: Optional[str]) -> Dict[str, str]:
    """Liên hệ của shop (shop/). Hỏng thì rỗng — nơi gọi rơi về ZALO/TELEGRAM mặc định."""
    try:
        import shop
        return dict(shop.lay(khong_gian).lien_he)
    except Exception:  # noqa: BLE001
        return {}


def _chu_trung_lap(ngon_ngu: str) -> str:
    if ngon_ngu == 'vi':
        return f"xem thông tin mới nhất tại {khuon_mau.DUONG_DAN_BANG_GIA}"
    return f"see the latest info at {khuon_mau.DUONG_DAN_BANG_GIA}"


def _chu_khong_co_link(ngon_ngu: str) -> str:
    if ngon_ngu == 'vi':
        return f"anh/chị nhắn Zalo {ZALO} hoặc Telegram {TELEGRAM} để nhận link tải giúp em ạ"
    return f"please message Zalo {ZALO} or Telegram {TELEGRAM} for the download link"


# =====================================================================
# TRA LINK TẢI SỐNG — cột product.download_url (VARCHAR(500) NULL)
# =====================================================================
# Cột và bảng đã tra trong repo:
#   - private/sql/2026-09-16_product_model.sql dòng 47: `download_url`
#     VARCHAR(500) NULL trong bảng `product`.
#   - public/admin/product-edit.php dòng 70/183/423: form quản trị đọc/ghi
#     đúng cột `download_url` của bảng `product`.
# Không cache ở đây — mỗi lần gọi dien() tự gom (xem `dien()`), không cần
# thêm tầng cache dài hạn theo đúng lời dặn đầu tệp.

def _gia_gon(sp, ngon_ngu: str) -> str:
    """Giá dạng MẢNH GHÉP để nhét vào GIỮA câu chủ website tự viết.

    VÌ SAO không dùng thẳng `khuon_mau.cau_gia()`: hàm đó sinh ra một câu HOÀN
    CHỈNH, mở đầu bằng "Dạ giá <tên sản phẩm> (đơn vị credit...):" rồi xuống
    dòng liệt kê. Nó đúng khi bot tự trả lời câu hỏi giá, nhưng chủ website
    viết mục tư vấn theo kiểu "Dạ {ten:dcabot} {gia:dcabot} ạ." — ghép vào ra:

        Dạ SMART DCA EA  Dạ giá SMART DCA EA (đơn vị credit...):
        - 1 tháng: 20 credit

    Hai chữ "Dạ", tên sản phẩm lặp hai lần, và một câu bị chẻ làm đôi giữa
    chừng. Đã thấy thật khi chạy thử đầu-cuối trên CSDL.

    Dạng gọn: "20 credit/tháng, 180 credit/12 tháng" — nhét vào đâu cũng xuôi.
    `{bang_gia}` vẫn giữ dạng khối đầy đủ, vì nó vốn đứng riêng một mình.
    """
    cac = []
    for goi in sp.cac_goi:
        so = khuon_mau._dinh_dang_so(goi.gia)
        nhan = khuon_mau._nhan_goi(goi, ngon_ngu)
        cac.append('%s credit/%s' % (so, nhan.lower()))
    return ', '.join(cac)


def _link_tai_song(ma_ds: List[str]) -> Dict[str, Optional[str]]:
    """Tra download_url cho một danh sách mã sản phẩm, MỘT lần truy vấn
    cho toàn bộ danh sách. Trả {} nếu không tra được (thiếu pymysql/CSDL)
    — bot thà không biết còn hơn sập, giống quy ước của san_pham.py."""
    if not ma_ds:
        return {}

    ket_noi = csdl.ket_noi()  # tự lo pymysql/.env/kết nối, không bao giờ ném lỗi
    if ket_noi is None:
        return {}

    try:
        with ket_noi:
            with ket_noi.cursor() as con_tro:
                cho_hoi = ','.join(['%s'] * len(ma_ds))
                con_tro.execute(
                    f"SELECT code, download_url FROM product "
                    f"WHERE code IN ({cho_hoi}) AND status = 1",
                    tuple(ma_ds),
                )
                return {
                    hang['code']: (hang.get('download_url') or None)
                    for hang in con_tro.fetchall()
                }
    except Exception as loi:  # noqa: BLE001 - biên mạng/CSDL, cố tình bắt rộng
        logging.warning('Lỗi khi tra link tải sản phẩm: %s', loi)
        return {}


# =====================================================================
# ĐIỀN CHỖ TRỐNG
# =====================================================================

LayDanhSachSanPham = Callable[[], List[SanPham]]
LayLinkTai = Callable[[List[str]], Dict[str, Optional[str]]]


def dien(
    van_ban: str,
    ngon_ngu: str = 'vi',
    *,
    lay_danh_sach_san_pham: Optional[LayDanhSachSanPham] = None,
    lay_link_tai: Optional[LayLinkTai] = None,
    khong_gian: Optional[str] = None,
) -> KetQuaDien:
    """Điền mọi chỗ trống trong `van_ban` bằng dữ liệu sống.

    `lay_danh_sach_san_pham` / `lay_link_tai` là điểm TIÊM nguồn dữ liệu:
    mặc định dùng `san_pham.danh_sach_san_pham` và `_link_tai_song` (CSDL
    thật), nhưng tự kiểm ở cuối tệp tiêm hàm giả để chạy không cần CSDL.
    """
    lay_danh_sach_san_pham = lay_danh_sach_san_pham or san_pham.danh_sach_san_pham
    lay_link_tai = lay_link_tai or _link_tai_song

    if not van_ban:
        return KetQuaDien(van_ban='', du=True, thieu=[])

    # --- Gom tra cứu: quét trước để biết cần những mã nào, tra MỖI mã
    # (và bảng giá tổng) đúng MỘT lần cho toàn bộ lượt gọi dien() này. ---
    ma_can_san_pham: Set[str] = set()
    ma_can_link: Set[str] = set()
    can_bang_gia = False

    for khop in CHO_TRONG_RE.finditer(van_ban):
        ten_loai, tham_so = khop.group(1), khop.group(2)
        if not _hop_le(ten_loai, tham_so):
            continue
        ten_loai = _chuan_hoa(ten_loai)
        if ten_loai == 'bang_gia':
            can_bang_gia = True
        elif ten_loai in ('gia', 'ten'):
            ma_can_san_pham.add(_chuan_hoa(tham_so))
        elif ten_loai == 'link_tai':
            ma_can_link.add(_chuan_hoa(tham_so))

    ds_san_pham: List[SanPham] = []
    if can_bang_gia or ma_can_san_pham:
        try:
            ds_san_pham = lay_danh_sach_san_pham() or []
        except Exception as loi:  # noqa: BLE001 - nguồn tiêm có thể hỏng lúc tự kiểm
            logging.warning('Lỗi khi tra danh mục sản phẩm để điền chỗ trống: %s', loi)
            ds_san_pham = []
    # Tra được theo mã, slug hoặc bí danh ({gia:copytrade} = slug 'copytrade').
    tra_sp: Dict[str, SanPham] = {}
    for sp in ds_san_pham:
        if sp.slug:
            tra_sp.setdefault(_chuan_hoa(sp.slug), sp)
    for sp in ds_san_pham:
        tra_sp[sp.ma] = sp
    for bi_danh, slug in BI_DANH.items():
        if slug in tra_sp:
            tra_sp.setdefault(bi_danh, tra_sp[slug])

    tra_link: Dict[str, Optional[str]] = {}
    if ma_can_link:
        try:
            tra_link = lay_link_tai(sorted(ma_can_link)) or {}
        except Exception as loi:  # noqa: BLE001
            logging.warning('Lỗi khi tra link tải để điền chỗ trống: %s', loi)
            tra_link = {}

    # --- Thay thế thật sự. ---
    thieu: List[str] = []

    def _thay(khop: 're.Match[str]') -> str:
        raw = khop.group(0)
        ten_loai, tham_so = khop.group(1), khop.group(2)

        if not _hop_le(ten_loai, tham_so):
            thieu.append(raw)
            return _chu_trung_lap(ngon_ngu)
        ten_loai = _chuan_hoa(ten_loai)

        if ten_loai == 'zalo':
            return _lien_he(khong_gian).get('zalo') or ZALO
        if ten_loai == 'telegram':
            return _lien_he(khong_gian).get('telegram') or TELEGRAM
        if ten_loai == 'bang_gia':
            if not ds_san_pham:
                thieu.append(raw)
            return khuon_mau.cau_bang_gia_tat_ca(ds_san_pham, ngon_ngu)

        ma = _chuan_hoa(tham_so)

        if ten_loai == 'gia':
            sp = tra_sp.get(ma)
            if sp is None:
                thieu.append(raw)
                return _chu_trung_lap(ngon_ngu)
            if not sp.cac_goi:
                thieu.append(raw)   # giá trống — không an toàn để gửi khách
                return _chu_trung_lap(ngon_ngu)
            return _gia_gon(sp, ngon_ngu)

        if ten_loai == 'ten':
            sp = tra_sp.get(ma)
            ten_hien_thi = ''
            if sp is not None:
                ten_hien_thi = (sp.ten_vi or sp.ten) if ngon_ngu == 'vi' else (sp.ten or sp.ten_vi)
            if not ten_hien_thi:
                thieu.append(raw)
                return _chu_trung_lap(ngon_ngu)
            return ten_hien_thi

        if ten_loai == 'link_tai':
            link = tra_link.get(ma)
            if not link:
                thieu.append(raw)
                return _chu_khong_co_link(ngon_ngu)
            return link

        # Không tới đây được vì _hop_le() đã lọc hết CAC_LOAI_HOP_LE ở trên.
        thieu.append(raw)  # pragma: no cover - lưới an toàn
        return _chu_trung_lap(ngon_ngu)

    van_ban_ra = CHO_TRONG_RE.sub(_thay, van_ban)

    # --- Lưới an toàn cuối cùng: van_ban KHÔNG BAO GIỜ còn '{' hay '}',
    # kể cả với chỗ trống sai khuôn dạng (vd thiếu dấu đóng) mà regex ở
    # trên không khớp được nên không đi qua _thay(). ---
    if '{' in van_ban_ra or '}' in van_ban_ra:
        thieu.append('(còn dấu ngoặc { } chưa xử lý được trong văn bản)')
        van_ban_ra = van_ban_ra.replace('{', '').replace('}', '')

    return KetQuaDien(van_ban=van_ban_ra, du=(len(thieu) == 0), thieu=thieu)


# =====================================================================
# TỰ KIỂM — không cần CSDL, dùng nguồn dữ liệu giả tiêm vào
# =====================================================================
if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    # -------------------------------------------------------------
    # 1) kiem_cu_phap() — ít nhất 8 chuỗi, đủ 6 loại hợp lệ + 2 loại lỗi
    #    + một trường hợp chứng minh không bị bẫy chuỗi con.
    # -------------------------------------------------------------
    print('=== 1) kiem_cu_phap() ===')
    cac_truong_hop = [
        ('{gia:dcabot}', True),
        ('{bang_gia}', True),
        ('{link_tai:dcabot}', True),
        ('{ten:dcabot}', True),
        ('{zalo}', True),
        ('{telegram}', True),
        ('{gia_ca:dcabot}', False),          # tên chỗ trống không hợp lệ
        ('{gia}', False),                     # thiếu tham số bắt buộc
        ('Xem thêm {link_tai:broadcaster} nhé', False),  # 'broadcaster' != 'signalbroadcaster' -> không bị bẫy chuỗi con
        ('{TEN:DCABOT}', True),               # chuẩn hoá chữ thường, KHÔNG bỏ dấu
        ('Giá {gia:dcabot} và {ten:viptrend} và {zalo}', True),   # nhiều chỗ trống hợp lệ cùng câu
    ]
    for van_ban, ky_vong_hop_le in cac_truong_hop:
        loi = kiem_cu_phap(van_ban)
        thuc_te_hop_le = (len(loi) == 0)
        trang_thai = 'OK' if thuc_te_hop_le == ky_vong_hop_le else 'SAI!!'
        print(f"  [{trang_thai}] {van_ban!r:55} -> loi={loi}")
        assert thuc_te_hop_le == ky_vong_hop_le, f"kiem_cu_phap sai với {van_ban!r}"
    print('  KHANG DINH: kiem_cu_phap() phân loại đúng toàn bộ trường hợp trên — QUA.')
    print()

    # -------------------------------------------------------------
    # 2) dien() — nguồn dữ liệu GIẢ tiêm vào, đếm số lần gọi để chứng
    #    minh "gom tra cứu: mỗi sản phẩm chỉ tra một lần / mỗi dien()".
    # -------------------------------------------------------------
    print('=== 2) dien() với nguồn dữ liệu giả ===')

    from giao_uoc import GiaGoi  # noqa: E402 - import muộn có chủ ý, chỉ dùng trong tự kiểm

    sp_dcabot = SanPham(
        ma='dcabot', slug='dcabot', ten='DCA Bot', ten_vi='DCA Bot',
        backend='dcabot', phien_ban='2.1',
        cac_goi=[GiaGoi(thang=1, gia=9, nhan='1 tháng'), GiaGoi(thang=0, gia=49, nhan='Vĩnh viễn')],
    )
    sp_viptrend_gia_trong = SanPham(
        ma='viptrend', slug='viptrend', ten='VipTrend', ten_vi='VipTrend',
        backend='viptrend', phien_ban='1.0', cac_goi=[],   # cố ý: giá trống
    )
    _DANH_SACH_GIA = [sp_dcabot, sp_viptrend_gia_trong]

    dem_goi_danh_sach = {'so_lan': 0}

    def _fake_danh_sach_ok() -> List[SanPham]:
        dem_goi_danh_sach['so_lan'] += 1
        return _DANH_SACH_GIA

    dem_goi_link = {'so_lan': 0, 'ma_da_hoi': None}

    def _fake_link_ok(ma_ds: List[str]) -> Dict[str, Optional[str]]:
        dem_goi_link['so_lan'] += 1
        dem_goi_link['ma_da_hoi'] = list(ma_ds)
        return {'dcabot': 'https://tradingauto.org/tai/dcabot.zip'}   # viptrend cố ý KHÔNG có link

    def _fake_danh_sach_hong() -> List[SanPham]:
        raise RuntimeError('CSDL sập giả lập để tự kiểm')

    def _fake_link_hong(ma_ds: List[str]) -> Dict[str, Optional[str]]:
        raise RuntimeError('CSDL sập giả lập để tự kiểm')

    # --- (2a) MỌI chỗ trống điền được -> du=True ---
    van_ban_a = 'Giá {gia:dcabot} ạ, tên đầy đủ là {ten:dcabot}. Liên hệ {zalo} / {telegram}.'
    kq_a = dien(van_ban_a, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2a) du={kq_a.du} thieu={kq_a.thieu}")
    print(f"       van_ban=\n{kq_a.van_ban}\n")
    assert '{' not in kq_a.van_ban and '}' not in kq_a.van_ban
    assert kq_a.du is True
    assert kq_a.thieu == []
    print('  KHANG DINH (2a): mọi chỗ trống điền được -> du=True, thieu=[], không còn ký tự { — QUA.')
    print()

    # --- (2b) mã sản phẩm KHÔNG TỒN TẠI -> du=False, vẫn không còn '{' ---
    van_ban_b = 'Giá {gia:khongtontai} anh nhé.'
    kq_b = dien(van_ban_b, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2b) du={kq_b.du} thieu={kq_b.thieu}")
    print(f"       van_ban={kq_b.van_ban!r}\n")
    assert '{' not in kq_b.van_ban and '}' not in kq_b.van_ban
    assert kq_b.du is False
    assert kq_b.thieu != []
    print('  KHANG DINH (2b): mã sản phẩm không tồn tại -> du=False, không còn ký tự { — QUA.')
    print()

    # --- (2c) giá trống (sản phẩm có nhưng cac_goi=[]) -> du=False ---
    van_ban_c = 'Giá {gia:viptrend} anh nhé.'
    kq_c = dien(van_ban_c, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2c) du={kq_c.du} thieu={kq_c.thieu}")
    print(f"       van_ban={kq_c.van_ban!r}\n")
    assert '{' not in kq_c.van_ban
    assert kq_c.du is False
    print('  KHANG DINH (2c): giá trống -> du=False (dùng luôn câu trung tính có sẵn của khuon_mau) — QUA.')
    print()

    # --- (2d) link tải rỗng (viptrend không có download_url) -> du=False ---
    van_ban_d = 'Tải tại {link_tai:viptrend} nhé, hoặc {link_tai:dcabot}.'
    kq_d = dien(van_ban_d, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2d) du={kq_d.du} thieu={kq_d.thieu}")
    print(f"       van_ban={kq_d.van_ban!r}\n")
    assert '{' not in kq_d.van_ban
    assert kq_d.du is False   # vì link_tai:viptrend rỗng, dù link_tai:dcabot có
    print('  KHANG DINH (2d): thiếu link tải của MỘT sản phẩm vẫn kéo du=False cho cả câu — QUA.')
    print()

    # --- (2e) CSDL "hỏng" (nguồn tiêm ném lỗi) -> KHÔNG sập, du=False, không còn '{' ---
    van_ban_e = 'Giá {gia:dcabot}, tải {link_tai:dcabot}, xem {bang_gia}.'
    kq_e = dien(van_ban_e, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_hong, lay_link_tai=_fake_link_hong)
    print(f"  (2e) du={kq_e.du} thieu={kq_e.thieu}")
    print(f"       van_ban={kq_e.van_ban!r}\n")
    assert '{' not in kq_e.van_ban and '}' not in kq_e.van_ban
    assert kq_e.du is False
    print('  KHANG DINH (2e): nguồn tra cứu ném lỗi -> KHÔNG sập chương trình, du=False, không còn { — QUA.')
    print()

    # --- (2f) chỗ trống sai khuôn dạng lọt qua kiem_cu_phap kiểu khác:
    #     thiếu dấu đóng ngoặc -> lưới an toàn cuối cùng phải dọn sạch. ---
    van_ban_f = 'Giá con {gia:dcabot mà thiếu ngoặc đóng.'
    kq_f = dien(van_ban_f, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2f) du={kq_f.du} thieu={kq_f.thieu}")
    print(f"       van_ban={kq_f.van_ban!r}\n")
    assert '{' not in kq_f.van_ban and '}' not in kq_f.van_ban
    assert kq_f.du is False
    print('  KHANG DINH (2f): ngoặc lẻ (thiếu dấu đóng) -> lưới an toàn dọn sạch, du=False — QUA.')
    print()

    # --- (2g) gom tra cứu: 'dcabot' xuất hiện 2 lần trong {gia:...} và
    #     {ten:...} -> lay_danh_sach_san_pham() CHỈ được gọi 1 lần. ---
    dem_goi_danh_sach['so_lan'] = 0
    dem_goi_link['so_lan'] = 0
    van_ban_g = '{gia:dcabot} - {ten:dcabot} - {link_tai:dcabot} - {link_tai:dcabot} - {bang_gia}'
    kq_g = dien(van_ban_g, 'vi', lay_danh_sach_san_pham=_fake_danh_sach_ok, lay_link_tai=_fake_link_ok)
    print(f"  (2g) so_lan_goi_danh_sach={dem_goi_danh_sach['so_lan']} "
          f"so_lan_goi_link={dem_goi_link['so_lan']} ma_da_hoi_link={dem_goi_link['ma_da_hoi']}")
    assert dem_goi_danh_sach['so_lan'] == 1, 'phải gom, chỉ tra danh sách sản phẩm MỘT lần mỗi dien()'
    assert dem_goi_link['so_lan'] == 1, 'phải gom, chỉ tra link tải MỘT lần (một truy vấn) mỗi dien()'
    assert dem_goi_link['ma_da_hoi'] == ['dcabot'], 'phải khử trùng mã trước khi tra'
    print('  KHANG DINH (2g): nhiều chỗ trống cùng trỏ 1 sản phẩm -> mỗi nguồn chỉ tra ĐÚNG 1 lần — QUA.')
    print()

    print('=== TẤT CẢ KHẲNG ĐỊNH TỰ KIỂM ĐÃ QUA. ===')
