# -*- coding: utf-8 -*-
"""BAI_DANG.PY — bài đăng Fanpage, chủ shop DUYỆT qua Telegram rồi mới đăng.

VÌ SAO (25/09/2026)
-------------------
Chủ dự án: "soạn 1 bài để quảng cáo cho mt5 manager sau đó gửi qua tele cho tôi
duyệt. tốt nhất là cấu trúc để sau này thành skill để tôi có thể gọi khi muốn
đăng bài". Phần SOẠN nằm ở skill `.claude/skills/dang-bai` (Claude đọc trang sản
phẩm, bảng giá, rồi viết). Tệp này lo phần CHẠY: lưu nháp, gửi thẻ, đăng.

LUỒNG
-----
1. `gui_duyet()` (hoặc CLI `python kenh/bai_dang.py gui bai.json`): lưu
   `chat_bai_dang` trạng thái 'cho_duyet', gửi ẢNH (nếu có) rồi THẺ Telegram:
   toàn văn bài + nút ✅ Đăng lên Fanpage / ❌ Bỏ.
2. Muốn sửa: TRẢ LỜI (reply) vào thẻ bằng nội dung mới -> tele-duyet.php gọi
   /bai/sua -> `sua()` thay nội dung, vẽ lại thẻ.
3. Bấm nút -> tele-duyet.php (kiểm chuỗi bí mật webhook, ID được phép, chữ ký
   HMAC, đúng cuộc trò chuyện — y như nút duyệt nạp) -> /bai/hanh-dong ->
   `hanh_dong()`: 'bd' đăng (ảnh: /{page}/photos, chữ: /{page}/feed), 'bb' bỏ.
   KHÔNG có đường nào đăng bài mà không qua nút của chủ shop.

Token Trang lấy từ `chat_kenh` của đúng shop (cần pages_manage_posts — lấy qua
nút Kết nối Fanpage ở trang Kênh chat).

Chạy tự kiểm (không cần CSDL, không gọi mạng):  python kenh/bai_dang.py
"""
from __future__ import annotations

import html
import json
import logging
import os
import sys
from typing import Any, Callable, Dict, List, Optional, Tuple

try:
    from congcu import csdl
    from kenh.phieu import _cau_hinh_tele, _bi_mat_ky, ky, _tele
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 congcu import csdl  # type: ignore
    from kenh.phieu import _cau_hinh_tele, _bi_mat_ky, ky, _tele  # type: ignore

_log = logging.getLogger('chatbot.kenh.bai_dang')

GRAPH = 'https://graph.facebook.com/v24.0'
TOI_DA_THE = 3600            # Telegram giới hạn 4096 ký tự mỗi tin; chừa chỗ đầu/cuối thẻ
TT = {'cho_duyet': '🟡 Chờ duyệt', 'da_dang': '🟢 Đã đăng', 'bo': '⚪ Đã bỏ', 'loi': '🔴 Đăng lỗi'}


# ---------------------------------------------------------------------
# THẺ TELEGRAM
# ---------------------------------------------------------------------

def the_bai(b: Dict[str, Any], ten_trang: str) -> str:
    h = html.escape
    nd = str(b.get('noi_dung') or '')
    if len(nd) > TOI_DA_THE:
        nd = nd[:TOI_DA_THE] + '…(cắt bớt khi hiển thị; bài đăng giữ nguyên)'
    dong = [f'📝 <b>BÀI #{int(b["id"])}</b> · {h(str(b.get("chu_de") or ""))}',
            f'Trang: {h(ten_trang)}' + (' · kèm ảnh (tin ảnh "Ảnh kèm bài #' + str(int(b['id'])) + '" mới nhất)'
                                         if b.get('anh_url') else ' · bài chữ'),
            '━━━━━━━━━━━━', h(nd), '━━━━━━━━━━━━']
    tt = TT.get(b.get('trang_thai') or 'cho_duyet', '')
    if b.get('nguoi_duyet') and b.get('trang_thai') != 'cho_duyet':
        tt += f' — {h(str(b["nguoi_duyet"]))}'
    if b.get('fb_link'):
        tt += f'\n<a href="{h(str(b["fb_link"]))}">Xem bài trên Fanpage</a>'
    if b.get('trang_thai') == 'loi' and b.get('loi'):
        tt += f'\n{h(str(b["loi"]))}'
    dong.append(f'<b>{tt}</b>')
    if b.get('trang_thai') in ('cho_duyet', 'loi'):
        dong.append('<i>Muốn sửa: TRẢ LỜI tin này bằng toàn bộ nội dung mới.</i>')
    return '\n'.join(dong)


def nut_bai(b: Dict[str, Any], bi_mat: str) -> List[List[Dict[str, str]]]:
    if b.get('trang_thai') not in ('cho_duyet', 'loi'):
        return []
    i = int(b['id'])
    n = lambda chu, hanh: {'text': chu, 'callback_data': f'{hanh}:{i}:{ky(bi_mat, hanh, i)}'}  # noqa: E731
    return [[n('✅ Đăng lên Fanpage' if b.get('trang_thai') == 'cho_duyet' else '🔁 Đăng lại', 'bd'), n('❌ Bỏ', 'bb')]]


# ---------------------------------------------------------------------
# CSDL
# ---------------------------------------------------------------------

def _lay_bai(ct, id_: int) -> Optional[Dict[str, Any]]:
    ct.execute('SELECT * FROM chat_bai_dang WHERE id = %s', (int(id_),))
    return ct.fetchone()


def _lay_trang(ct, khong_gian: str) -> Optional[Dict[str, Any]]:
    ct.execute("""SELECT page_id, page_token, ten FROM chat_kenh
                  WHERE khong_gian = %s AND kenh = 'messenger' AND page_token <> ''
                  ORDER BY bat DESC, id LIMIT 1""", (khong_gian,))
    return ct.fetchone()


def _ve_lai(ct, b: Dict[str, Any], *, _tele_goi=None) -> None:
    token, chat = _cau_hinh_tele(ct)
    if not token or not chat:
        return
    goi = _tele_goi or (lambda pt, dl: _tele(token, pt, dl))
    trang = _lay_trang(ct, b.get('khong_gian') or 'tradingauto') or {}
    du_lieu: Dict[str, Any] = {
        'chat_id': chat, 'parse_mode': 'HTML', 'disable_web_page_preview': True,
        'text': the_bai(b, str(trang.get('ten') or trang.get('page_id') or '(chưa gắn Fanpage)')),
        'reply_markup': {'inline_keyboard': nut_bai(b, _bi_mat_ky(ct))},
    }
    if b.get('tele_msg_id'):
        goi('editMessageText', dict(du_lieu, message_id=int(b['tele_msg_id'])))
        return
    if b.get('anh_url'):
        goi('sendPhoto', {'chat_id': chat, 'photo': b['anh_url'], 'caption': f'Ảnh kèm bài #{int(b["id"])}'})
    j = goi('sendMessage', du_lieu) or {}
    mid = (j.get('result') or {}).get('message_id') if isinstance(j, dict) else None
    if mid:
        ct.execute('UPDATE chat_bai_dang SET tele_msg_id = %s WHERE id = %s', (int(mid), int(b['id'])))
        b['tele_msg_id'] = int(mid)


def gui_duyet(noi_dung: str, *, chu_de: str = '', anh_url: str = '', khong_gian: str = 'tradingauto', nguon: str = '',
              _lay_ket_noi: Optional[Callable[[], Any]] = None, _tele_goi=None) -> int:
    """Lưu bài nháp và gửi thẻ duyệt. Trả id bài. `nguon`: khoá nguồn bài (vd 'sp:dcabot', 'kho:38') để
    kenh/bai_ngay.py không lặp chủ đề; bài soạn tay để trống."""
    noi_dung = (noi_dung or '').strip()
    if not noi_dung:
        raise ValueError('Bài trống.')
    if anh_url and not anh_url.startswith('https://'):
        raise ValueError('Ảnh phải là URL https công khai (Facebook tự tải về).')
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        raise RuntimeError('Không kết nối được CSDL.')
    with conn:
        with conn.cursor() as ct:
            ct.execute("""INSERT INTO chat_bai_dang (khong_gian, chu_de, noi_dung, anh_url, nguon, trang_thai, tao_luc, cap_nhat)
                          VALUES (%s, %s, %s, %s, %s, 'cho_duyet', NOW(), NOW())""",
                       (khong_gian, chu_de[:190], noi_dung, anh_url[:500], (nguon or '')[:64] or None))
            id_ = int(ct.lastrowid)
            _ve_lai(ct, _lay_bai(ct, id_), _tele_goi=_tele_goi)
        conn.commit()
    return id_


def sua(tele_msg_id: int, noi_dung: str, nguoi: str, *,
        _lay_ket_noi: Optional[Callable[[], Any]] = None, _tele_goi=None) -> Tuple[bool, str]:
    """Chủ shop trả lời vào thẻ bằng nội dung mới."""
    noi_dung = (noi_dung or '').strip()
    if len(noi_dung) < 20:
        return False, 'Nội dung mới quá ngắn — trả lời bằng TOÀN BỘ bài.'
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return False, 'Không kết nối được CSDL.'
    with conn:
        with conn.cursor() as ct:
            ct.execute('SELECT * FROM chat_bai_dang WHERE tele_msg_id = %s', (int(tele_msg_id),))
            b = ct.fetchone()
            if not b:
                return False, 'Không phải thẻ bài đăng.'
            if b['trang_thai'] not in ('cho_duyet', 'loi'):
                return False, f'Bài #{b["id"]} đã {TT.get(b["trang_thai"], b["trang_thai"])}, không sửa được.'
            ct.execute("""UPDATE chat_bai_dang SET noi_dung = %s, so_lan_sua = so_lan_sua + 1, cap_nhat = NOW()
                          WHERE id = %s""", (noi_dung, int(b['id'])))
            b = _lay_bai(ct, int(b['id']))
            _ve_lai(ct, b, _tele_goi=_tele_goi)
        conn.commit()
    return True, f'Đã sửa bài #{b["id"]} ({nguoi}).'


def doi_anh(id_: int, anh_url: str, *, _lay_ket_noi: Optional[Callable[[], Any]] = None,
            _tele_goi=None) -> Tuple[bool, str]:
    """Đổi / thêm / bỏ ảnh của bài đang chờ duyệt (anh_url rỗng = thành bài chữ).
    Ảnh mới gửi thành tin trả lời vào thẻ để chủ shop xem trước khi bấm Đăng."""
    anh_url = (anh_url or '').strip()
    if anh_url and not anh_url.startswith('https://'):
        return False, 'Ảnh phải là URL https công khai.'
    conn = (_lay_ket_noi or csdl.ket_noi)()
    if conn is None:
        return False, 'Không kết nối được CSDL.'
    with conn:
        with conn.cursor() as ct:
            b = _lay_bai(ct, id_)
            if not b or b['trang_thai'] not in ('cho_duyet', 'loi'):
                return False, f'Bài #{id_} không còn chờ duyệt.'
            ct.execute('UPDATE chat_bai_dang SET anh_url = %s, cap_nhat = NOW() WHERE id = %s', (anh_url[:500], id_))
            b = _lay_bai(ct, id_)
            token, chat = _cau_hinh_tele(ct)
            if anh_url and token and chat:
                goi = _tele_goi or (lambda pt, dl: _tele(token, pt, dl))
                du = {'chat_id': chat, 'photo': anh_url, 'caption': f'Ảnh kèm bài #{int(id_)} (ảnh mới, thay ảnh trước)'}
                if b.get('tele_msg_id'):
                    du['reply_to_message_id'] = int(b['tele_msg_id'])
                goi('sendPhoto', du)
            _ve_lai(ct, b, _tele_goi=_tele_goi)
        conn.commit()
    return True, f'Đã {"đổi ảnh" if anh_url else "bỏ ảnh"} bài #{id_}.'


# ---------------------------------------------------------------------
# ĐĂNG
# ---------------------------------------------------------------------

def _dang_fb(trang: Dict[str, Any], b: Dict[str, Any], _post=None, _get=None) -> Tuple[bool, str, str, str]:
    """(ok, post_id, link, lỗi)."""
    import httpx
    post = _post or (lambda url, data: httpx.post(url, data=data, timeout=60).json())
    get = _get or (lambda url, q: httpx.get(url, params=q, timeout=20).json())
    tk, pid = str(trang['page_token']), str(trang['page_id'])
    if b.get('anh_url'):
        j = post(f'{GRAPH}/{pid}/photos', {'url': b['anh_url'], 'caption': b['noi_dung'], 'published': 'true', 'access_token': tk})
        post_id = str(j.get('post_id') or '')
    else:
        j = post(f'{GRAPH}/{pid}/feed', {'message': b['noi_dung'], 'access_token': tk})
        post_id = str(j.get('id') or '')
    if not post_id:
        return False, '', '', str((j.get('error') or {}).get('message') or j)[:450]
    link = str((get(f'{GRAPH}/{post_id}', {'fields': 'permalink_url', 'access_token': tk}) or {}).get('permalink_url') or '')
    return True, post_id, link, ''


def hanh_dong(id_: int, hanh: str, nguoi: str, *, _lay_ket_noi: Optional[Callable[[], Any]] = None,
              _tele_goi=None, _dang=None) -> Tuple[bool, str]:
    if hanh not in ('bd', 'bb'):
        return False, 'Nút không hợp lệ.'
    nguoi = (nguoi or '')[:120]

    def _giu_cho(ct):
        b = _lay_bai(ct, id_)
        if not b:
            return False, f'Không có bài #{id_}.', None, None
        if b['trang_thai'] not in ('cho_duyet', 'loi'):
            return False, f'Bài #{id_} đã {TT.get(b["trang_thai"], b["trang_thai"])}.', None, None
        if hanh == 'bb':
            ct.execute("UPDATE chat_bai_dang SET trang_thai = 'bo', nguoi_duyet = %s, cap_nhat = NOW() WHERE id = %s",
                       (nguoi, id_))
            _ve_lai(ct, _lay_bai(ct, id_), _tele_goi=_tele_goi)
            return True, 'Đã bỏ bài.', None, None
        # Khoá bài trước khi gọi Facebook: bấm hai lần không thành hai bài.
        ct.execute("""UPDATE chat_bai_dang SET trang_thai = 'da_dang', nguoi_duyet = %s, cap_nhat = NOW()
                      WHERE id = %s AND trang_thai IN ('cho_duyet','loi')""", (nguoi, id_))
        if ct.rowcount != 1:
            return False, 'Bài đang được đăng.', None, None
        return None, '', b, _lay_trang(ct, b['khong_gian'])

    try:
        ok, tb, b, trang = csdl.voi_ct(_lay_ket_noi, _giu_cho)
    except RuntimeError as e:
        return False, str(e)
    if ok is not None:
        return ok, tb
    # Gọi Facebook KHÔNG giữ kết nối CSDL (đăng kèm ảnh có thể quá 10 giây = wait_timeout của MySQL).
    if not trang:
        ok, pid, link, loi = False, '', '', 'Shop chưa gắn Fanpage có token.'
    else:
        try:
            ok, pid, link, loi = (_dang or _dang_fb)(trang, b)
        except Exception as e:  # noqa: BLE001
            ok, pid, link, loi = False, '', '', f'Lỗi mạng: {e}'[:450]

    def _ket_qua(ct):
        if ok:
            ct.execute("""UPDATE chat_bai_dang SET fb_post_id = %s, fb_link = %s, dang_luc = NOW(), loi = NULL
                          WHERE id = %s""", (pid, link or None, id_))
        else:
            ct.execute("UPDATE chat_bai_dang SET trang_thai = 'loi', loi = %s WHERE id = %s", (loi, id_))
        _ve_lai(ct, _lay_bai(ct, id_), _tele_goi=_tele_goi)

    csdl.voi_ct(_lay_ket_noi, _ket_qua)
    return (True, 'Đã đăng lên Fanpage.') if ok else (False, f'Đăng lỗi: {loi}'[:190])


# =====================================================================
# CLI — skill `dang-bai` gọi qua SSH:
#   python kenh/bai_dang.py gui /đường/dẫn/bai.json
#   bai.json = {"chu_de": "...", "noi_dung": "...", "anh_url": "https://...", "khong_gian": "tradingauto"}
# =====================================================================

def _cli(argv: List[str]) -> int:
    if len(argv) >= 3 and argv[1] == 'gui':
        with open(argv[2], encoding='utf-8') as f:
            d = json.load(f)
        id_ = gui_duyet(str(d.get('noi_dung') or ''), chu_de=str(d.get('chu_de') or ''),
                        anh_url=str(d.get('anh_url') or ''), khong_gian=str(d.get('khong_gian') or 'tradingauto'),
                        nguon=str(d.get('nguon') or ''))
        print(f'OK bai #{id_} da gui Telegram cho chu shop duyet.')
        return 0
    if len(argv) >= 3 and argv[1] == 'anh':          # anh <id> [url]  (bỏ url = thành bài chữ)
        print(doi_anh(int(argv[2]), argv[3] if len(argv) > 3 else ''))
        return 0
    if len(argv) >= 2 and argv[1] == 'ds':
        conn = csdl.ket_noi()
        with conn:
            with conn.cursor() as ct:
                ct.execute('SELECT id, chu_de, trang_thai, tao_luc, fb_link FROM chat_bai_dang ORDER BY id DESC LIMIT 20')
                for r in ct.fetchall() or []:
                    print(r['id'], r['trang_thai'], r['tao_luc'], r['chu_de'], r['fb_link'] or '')
        return 0
    print('Dùng: bai_dang.py gui <bai.json> | anh <id> [url] | ds')
    return 2


if __name__ == '__main__' and len(sys.argv) > 1:
    sys.exit(_cli(sys.argv))

if __name__ == '__main__':
    import re
    logging.disable(logging.WARNING)

    print('=== 1) thẻ + nút ===')
    b = {'id': 3, 'chu_de': 'MT5 Manager', 'noi_dung': 'Bài <thử> & xem', 'anh_url': 'https://x/a.jpg', 'trang_thai': 'cho_duyet'}
    t = the_bai(b, 'Tradingauto.org')
    assert 'BÀI #3' in t and '&lt;thử&gt; &amp;' in t and 'kèm ảnh' in t and 'TRẢ LỜI' in t
    n = nut_bai(b, 'k')
    assert [x['callback_data'][:2] for x in n[0]] == ['bd', 'bb']
    assert all(re.match(r'^b[db]:3:[0-9a-f]{16}$', x['callback_data']) for x in n[0])
    assert nut_bai(dict(b, trang_thai='da_dang'), 'k') == []
    assert 'Đăng lại' in nut_bai(dict(b, trang_thai='loi'), 'k')[0][0]['text']
    assert len(the_bai(dict(b, noi_dung='x' * 5000), 'T')) < 4096
    print('  OK')

    print('\n=== 2) gui_duyet / sua / hanh_dong với CSDL giả ===')

    class _CT:
        def __init__(s, db): s.db = db; s._kq = []; s.lastrowid = 0; s.rowcount = 0
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def execute(s, sql, ts=()):
            q = ' '.join(sql.split()); s._kq = []; db = s.db; s.rowcount = 0
            if q.startswith('SELECT access_token'):
                s._kq = [{'access_token': 'T', 'chat_id': '42'}]
            elif q.startswith('SELECT gia_tri FROM cau_hinh_thong_bao'):
                s._kq = [{'gia_tri': 'k'}]
            elif q.startswith('INSERT INTO chat_bai_dang'):
                i = len(db['bai']) + 1
                db['bai'][i] = dict(id=i, khong_gian=ts[0], chu_de=ts[1], noi_dung=ts[2], anh_url=ts[3],
                                    trang_thai='cho_duyet', tele_msg_id=None, nguoi_duyet=None, fb_link=None, loi=None)
                s.lastrowid = i
            elif q.startswith('SELECT * FROM chat_bai_dang WHERE id'):
                s._kq = [dict(db['bai'][ts[0]])] if ts[0] in db['bai'] else []
            elif q.startswith('SELECT * FROM chat_bai_dang WHERE tele_msg_id'):
                s._kq = [dict(x) for x in db['bai'].values() if x['tele_msg_id'] == ts[0]]
            elif q.startswith('SELECT page_id, page_token'):
                s._kq = [{'page_id': 'P', 'page_token': 'PT', 'ten': 'Tradingauto.org'}]
            elif q.startswith('UPDATE chat_bai_dang SET tele_msg_id'):
                db['bai'][ts[1]]['tele_msg_id'] = ts[0]
            elif q.startswith('UPDATE chat_bai_dang SET anh_url'):
                db['bai'][ts[1]]['anh_url'] = ts[0]
            elif q.startswith('UPDATE chat_bai_dang SET noi_dung'):
                db['bai'][ts[1]]['noi_dung'] = ts[0]
            elif q.startswith("UPDATE chat_bai_dang SET trang_thai = 'bo'"):
                db['bai'][ts[1]].update(trang_thai='bo', nguoi_duyet=ts[0])
            elif q.startswith("UPDATE chat_bai_dang SET trang_thai = 'da_dang'"):
                x = db['bai'][ts[1]]
                if x['trang_thai'] in ('cho_duyet', 'loi'):
                    x.update(trang_thai='da_dang', nguoi_duyet=ts[0]); s.rowcount = 1
            elif q.startswith('UPDATE chat_bai_dang SET fb_post_id'):
                db['bai'][ts[2]].update(fb_link=ts[1])
            elif q.startswith("UPDATE chat_bai_dang SET trang_thai = 'loi'"):
                db['bai'][ts[1]].update(trang_thai='loi', loi=ts[0])
        def fetchone(s): return s._kq[0] if s._kq else None
        def fetchall(s): return list(s._kq)

    class _Conn:
        def __init__(s, db): s.db = db
        def __enter__(s): return s
        def __exit__(s, *a): return False
        def cursor(s): return _CT(s.db)
        def commit(s): pass

    db = {'bai': {}}
    lay = lambda: _Conn(db)  # noqa: E731
    tele = []
    def _tg(pt, dl):
        tele.append((pt, dl))
        return {'ok': True, 'result': {'message_id': 900 + len(tele)}}
    i = gui_duyet('Nội dung bài MT5 Manager', chu_de='MT5 Manager', anh_url='https://tradingauto.org/a.jpg',
                  _lay_ket_noi=lay, _tele_goi=_tg)
    assert i == 1 and [x[0] for x in tele] == ['sendPhoto', 'sendMessage'] and db['bai'][1]['tele_msg_id'] == 902
    try:
        gui_duyet('x', anh_url='http://khong-an-toan/a.jpg', _lay_ket_noi=lay, _tele_goi=_tg); raise AssertionError
    except ValueError:
        pass
    ok, tb = doi_anh(1, 'https://tradingauto.org/b.jpg', _lay_ket_noi=lay, _tele_goi=_tg)
    assert ok and db['bai'][1]['anh_url'].endswith('b.jpg') and tele[-2][0] == 'sendPhoto'
    assert tele[-2][1]['reply_to_message_id'] == 902 and tele[-1][0] == 'editMessageText'
    assert not doi_anh(1, 'http://x/c.jpg', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    assert not sua(902, 'ngắn', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    ok, tb = sua(902, 'Nội dung mới đã sửa lại cho gọn hơn nhiều', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)
    assert ok and db['bai'][1]['noi_dung'].startswith('Nội dung mới') and tele[-1][0] == 'editMessageText'
    goi_fb = []
    ok, tb = hanh_dong(1, 'bd', 'Quang', _lay_ket_noi=lay, _tele_goi=_tg,
                       _dang=lambda tr, b: (goi_fb.append((tr['page_id'], b['noi_dung'])) or (True, 'P_1', 'https://fb/p1', '')))
    assert ok and goi_fb == [('P', 'Nội dung mới đã sửa lại cho gọn hơn nhiều')] and db['bai'][1]['fb_link'] == 'https://fb/p1'
    assert tele[-1][1]['reply_markup']['inline_keyboard'] == []
    assert not hanh_dong(1, 'bd', 'Q', _lay_ket_noi=lay, _tele_goi=_tg, _dang=lambda *a: (True, '', '', ''))[0]
    assert not sua(902, 'Nội dung sau khi đã đăng thì không sửa được', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)[0]
    i2 = gui_duyet('Bài hai sẽ lỗi khi đăng lên', _lay_ket_noi=lay, _tele_goi=_tg)
    ok, tb = hanh_dong(i2, 'bd', 'Q', _lay_ket_noi=lay, _tele_goi=_tg, _dang=lambda *a: (False, '', '', '(#200) thiếu quyền'))
    assert not ok and db['bai'][i2]['trang_thai'] == 'loi' and 'thiếu quyền' in tb
    assert 'Đăng lại' in json.dumps(tele[-1][1]['reply_markup'], ensure_ascii=False)
    assert hanh_dong(i2, 'bb', 'Q', _lay_ket_noi=lay, _tele_goi=_tg)[0] and db['bai'][i2]['trang_thai'] == 'bo'
    print('  OK — gửi ảnh + thẻ, sửa bằng trả lời, đăng một lần, lỗi -> đăng lại, bỏ')

    print('\n=== 3) _dang_fb chọn đúng API ===')
    goi = []
    fake_post = lambda url, data: (goi.append((url.rsplit('/', 1)[1], sorted(data))) or  # noqa: E731
                                   ({'id': 'PH', 'post_id': 'P_9'} if url.endswith('photos') else {'id': 'P_8'}))
    fake_get = lambda url, q: {'permalink_url': 'https://fb/' + url.rsplit('/', 1)[1]}  # noqa: E731
    tr = {'page_id': 'P', 'page_token': 'T'}
    assert _dang_fb(tr, {'anh_url': 'https://a', 'noi_dung': 'x'}, fake_post, fake_get) == (True, 'P_9', 'https://fb/P_9', '')
    assert _dang_fb(tr, {'anh_url': '', 'noi_dung': 'x'}, fake_post, fake_get)[1] == 'P_8'
    assert goi[0] == ('photos', ['access_token', 'caption', 'published', 'url']) and goi[1] == ('feed', ['access_token', 'message'])
    assert _dang_fb(tr, {'anh_url': '', 'noi_dung': 'x'}, lambda u, d: {'error': {'message': 'hết hạn'}}, fake_get)[3] == 'hết hạn'
    print('  OK')
    print('\nTỰ KIỂM kenh/bai_dang.py QUA.')
