# -*- coding: utf-8 -*-
"""ĐỊNH TUYẾN Ý ĐỊNH — phân loại bằng LUẬT, chưa cần mô hình (giai đoạn 1).

VÌ SAO luật trước, mô hình sau: mọi nhánh có luật rõ ('cam_ket', 'tra_key',
'gia'...) đều KHÔNG được gọi mô hình (xem giao_uoc.py mục 2 và nao/tra_loi.py)
— giá, key, cam kết là những câu hỏi có câu trả lời XÁC ĐỊNH, không cần một
mô hình xác suất đoán hộ. Luật từ khoá rẻ, nhanh, và dự đoán được; mô hình
chỉ nên can thiệp khi luật thật sự không chắc (độ tin cậy thấp).

VÌ SAO ưu tiên 'cam_ket' đứng ĐẦU khi trùng nhiều nhóm: một câu vừa hỏi giá
vừa xin giảm giá ("giá bao nhiêu, giảm được không") mà bot lỡ trả lời phần
'gia' bằng khuôn mẫu rồi bỏ qua phần 'cam_ket' thì coi như bot vừa "im lặng
chấp nhận" khả năng giảm giá — một sự cố nghiêm trọng hơn nhiều so với việc
chuyển nhầm một khách chỉ đang hỏi giá đơn thuần sang người thật. Thà
chuyển người nhầm còn hơn để bot lỡ hứa.
"""
from __future__ import annotations

import os
import re
import sys
import unicodedata
from typing import List, Optional, Tuple

# Chịu được cả hai cách chạy: là package con `nao.dinh_tuyen` (app.py chạy
# từ thư mục services/chatbot làm cwd) HOẶC chạy trực tiếp tệp này
# (`python nao/dinh_tuyen.py`, hoặc `python dinh_tuyen.py` từ trong nao/).
# Ở nhánh sau, thư mục services/chatbot chưa chắc đã nằm trong sys.path.
try:
    from giao_uoc import NguCanh, Ydinh, SAN_PHAM
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, Ydinh, SAN_PHAM


# =====================================================================
# CHUẨN HOÁ VĂN BẢN — chịu được gõ thiếu dấu tiếng Việt
# =====================================================================

def chuan_hoa(chuoi: str) -> str:
    """Về chữ thường, bỏ dấu tiếng Việt. Xử lý riêng đ/Đ vì NFD không tách
    được ký tự này thành d + dấu phụ (giống hệt cách congcu/san_pham.py đã
    làm — lặp lại có chủ đích ở đây vì hai tệp là hai module độc lập, xem
    đầu file cau_hinh.py về nguyên tắc "song song, không phụ thuộc chéo")."""
    if not chuoi:
        return ''
    chuoi = chuoi.replace('đ', 'd').replace('Đ', 'D')
    chuoi = unicodedata.normalize('NFD', chuoi)
    chuoi = ''.join(kt for kt in chuoi if unicodedata.category(kt) != 'Mn')
    return chuoi.lower().strip()


def chi_thuong(chuoi: str) -> str:
    """Chỉ về chữ thường, GIỮ NGUYÊN dấu — dùng cho vài từ khoá ngắn mà bỏ
    dấu xong lại trùng một từ tiếng Anh/domain phổ biến khác (xem
    `_TU_KHOA_CHINH_XAC_THEO_YDINH` ngay dưới)."""
    return (chuoi or '').lower().strip()


def _co_khop(van_ban_chuan: str, tu_khoa_chuan: str) -> bool:
    """Khớp CÓ RANH GIỚI TỪ. VÌ SAO không dùng substring trần: nhiều từ
    khoá ngắn ('hi', 'gia', 'phi') là chuỗi con của rất nhiều từ khác
    ('history', 'gia hạn' lẫn vào 'nghia', 'phía'...) — khớp trần sẽ báo
    dương tính giả tràn lan. \\b của regex coi khoảng trắng/đầu-cuối chuỗi
    là ranh giới, đủ dùng vì `van_ban_chuan` đã được tách khoảng trắng rõ
    ràng bởi chuan_hoa()."""
    mau = re.escape(tu_khoa_chuan)
    if tu_khoa_chuan[:1].isalnum():
        mau = r'\b' + mau
    if tu_khoa_chuan[-1:].isalnum():
        mau = mau + r'\b'
    return re.search(mau, van_ban_chuan) is not None


# =====================================================================
# TỪ KHOÁ THEO Ý ĐỊNH — nguyên văn có dấu, chuan_hoa() lúc so khớp
# =====================================================================

_TU_KHOA_THEO_YDINH: Tuple[Tuple[Ydinh, Tuple[str, ...]], ...] = (
    # Thứ tự của TUPLE này CHÍNH LÀ thứ tự ưu tiên khi trùng nhiều nhóm —
    # xem đoạn giải thích ở đầu file vì sao 'cam_ket' đứng đầu.
    ('cam_ket', (
        # 'bớt' KHÔNG nằm ở đây — xem _TU_KHOA_CHINH_XAC_THEO_YDINH bên dưới:
        # bỏ dấu 'bớt' -> 'bot', trùng thẳng với từ "bot" (chatbot/trading
        # bot) xuất hiện trong gần như mọi câu hỏi huong_dan/loi của domain
        # này. Tolerant-match ở đây từng biến MỌI câu có chữ "bot" thành
        # cam_ket — một lỗi nghiêm trọng hơn nhiều so với việc "bớt" gõ
        # thiếu dấu thỉnh thoảng không khớp.
        'giảm giá', 'discount', 'khuyến mãi', 'hoàn tiền', 'refund',
        # 'bảo hành' đứng một mình, không chỉ 'bảo hành trọn đời': câu
        # "sản phẩm của tôi có bảo hành không" từng rơi vào 'khac' rồi bot
        # đi tìm tài liệu và tự diễn giải — mà hỏi bảo hành CHÍNH LÀ hỏi
        # cam kết. Thà chuyển người nhầm còn hơn để bot lỡ hứa.
        'bảo hành', 'cam kết', 'đổi trả', 'bảo đảm',
        # Khách xin viết thêm tính năng cũng là một lời hứa — bot không
        # được nhận. Đo thật: "can you add a feature for me" trước đây rơi
        # vào 'khac' rồi bot đi tìm tài liệu và trả lời như thể làm được.
        'thêm tính năng', 'viết thêm', 'làm thêm cho', 'custom feature',
        'add a feature', 'add feature', 'build me',
    )),
    ('tra_key', (
        'key của tôi', 'còn hạn', 'hết hạn', 'license của tôi', 'my key',
        # 'expire' không phủ được 'expiry' vì khớp có ranh giới từ.
        'expire', 'expiry', 'expiration', 'my license', 'license key của',
        # Thêm 23/09/2026. Sau khi TẮT tra key (xem nao/tra_loi.py ::
        # _xu_ly_tra_key), nhánh này không còn đọc dữ liệu nữa mà chỉ chỉ
        # khách sang /orders — nên bắt SÓT một cách hỏi giờ tốn kém hơn
        # trước: câu lọt xuống tầng RAG sẽ được mô hình diễn giải lan man
        # về key dựa trên tài liệu, thay vì chỉ đúng chỗ khách cần bấm.
        # Sáu cách hỏi dưới đây lấy từ bộ thu/thu_kin_key.py, nơi chúng
        # từng trượt thật.
        'xem key', 'cho tôi key', 'key đâu', 'mấy key', 'bao nhiêu key',
        'check key', 'kiểm tra key', 'tra key', 'key của em',
        'license của em', 'show key', 'check license',
    )),
    ('gia', (
        # 'thuê' ĐỨNG RIÊNG đã bỏ (23/09/2026): "nên thuê vps ở đâu để chạy
        # bot" bị xếp vào hỏi giá rồi nhận bảng giá. Khách hỏi giá thuê thì
        # gần như luôn nói kèm bot/key/license/tháng.
        'giá', 'bao nhiêu tiền', 'bao nhiêu credit', 'phí', 'mua',
        'thuê bot', 'thuê key', 'thuê license', 'thuê theo tháng', 'thuê tháng',
        'gia hạn', 'price', 'cost', 'how much',
    )),
    ('loi', (
        'lỗi', 'không chạy', 'không vào lệnh', 'báo đỏ', 'failed', 'error',
        'invalid', 'không kết nối',
    )),
    ('huong_dan', (
        'cài', 'cài đặt', 'hướng dẫn', 'làm sao', 'cách', 'setup', 'install',
        'how to', 'config',
    )),
    ('chao', (
        'chào', 'hello', 'hi', 'alo',
    )),
)

# Chuẩn hoá một lần lúc nạp module, không phải mỗi lần gọi phan_loai() —
# danh sách từ khoá cố định, chuẩn hoá lại mỗi câu hỏi là lãng phí vô ích.
_TU_KHOA_CHUAN: Tuple[Tuple[Ydinh, Tuple[str, ...]], ...] = tuple(
    (nhan, tuple(chuan_hoa(tu) for tu in ds_tu)) for nhan, ds_tu in _TU_KHOA_THEO_YDINH
)

# Từ khoá CHÍNH XÁC (giữ dấu, không tolerant) — dùng cho từ ngắn mà bỏ dấu
# xong trùng một từ khác hoàn toàn nghĩa. So khớp trên `chi_thuong()`
# (chỉ hạ chữ thường, KHÔNG bỏ dấu), không phải trên `chuan_hoa()`.
_TU_KHOA_CHINH_XAC_THEO_YDINH: Tuple[Tuple[Ydinh, Tuple[str, ...]], ...] = (
    ('cam_ket', ('bớt',)),
)
_TU_KHOA_CHINH_XAC_CHUAN: Tuple[Tuple[Ydinh, Tuple[str, ...]], ...] = tuple(
    (nhan, tuple(chi_thuong(tu) for tu in ds_tu)) for nhan, ds_tu in _TU_KHOA_CHINH_XAC_THEO_YDINH
)

# Độ tin cậy khi luật bắt trúng một nhóm cụ thể — khá cao vì đây là khớp
# từ khoá tường minh, không phải suy đoán.
_DO_TIN_CAY_KHOP_LUAT = 0.85
# Độ tin cậy khi KHÔNG khớp luật nào ('khac') — CỐ Ý < 0.5, để tham số
# nha_cung_cap (giai đoạn 2) biết đây là lúc nên hỏi mô hình thay vì tự
# quyết theo luật rỗng.
_DO_TIN_CAY_KHONG_KHOP = 0.3


# Hai tap cho LUAT SO HUU KEY (xem cho dung trong phan_loai). Tach ra
# thanh hang so de sua duoc ma khong phai doc lai ca ham.
_TU_KEY = ('key', 'license', 'licence', 'ban quyen')
_TU_SO_HUU = ('cua toi', 'cua em', 'cua minh', 'cua t', 'cua a', 'toi co',
              'em co', 'minh co', 'my ', 'i have')


def _co_khop_so_huu_key(van_ban_da_chuan_hoa: str) -> bool:
    """Cau vua nhac KEY vua co dai tu SO HUU -> khach hoi ve key cua chinh ho.

    Nhan van ban DA chuan hoa (bo dau, chu thuong) — nen o day viet khong dau.
    """
    vb = van_ban_da_chuan_hoa
    co_key = any(_co_khop(vb, t) for t in _TU_KEY)
    if not co_key:
        return False
    return any(t in vb for t in _TU_SO_HUU)



def _ho_so(ngu_canh):
    """Hồ sơ shop của lượt này (shop/). Hỏng thì shop mặc định — không bao giờ ngã."""
    try:
        import shop
        return shop.lay(getattr(ngu_canh, 'khong_gian', None))
    except Exception:  # noqa: BLE001
        import shop
        return shop.lay(None)

_MAU_PRICE_ACTION = re.compile(r'\b(price action|hanh dong gia)\b')
_MAU_HOI_GIA_RO = re.compile(
    r'\b(bao nhieu tien|gia bao nhieu|gia sao|gia the nao|gia nhu nao|chi phi|phi code|phi bao nhieu|'
    r'bao gia|het bao nhieu|mat bao nhieu|ton bao nhieu|price|cost|how much|gia\b.{0,40}\bbao nhieu|'
    r'thue\b.{0,20}\bgia)\b')


def _la_ta_chien_luoc(van_ban: str) -> bool:
    """Câu (đã chuẩn hoá) là một đoạn TẢ LOGIC giao dịch: nhiều thuật ngữ kỹ thuật."""
    try:
        from congcu.so_tay import so_tu_ky_thuat
    except Exception:  # noqa: BLE001
        return False
    so = so_tu_ky_thuat(van_ban)
    return so >= 3 or (so >= 2 and len(van_ban.split()) >= 15)


def phan_loai(cau_hoi: str, ngu_canh: NguCanh, nha_cung_cap=None) -> Tuple[Ydinh, float]:
    """Xếp câu hỏi vào một Ydinh, kèm độ tin cậy 0..1.

    `nha_cung_cap`: để sẵn cho GIAI ĐOẠN 2 — nếu được truyền vào (không
    None) và luật cho độ tin cậy thấp (<0.5, tức rơi vào 'khac'), hàm này
    mới gọi `nha_cung_cap.phan_loai(cau_hoi, ngu_canh)` để hỏi mô hình.
    GIAI ĐOẠN 1 (bây giờ) luôn gọi hàm này với nha_cung_cap=None, nên
    nhánh gọi mô hình không bao giờ chạy tới — để sẵn khung chứ chưa bật.
    """
    # "price action" / "hành động giá" là thuật ngữ giao dịch, không phải hỏi giá.
    van_ban = _MAU_PRICE_ACTION.sub('priceaction', chuan_hoa(cau_hoi or ''))
    van_ban_giu_dau = re.sub(r'\b(price action|hành động giá)\b', 'priceaction', chi_thuong(cau_hoi or ''))
    _tu_khoa_chinh_xac_theo_nhan = dict(_TU_KHOA_CHINH_XAC_CHUAN)

    # LUẬT SỞ HỮU — chạy TRƯỚC bảng từ khoá.
    #
    # "key dcabot của tôi là gì" không khớp cụm 'key của tôi' vì tên sản
    # phẩm chen vào giữa. Thêm từng cách hỏi vào bảng là đuổi bắt không có
    # hồi kết ("key copytrade của em", "license tradingview của mình"...).
    #
    # Luật tổng quát: nhắc tới KEY/LICENSE *và* một đại từ SỞ HỮU thì đó là
    # khách đang hỏi về key CỦA CHÍNH HỌ, bất kể chen gì vào giữa.
    #
    # Bắt sót ở đây tốn kém hơn từ khi TẮT tra key (23/09/2026): câu lọt
    # xuống tầng RAG sẽ được mô hình diễn giải lan man về key dựa trên tài
    # liệu, thay vì chỉ đúng chỗ khách cần bấm (/orders). Xem thu/thu_kin_key.py.
    # Khung nền của shop (shop/): luật nhận diện riêng của NGÀNH chỉ chạy đúng ngành.
    hs = _ho_so(ngu_canh)
    la_trading = hs.nganh == 'trading'

    if van_ban and hs.co_tra_key and _co_khop_so_huu_key(van_ban):
        return 'tra_key', _DO_TIN_CAY_KHOP_LUAT

    # KHÁCH ĐANG TẢ CHIẾN LƯỢC — không phải hỏi giá / mua. Đo thật 24/09/2026
    # trên 10 hội thoại Fanpage: 7 lần khách tả logic ("vào lệnh MUA khi GIÁ
    # đóng cửa trên EMA200", "lãi 5usd thì chốt") bị xếp 'gia' vì chữ "giá",
    # "mua", "phí" nằm trong mô tả — và nhận nguyên bảng giá 7 sản phẩm.
    # Tả logic mà KHÔNG kèm câu hỏi giá rõ ràng -> 'khac' (tư vấn như chuyên gia).
    if la_trading and van_ban and _la_ta_chien_luoc(van_ban) and not _MAU_HOI_GIA_RO.search(van_ban):
        return 'khac', _DO_TIN_CAY_KHOP_LUAT
    # Đang bàn ĐẶT CODE, khách bổ sung một chi tiết logic ("dời SL dương khi GIÁ
    # chạy 10 giá") -> vẫn là tả chiến lược, không phải hỏi giá.
    try:
        from congcu.so_tay import so_tu_ky_thuat
        dang_code = (getattr(ngu_canh, 'so_tay', None) or {}).get('chu_de') == 'code'
        sp = doan_san_pham(cau_hoi or '')
        if (la_trading and van_ban and dang_code and so_tu_ky_thuat(van_ban) >= 1
                and not _MAU_HOI_GIA_RO.search(van_ban) and sp in (None, 'codebot')):
            return 'khac', _DO_TIN_CAY_KHOP_LUAT
    except Exception:  # noqa: BLE001
        pass

    if van_ban:
        for nhan, ds_tu_khoa in _TU_KHOA_CHUAN:
            if nhan == 'tra_key' and not hs.co_tra_key:
                continue                     # shop không có key/license: câu nhắc "key" là chuyện khác
            for tu_khoa in ds_tu_khoa:
                if _co_khop(van_ban, tu_khoa):
                    return nhan, _DO_TIN_CAY_KHOP_LUAT
            # Từ khoá chính xác (giữ dấu) của CÙNG nhãn này — kiểm ngay
            # trong lượt của nhãn đó để không phá thứ tự ưu tiên.
            for tu_khoa_dau in _tu_khoa_chinh_xac_theo_nhan.get(nhan, ()):
                if _co_khop(van_ban_giu_dau, tu_khoa_dau):
                    return nhan, _DO_TIN_CAY_KHOP_LUAT

    # Không khớp luật nào -> 'khac', độ tin cậy thấp.
    if nha_cung_cap is not None:
        try:
            nhan_mo_hinh = nha_cung_cap.phan_loai(cau_hoi, ngu_canh)
            return nhan_mo_hinh, 0.6  # mô hình quyết thì tin cậy vừa phải, không tuyệt đối
        except Exception:  # noqa: BLE001 - mô hình hỏng thì rơi về luật, không được ném lỗi
            pass

    return 'khac', _DO_TIN_CAY_KHONG_KHOP


# =====================================================================
# ĐOÁN SẢN PHẨM — khớp DÀI TRƯỚC NGẮN, có bí danh dân dã
# =====================================================================

# Bí danh khách hay gõ, ánh xạ về đúng mã trong SAN_PHAM (giao_uoc.py).
# CẢNH BÁO ĐÃ DẶN Ở giao_uoc.py: 'dca' (bí danh của 'dcabot') là chuỗi
# con của 'broa' + 'dca' + 'ster'. Nếu khớp bí danh ngắn 'dca' TRƯỚC khi
# xét 'signalbroadcaster' (hoặc bí danh dài của nó như 'signal broadcaster',
# 'broadcaster'), câu hỏi về Signal Broadcaster sẽ bị gán nhầm sang dcabot.
# Cách phòng: KHÔNG lặp theo thứ tự khai báo trong SAN_PHAM, mà gộp hết
# alias lại rồi SẮP XẾP GIẢM DẦN THEO ĐỘ DÀI trước khi so khớp — bí danh
# dài luôn được xét trước bí danh ngắn, bất kể sản phẩm nào khai báo trước.
_ALIAS_SAN_PHAM = {
    'dcabot': ('dcabot', 'dca', 'smart dca', 'smartdca'),
    'copytrade': ('copytrade', 'copy trade'),
    'tradingview': ('tradingview', 'trading view', 'tv bot'),
    'signaltrader': ('signaltrader', 'signal trader', 'signal copier'),
    'mt5manager': ('mt5manager', 'mt5 manager', 'manager account', 'quan ly tai khoan'),
    'signalbroadcaster': ('signalbroadcaster', 'signal broadcaster', 'broadcaster'),
    'tradepanel': ('tradepanel', 'trade panel', 'quang trade panel', 'panel'),
    # GỘP 23/09/2026: 'trendbot' từng là một khoá riêng, nhưng nó với
    # 'viptrend' là CÙNG MỘT sản phẩm — chỉ khác cách gọi tên trong kho mã
    # (tệp `Introtrendbot.php` vs tám trang `goldvip1..7`). Tách đôi thì
    # khách hỏi "bot vip" chỉ với được một nửa số đoạn tri thức.
    # Mọi bí danh của cả hai nay gom về đây.
    'viptrend': ('viptrend', 'vip trend', 'trendbot', 'trend bot',
                 'bot vip', 'goldvip', 'gold vip'),
    'codebot': ('codebot', 'code bot', 'viet bot', 'code thue',
                'bot theo yeu cau'),
}

# Kiểm tra tại thời điểm nạp module: mọi khoá alias phải là một sản phẩm
# THẬT trong giao_uoc.SAN_PHAM — lệch nhau ở đây là lỗi cấu hình, nên báo
# ngay lúc import thay vì âm thầm không bao giờ khớp được sản phẩm đó.
assert set(_ALIAS_SAN_PHAM) == set(SAN_PHAM), (
    f"_ALIAS_SAN_PHAM lệch với giao_uoc.SAN_PHAM: "
    f"{set(_ALIAS_SAN_PHAM) ^ set(SAN_PHAM)}"
)

# Danh sách phẳng (alias_chuan_hoa, san_pham), SẮP THEO ĐỘ DÀI GIẢM DẦN —
# dựng một lần lúc nạp module.
_DS_ALIAS_DAI_TRUOC: Tuple[Tuple[str, str], ...] = tuple(
    sorted(
        (
            (chuan_hoa(alias), san_pham)
            for san_pham, ds_alias in _ALIAS_SAN_PHAM.items()
            for alias in ds_alias
        ),
        key=lambda cap: -len(cap[0]),
    )
)


def doan_san_pham(cau_hoi: str) -> Optional[str]:
    """Đoán khách đang nói tới sản phẩm nào trong SAN_PHAM, hoặc None nếu
    không đoán được — KHÔNG được đoán bừa (giống tim_san_pham() trong
    congcu/san_pham.py: None thì tầng gọi tự chuyển sang "hỏi lại/bảng giá
    chung", không tự bịa một sản phẩm)."""
    van_ban = chuan_hoa(cau_hoi or '')
    if not van_ban:
        return None
    for alias_chuan, san_pham in _DS_ALIAS_DAI_TRUOC:
        if _co_khop(van_ban, alias_chuan):
            return san_pham
    return None


if __name__ == '__main__':
    # =================================================================
    # TỰ KIỂM — chạy: python nao/dinh_tuyen.py  (hoặc từ trong nao/:
    # python dinh_tuyen.py)
    # =================================================================
    CAC_CAU_MAU: Tuple[Tuple[str, Optional[Ydinh]], ...] = (
        ('cho em xin giảm giá', 'cam_ket'),
        ('giá bao nhiêu', 'gia'),
        ('key của tôi còn hạn không', 'tra_key'),
        ('cài EA vào MT5 sao', 'huong_dan'),
        ('chao ban', 'chao'),
        ('hello, how much does dcabot cost', 'gia'),
        ('my key expire when', 'tra_key'),
        ('bot bao loi khong vao lenh', 'loi'),
        ('how to install signalbroadcaster', 'huong_dan'),
        ('co giam gia khong ban', 'cam_ket'),
        ('gia dca bot bao nhieu', 'gia'),
        ('hom nay troi dep qua', 'khac'),
    )

    print('=' * 70)
    print(f'THỬ {len(CAC_CAU_MAU)} CÂU MẪU — phan_loai()')
    print('=' * 70)
    nc = NguCanh(phien='tu-kiem')
    so_dung = 0
    for cau, ky_vong in CAC_CAU_MAU:
        y_dinh, do_tin_cay = phan_loai(cau, nc)
        dung = (y_dinh == ky_vong)
        so_dung += dung
        co = 'OK ' if dung else 'SAI'
        print(f"[{co}] {cau!r:55} -> {y_dinh:10} (tin_cay={do_tin_cay:.2f}, ky_vong={ky_vong})")
    print(f"\n=> {so_dung}/{len(CAC_CAU_MAU)} đúng như kỳ vọng.")
    assert so_dung == len(CAC_CAU_MAU), "CÓ CÂU SAI KỲ VỌNG — xem bảng trên."

    print('\n' + '=' * 70)
    print('THỬ doan_san_pham() — trọng tâm: signalbroadcaster không bị "dca" nuốt')
    print('=' * 70)
    CAC_CAU_SAN_PHAM: Tuple[Tuple[str, Optional[str]], ...] = (
        ('cài dcabot vào MT5 sao', 'dcabot'),
        ('signalbroadcaster báo lỗi gì vậy', 'signalbroadcaster'),
        ('signal broadcaster dùng sao', 'signalbroadcaster'),
        ('smart dca giá bao nhiêu', 'dcabot'),
        ('copytrade với copy trade khác nhau không', 'copytrade'),
        ('tôi hỏi chung chung thôi', None),
    )
    so_dung2 = 0
    for cau, ky_vong in CAC_CAU_SAN_PHAM:
        sp = doan_san_pham(cau)
        dung = (sp == ky_vong)
        so_dung2 += dung
        co = 'OK ' if dung else 'SAI'
        print(f"[{co}] {cau!r:45} -> {sp!r:20} (ky_vong={ky_vong!r})")
    print(f"\n=> {so_dung2}/{len(CAC_CAU_SAN_PHAM)} đúng như kỳ vọng.")
    assert so_dung2 == len(CAC_CAU_SAN_PHAM), "CÓ CÂU SAI KỲ VỌNG — xem bảng trên."

    print('\nTỰ KIỂM dinh_tuyen.py HOÀN TẤT — TẤT CẢ ĐÚNG NHƯ KỲ VỌNG.')
