# -*- coding: utf-8 -*-
"""TANG_MOT.PY — TẦNG 1: khớp câu hỏi khách với mục tư vấn của CHỦ WEBSITE.

VÌ SAO FILE NÀY TỒN TẠI
------------------------
Bot có ba tầng trả lời. Tầng 1 (ở đây) là tầng TRÊN CÙNG: câu trả lời do
chủ website tự soạn và tự duyệt trong phần quản trị (bảng `chat_muc`).
Khớp được thì trả NGUYÊN VĂN, KHÔNG gọi mô hình AI — tốn 0 đồng, ~50 ms,
và không thể sai vì đó là lời của chủ. Khớp không được (hoặc khớp mà điền
hụt dữ liệu sống) thì lõi (`nao/tra_loi.py`) rơi xuống tầng 2 (tìm kiếm +
diễn giải) như cũ.

CÁCH KHỚP — đúng hai bước, không sáng tạo thêm
------------------------------------------------
Bước A, khớp CHÍNH XÁC sau chuẩn hoá (hạ chữ thường, gộp khoảng trắng, bỏ
dấu câu ở hai đầu — KHÔNG bỏ dấu tiếng Việt, xem bẫy 1 dưới đây), so với
`cau_hoi_chuan` và từng dòng của `cach_hoi_khac`. Trúng thì điểm 1.0.

Bước B, khớp theo ĐỘ PHỦ TỪ: tách từ bằng `tim.bm25.tach_tu()` (đã tự bỏ
hư từ trong `tim.bm25.HU_TU`, ở đây lọc lại thêm một lớp cho chắc). Với
mỗi câu mẫu (câu hỏi chuẩn + từng dòng cách hỏi khác) của mỗi mục:

    điểm = |giao của tập từ câu khách và tập từ câu mẫu| / |tập từ CÂU MẪU|

Lấy mục có điểm cao nhất trong toàn bộ câu mẫu của toàn bộ mục. Chỉ chấp
nhận khi điểm >= NGUONG (0.6) VÀ tập từ câu khách (sau khi bỏ hư từ) có
ÍT NHẤT 2 từ. Chia cho tập từ CÂU MẪU (không phải câu khách) vì: khách
viết dài dòng thêm lời chào/cảm ơn thì không được làm rớt điểm; nhưng câu
mẫu còn sót từ quan trọng nào không có trong câu khách thì phải rớt.

HAI BẪY ĐÃ CẮN DỰ ÁN NÀY — TRÁNH LẠI Ở ĐÂY
---------------------------------------------
1. KHÔNG bỏ dấu tiếng Việt khi chuẩn hoá bước A. 'bớt' bỏ dấu thành 'bot',
   đụng đúng chữ khách dùng để gọi sản phẩm (chatbot/trading bot). Chỉ hạ
   chữ thường + gộp khoảng trắng + bỏ dấu câu ở hai đầu.
2. Nếu phải so khớp mã sản phẩm ở đâu đó: khớp DÀI NHẤT TRƯỚC hoặc khớp
   CHÍNH XÁC cả chuỗi, không dùng `in` trên chuỗi ('dca' nằm trong
   'signalbroadcaster'). Ở tệp này không tự so khớp mã sản phẩm — việc đó
   đã nằm trong `congcu/cho_trong.py` — nhưng nhắc lại để không ai lỡ tay
   thêm logic đó vào đây theo kiểu sai.

Cách chạy tự kiểm (không cần CSDL, dùng danh sách mục GIẢ LẬP dựng sẵn):
    python nao/tang_mot.py
"""
from __future__ import annotations

import logging
import os
import re
import sys
import time
from typing import Any, Dict, List, Optional, Tuple

# ---------------------------------------------------------------------
# Nhập giao ước — chịu được cả hai cách chạy, y hệt nao/tra_loi.py.
# ---------------------------------------------------------------------
try:
    from giao_uoc import NguCanh, TraLoi
except ImportError:
    _THU_MUC_CHA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    if _THU_MUC_CHA not in sys.path:
        sys.path.insert(0, _THU_MUC_CHA)
    from giao_uoc import NguCanh, TraLoi

_log = logging.getLogger('chatbot.nao.tang_mot')


# =====================================================================
# PHỤ THUỘC TỪ CÁC MODULE ANH EM — bọc try/except như nao/tra_loi.py
# đang làm: thiếu mảnh nào thì mảnh đó biến thành None, module này vẫn
# nạp được, và lõi gọi vào sẽ tự rơi êm xuống tầng 2.
# =====================================================================

try:
    from kho.tu_kho_tuvan import doc_muc_duyet
except Exception as _loi_kho:  # noqa: BLE001
    doc_muc_duyet = None  # type: ignore
    _log.error('Chưa dùng được kho.tu_kho_tuvan.doc_muc_duyet() — tầng 1 sẽ luôn coi như không có mục nào. Lỗi: %s', _loi_kho)

try:
    from congcu.cho_trong import dien
except Exception as _loi_ct:  # noqa: BLE001
    dien = None  # type: ignore
    _log.error('Chưa dùng được congcu.cho_trong.dien() — tầng 1 sẽ luôn bỏ qua mọi mục (không điền được chỗ trống). Lỗi: %s', _loi_ct)

try:
    from congcu import csdl
except Exception as _loi_csdl:  # noqa: BLE001
    csdl = None  # type: ignore
    _log.error('Chưa dùng được congcu.csdl — sẽ không tăng được so_lan_dung. Lỗi: %s', _loi_csdl)

try:
    from tim.bm25 import tach_tu, HU_TU
except Exception as _loi_bm25:  # noqa: BLE001
    tach_tu = None  # type: ignore
    HU_TU = frozenset()  # type: ignore
    _log.error('Chưa dùng được tim.bm25.tach_tu()/HU_TU — khớp theo độ phủ từ (bước B) sẽ luôn thất bại. Lỗi: %s', _loi_bm25)


# =====================================================================
# HẰNG SỐ
# =====================================================================

NGUONG = 0.6                 # ngưỡng điểm tối thiểu cho khớp bước B
SO_TU_TOI_THIEU = 2          # câu khách phải có ít nhất chừng này từ (sau khi bỏ hư từ)
THOI_GIAN_CACHE_GIAY = 60    # đệm nạp mục — CHỦ Ý NGẮN, y hệt congcu/san_pham.py


# =====================================================================
# CHUẨN HOÁ CÂU — bước A. KHÔNG bỏ dấu tiếng Việt (xem bẫy 1 ở đầu tệp).
# =====================================================================

_RE_KHOANG_TRANG = re.compile(r'\s+')
# Dấu câu coi là "mép câu" để cắt ở hai đầu — không đụng nội dung ở giữa.
_KY_TU_MEP_CAU = ' \t\r\n.,!?;:…“”"\'‘’()[]{}<>«»-–—/\\'


def _chuan_hoa_cau(van_ban: str) -> str:
    """Hạ chữ thường + gộp khoảng trắng + bỏ dấu câu ở hai đầu. KHÔNG bỏ
    dấu tiếng Việt — bẫy 1 đã dặn ở đầu tệp."""
    if not van_ban:
        return ''
    sach = _RE_KHOANG_TRANG.sub(' ', van_ban.strip().lower())
    return sach.strip(_KY_TU_MEP_CAU)


def _tach_dong_khong_rong(van_ban: Optional[str]) -> List[str]:
    return [dong.strip() for dong in (van_ban or '').splitlines() if dong.strip()]


def _cac_cau_mau(muc: dict) -> List[str]:
    """Câu hỏi chuẩn + từng dòng của cách hỏi khác, đúng thứ tự trong mục."""
    cac_cau: List[str] = []
    cau_hoi_chuan = (muc.get('cau_hoi_chuan') or '').strip()
    if cau_hoi_chuan:
        cac_cau.append(cau_hoi_chuan)
    cac_cau.extend(_tach_dong_khong_rong(muc.get('cach_hoi_khac')))
    return cac_cau


def _tu_khoa(van_ban: str) -> List[str]:
    """tach_tu() rồi lọc lại HU_TU thêm một lớp cho chắc (tach_tu() vốn
    đã tự lọc HU_TU bên trong, nhưng lọc lại ở đây không tốn gì và không
    phụ thuộc ngầm vào việc tach_tu() có luôn giữ hành vi đó không)."""
    if tach_tu is None:
        return []
    try:
        tho = tach_tu(van_ban)
    except Exception as loi:  # noqa: BLE001 - tach_tu() không nên ném lỗi, nhưng phòng vẫn hơn
        _log.warning('Lỗi khi tách từ câu %r: %s', van_ban, loi)
        return []
    return [t for t in tho if t not in HU_TU]


def do_phu_cau_khach(cau_hoi: str, muc: dict) -> float:
    """Chiều NGƯỢC của điểm khớp: mục phủ được bao nhiêu phần CÂU KHÁCH.

    khop() chấm |khách ∩ mẫu| / |mẫu| — đo "câu khách có chứa câu mẫu
    không". Nó không thấy câu khách hỏi THÊM bao nhiêu thứ khác: đo thật
    23/09/2026, "dca bot giá sao, cài thế nào, chạy vps được không, có copy
    sang tài khoản khác được không" đạt điểm cao với mục "cài dcabot" và
    nhận nguyên văn bài cài đặt, bỏ rơi ba câu còn lại. Hàm này trả
    max(|khách ∩ mẫu| / |khách|) trên mọi câu mẫu của mục — thấp nghĩa là
    câu khách rộng hơn mục nhiều."""
    tu_khach = set(_tu_khoa(cau_hoi))
    if not tu_khach:
        return 0.0
    tot = 0.0
    for cau_mau in _cac_cau_mau(muc):
        tu_mau = set(_tu_khoa(cau_mau))
        if tu_mau:
            tot = max(tot, len(tu_khach & tu_mau) / len(tu_khach))
    return tot


# =====================================================================
# NẠP MỤC ĐÃ DUYỆT — đệm 60 GIÂY, CHỦ Ý NGẮN (y hệt congcu/san_pham.py:
# 0 giây thì mỗi câu hỏi của một cuộc hội thoại đều chạm CSDL, vô hạn thì
# lặp lại đúng lỗi đã giết bot cũ — chủ sửa mục trong quản trị thì chậm
# nhất một phút sau bot phải biết).
# =====================================================================

_dem: Dict[str, Any] = {'thoi_diem': 0.0, 'du_lieu': None, 'khong_gian': None}


def nap_muc(khong_gian: str = 'tradingauto', buoc_nap_lai: bool = False) -> List[dict]:
    """Mục tư vấn đã duyệt, đệm 60 giây theo `khong_gian`.

    `buoc_nap_lai=True` bỏ qua đệm, đọc thẳng CSDL — dùng cho tự kiểm/nội
    bộ, không cần dùng trong luồng trả lời khách bình thường.
    """
    if doc_muc_duyet is None:
        _log.error('Thiếu kho.tu_kho_tuvan.doc_muc_duyet() — trả danh sách mục rỗng.')
        return []

    bay_gio = time.time()
    cung_khong_gian = _dem['khong_gian'] == khong_gian
    con_han = (bay_gio - float(_dem['thoi_diem'])) < THOI_GIAN_CACHE_GIAY
    if not buoc_nap_lai and cung_khong_gian and _dem['du_lieu'] is not None and con_han:
        return _dem['du_lieu']  # type: ignore[return-value]

    try:
        ds = doc_muc_duyet(khong_gian)
    except Exception as loi:  # noqa: BLE001 - biên CSDL, cố tình bắt rộng
        _log.warning('Lỗi khi nạp mục tư vấn đã duyệt (không gian=%s): %s', khong_gian, loi)
        ds = []

    if ds:
        # Chỉ ghi đè đệm khi đọc THÀNH CÔNG — đọc lỗi thì không đụng vào
        # đệm cũ, để lần gọi sau còn cơ hội thử lại CSDL ngay (y hệt lý do
        # đã ghi trong congcu/san_pham.py).
        _dem['du_lieu'] = ds
        _dem['thoi_diem'] = bay_gio
        _dem['khong_gian'] = khong_gian
    return _dem['du_lieu'] or []  # type: ignore[return-value]


# =====================================================================
# KHỚP
# =====================================================================

def khop(
    cau_hoi: str,
    ngu_canh: NguCanh,
    *,
    cac_muc: Optional[List[dict]] = None,
) -> Optional[Tuple[dict, float]]:
    """Trả (mục, điểm 0..1) nếu khớp đủ chắc, ngược lại None.

    `cac_muc`: điểm TIÊM danh sách mục — mặc định gọi `nap_muc()` (CSDL
    thật qua đệm), nhưng tự kiểm tiêm thẳng danh sách giả để chạy không
    cần CSDL (cùng cách `congcu/cho_trong.dien()` đã làm).
    """
    if cac_muc is None:
        cac_muc = nap_muc(ngu_canh.khong_gian)
    if not cac_muc:
        return None

    cau_chuan = _chuan_hoa_cau(cau_hoi)
    if not cau_chuan:
        return None

    # ---- Bước A: khớp CHÍNH XÁC sau chuẩn hoá. ----
    for muc in cac_muc:
        for cau_mau in _cac_cau_mau(muc):
            if _chuan_hoa_cau(cau_mau) == cau_chuan:
                return muc, 1.0

    # ---- Bước B: khớp theo ĐỘ PHỦ TỪ. ----
    tu_khach = set(_tu_khoa(cau_hoi))
    if len(tu_khach) < SO_TU_TOI_THIEU:
        return None

    diem_cao_nhat = 0.0
    muc_tot_nhat: Optional[dict] = None
    for muc in cac_muc:
        for cau_mau in _cac_cau_mau(muc):
            tu_mau = set(_tu_khoa(cau_mau))
            if not tu_mau:
                continue
            diem = len(tu_khach & tu_mau) / len(tu_mau)
            if diem > diem_cao_nhat:
                diem_cao_nhat = diem
                muc_tot_nhat = muc

    if muc_tot_nhat is not None and diem_cao_nhat >= NGUONG:
        return muc_tot_nhat, diem_cao_nhat
    return None


# =====================================================================
# SOẠN TraLoi TỪ MỘT MỤC — LUẬT SỐNG CÒN: điền hụt thì trả None
# =====================================================================

def tra_loi_tu_muc(
    muc: dict,
    ngu_canh: NguCanh,
    diem_khop: float = 0.0,
) -> Optional[TraLoi]:
    """Ghép mục tư vấn thành TraLoi, hoặc None nếu không soạn được.

    `diem_khop`: điểm trả về từ `khop()`, dùng để gán `TraLoi.do_tin_cay`
    (giao_uoc.py: 0..1, càng lớn càng khớp) — hàm này không tự tính lại
    điểm, bên gọi (`khop()` đã tính) phải truyền vào.

    LUẬT SỐNG CÒN: nếu `congcu.cho_trong.dien()` báo KHÔNG điền đủ chỗ
    trống (`KetQuaDien.du is False`) thì trả None — bỏ mục này, để lõi rơi
    xuống tầng 2. Điền hụt nghĩa là dữ liệu sống không lấy được (giá
    trống, mã sản phẩm sai, CSDL sập...); gửi khách một câu thiếu giá còn
    tệ hơn một câu trả lời chung chung của tầng 2.
    """
    muc_id = muc.get('id')

    if dien is None:
        _log.warning('Thiếu congcu.cho_trong.dien() — bỏ mục tư vấn id=%s, rơi xuống tầng 2.', muc_id)
        return None

    ngon_ngu = ngu_canh.ngon_ngu
    tra_loi_en = (muc.get('tra_loi_en') or '').strip()
    if ngon_ngu == 'en' and tra_loi_en:
        van_ban_goc = tra_loi_en
    else:
        van_ban_goc = (muc.get('tra_loi_vi') or '').strip()

    if not van_ban_goc:
        _log.warning('Mục tư vấn id=%s không có nội dung trả lời (ngôn ngữ=%s) — bỏ.', muc_id, ngon_ngu)
        return None

    try:
        ket_qua = dien(van_ban_goc, ngon_ngu, khong_gian=getattr(ngu_canh, 'khong_gian', None))
    except Exception as loi:  # noqa: BLE001 - biên phụ thuộc (CSDL/khuon_mau), cố tình bắt rộng
        _log.warning('Lỗi khi điền chỗ trống cho mục tư vấn id=%s: %s', muc_id, loi)
        return None

    if not ket_qua.du:
        _log.warning(
            'Mục tư vấn id=%s điền chỗ trống KHÔNG đủ (thiếu %s) — bỏ mục, rơi xuống tầng 2.',
            muc_id, ket_qua.thieu,
        )
        return None

    duong_dan_nguon = (muc.get('duong_dan_nguon') or '').strip()
    if duong_dan_nguon:
        tieu_de = (muc.get('tieu_de_nguon') or '').strip() or (muc.get('cau_hoi_chuan') or '').strip()
        nguon: List[Dict[str, str]] = [{'tieu_de': tieu_de, 'duong_dan': duong_dan_nguon}]
    else:
        nguon = []

    return TraLoi(
        tra_loi=ket_qua.van_ban,
        nguon=nguon,
        y_dinh=muc.get('y_dinh') or 'khac',
        chuyen_nguoi=False,
        do_tin_cay=diem_khop,
        da_goi_llm=False,
        tang=1,
        muc_id=muc_id,
    )


# =====================================================================
# ĐẾM SỐ LẦN DÙNG — thống kê KHÔNG được làm hỏng câu trả lời
# =====================================================================

def tang_so_lan_dung(muc_id: int) -> None:
    """`UPDATE chat_muc SET so_lan_dung = so_lan_dung + 1 WHERE id=%s`.

    Lỗi thì nuốt, chỉ ghi log — đếm thống kê KHÔNG được làm hỏng câu trả
    lời đã gửi khách (bản thân hàm này luôn được gọi SAU KHI đã có TraLoi
    để trả về, xem nao/tra_loi.py)."""
    if csdl is None:
        _log.warning('Thiếu congcu.csdl — bỏ qua tăng so_lan_dung cho mục id=%s.', muc_id)
        return

    conn = None
    try:
        conn = csdl.ket_noi()
        if conn is None:
            return
        with conn:
            with conn.cursor() as con_tro:
                con_tro.execute(
                    'UPDATE chat_muc SET so_lan_dung = so_lan_dung + 1 WHERE id=%s',
                    (muc_id,),
                )
            conn.commit()
    except Exception as loi:  # noqa: BLE001 - biên CSDL, cố tình bắt rộng; đếm hỏng không được làm hỏng câu trả lời
        _log.warning('Lỗi khi tăng so_lan_dung cho mục tư vấn id=%s: %s', muc_id, loi)


# =====================================================================
# TỰ KIỂM — không cần CSDL, dùng danh sách mục GIẢ LẬP dựng sẵn trong mã
# =====================================================================
if __name__ == '__main__':
    logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

    MUC_GIA = {
        'id': 1,
        'khong_gian': 'tradingauto',
        'san_pham': 'dcabot',
        'y_dinh': 'gia',
        'cau_hoi_chuan': 'DCA Bot giá bao nhiêu?',
        'cach_hoi_khac': 'mua dca bot hết bao nhiêu tiền\ndcabot giá sao vậy ạ',
        'tra_loi_vi': 'Dạ {ten:dcabot} hiện có giá {gia:dcabot} ạ.',
        'tra_loi_en': '',
        'duong_dan_nguon': '/toolintroduce/dcabot',
        'tieu_de_nguon': 'Giới thiệu DCA Bot',
    }
    MUC_HUONG_DAN = {
        'id': 2,
        'khong_gian': 'tradingauto',
        'san_pham': 'dcabot',
        'y_dinh': 'huong_dan',
        'cau_hoi_chuan': 'Cài đặt DCA Bot vào MT5 như thế nào?',
        'cach_hoi_khac': 'hướng dẫn cài dca bot lên mt5\ncách cài đặt dcabot cho mt5',
        'tra_loi_vi': 'Anh/chị tải EA về rồi kéo vào thư mục Experts của MT5, sau đó kéo thả vào biểu đồ.',
        'tra_loi_en': '',
        'duong_dan_nguon': '/guilde/dcabot',
        'tieu_de_nguon': 'Hướng dẫn cài DCA Bot',
    }
    # Chỗ trống KHÔNG điền được: mã sản phẩm 'khongtontai' không nằm
    # trong SAN_PHAM -> congcu.cho_trong._hop_le() luôn False, nên
    # KetQuaDien.du luôn False BẤT KỂ máy này có CSDL hay không — tự kiểm
    # này chắc chắn không cần CSDL để đúng.
    MUC_CHO_TRONG_HONG = {
        'id': 3,
        'khong_gian': 'tradingauto',
        'san_pham': 'signalbroadcaster',
        'y_dinh': 'gia',
        'cau_hoi_chuan': 'Giá gói vĩnh viễn Signal Broadcaster bao nhiêu?',
        'cach_hoi_khac': '',
        'tra_loi_vi': 'Dạ giá là {gia:khongtontai} ạ.',
        'tra_loi_en': '',
        'duong_dan_nguon': '',
        'tieu_de_nguon': '',
    }

    DANH_SACH_GIA_LAP = [MUC_GIA, MUC_HUONG_DAN, MUC_CHO_TRONG_HONG]

    nc = NguCanh(phien='tu-kiem', ngon_ngu='vi', nguoi_dung='khach')

    print('=' * 78)
    print('1) TỰ KIỂM khop() — danh sách mục GIẢ LẬP, KHÔNG CSDL')
    print('=' * 78)

    CAC_CA: List[Tuple[str, str, Any]] = [
        # (mô tả, câu hỏi, kỳ vọng: 'khop_1.0' | 'khop_60' | None)
        ('trúng chính xác câu hỏi chuẩn', 'DCA Bot giá bao nhiêu?', 'khop_1.0'),
        ('trúng một dòng trong cách hỏi khác', 'dcabot giá sao vậy ạ', 'khop_1.0'),
        ('diễn đạt khác nhưng đủ từ khoá', 'cho em hỏi dca bot giá bao nhiêu tiền vậy ạ', 'khop_ngong'),
        ('hỏi chuyện HOÀN TOÀN KHÁC (đối chứng âm)', 'hôm nay Sài Gòn có mưa không nhỉ', None),
        ('chỉ còn 1 từ sau khi bỏ hư từ', 'giá', None),
    ]

    tat_ca_dat = True
    for mo_ta, cau, ky_vong in CAC_CA:
        kq = khop(cau, nc, cac_muc=DANH_SACH_GIA_LAP)
        if ky_vong is None:
            dat = kq is None
            hien_thi = 'None' if kq is None else f'muc_id={kq[0]["id"]} diem={kq[1]:.2f}'
        elif ky_vong == 'khop_1.0':
            dat = kq is not None and kq[1] == 1.0
            hien_thi = 'None' if kq is None else f'muc_id={kq[0]["id"]} diem={kq[1]:.2f}'
        else:  # 'khop_ngong' — khớp, điểm >= NGUONG (không nhất thiết 1.0)
            dat = kq is not None and kq[1] >= NGUONG
            hien_thi = 'None' if kq is None else f'muc_id={kq[0]["id"]} diem={kq[1]:.2f}'
        tat_ca_dat &= dat
        co = 'OK ' if dat else 'SAI'
        print(f"  [{co}] {mo_ta:42} cau={cau!r:55} -> {hien_thi}")
        assert dat, f'TRƯỢT ca: {mo_ta} ({cau!r})'

    print()
    print(f"=> {'TẤT CẢ ĐÚNG' if tat_ca_dat else 'CÓ CA SAI'}")

    print()
    print('=' * 78)
    print('2) TỰ KIỂM tra_loi_tu_muc() — mục có chỗ trống KHÔNG điền được phải trả None')
    print('=' * 78)

    # (2a) mục điền được hết (MUC_HUONG_DAN không có chỗ trống nào)
    tl_a = tra_loi_tu_muc(MUC_HUONG_DAN, nc, diem_khop=1.0)
    print(f"  (2a) mục KHÔNG có chỗ trống -> {'None (SAI!!)' if tl_a is None else 'có TraLoi (OK)'}")
    assert tl_a is not None, 'mục không có chỗ trống phải trả được TraLoi'
    assert tl_a.do_tin_cay == 1.0
    assert tl_a.nguon == [{'tieu_de': 'Hướng dẫn cài DCA Bot', 'duong_dan': '/guilde/dcabot'}]
    assert tl_a.muc_id == MUC_HUONG_DAN['id'], (
        f'LỖI: muc_id phải là {MUC_HUONG_DAN["id"]!r}, nhận được {tl_a.muc_id!r}'
    )
    print(f"       tra_loi={tl_a.tra_loi!r}")
    print(f"       nguon={tl_a.nguon}")
    print(f"       muc_id={tl_a.muc_id!r} (OK)")

    # (2b) mục có chỗ trống không điền được (mã sản phẩm không tồn tại)
    tl_b = tra_loi_tu_muc(MUC_CHO_TRONG_HONG, nc, diem_khop=0.8)
    print(f"  (2b) mục có chỗ trống hỏng -> {'None (OK)' if tl_b is None else 'có TraLoi (SAI!!)'}")
    assert tl_b is None, 'mục điền hụt chỗ trống PHẢI trả None (luật sống còn)'

    # (2c) mục KHÔNG có duong_dan_nguon -> nguon = []
    muc_khong_nguon = dict(MUC_HUONG_DAN)
    muc_khong_nguon['duong_dan_nguon'] = ''
    tl_c = tra_loi_tu_muc(muc_khong_nguon, nc, diem_khop=1.0)
    assert tl_c is not None and tl_c.nguon == [], 'thiếu duong_dan_nguon thì nguon phải là []'
    print(f"  (2c) mục không có duong_dan_nguon -> nguon={tl_c.nguon} (OK)")

    print()
    print('KHẲNG ĐỊNH: tất cả assert ở trên đều QUA (không có AssertionError nào bị ném).')

    print()
    print('=' * 78)
    print('3) TỰ KIỂM nap_muc() khi KHÔNG có CSDL — không được sập, trả []')
    print('=' * 78)
    ds_that = nap_muc('tradingauto', buoc_nap_lai=True)
    print(f"  nap_muc() -> {len(ds_that)} mục "
          f"(kỳ vọng 0 trên máy không có CSDL — chương trình KHÔNG bị sập).")

    print()
    print('=' * 78)
    print('4) TỰ KIỂM tang_so_lan_dung() khi KHÔNG có CSDL — không được sập')
    print('=' * 78)
    tang_so_lan_dung(1)
    print('  tang_so_lan_dung(1) chạy xong, không ném lỗi (đã tự nuốt lỗi CSDL nếu có).')

    print()
    print('TỰ KIỂM tang_mot.py HOÀN TẤT — TẤT CẢ ĐÚNG NHƯ KỲ VỌNG.')
