"""Live-тест киллер-фич KF1 (зарплатный бейдж), KF2 (сопроводительное),
KF4 (избранное + отсутствие откликов). v5.

Две вещи, на которых спотыкались прошлые версии — обе про адресацию сообщений,
не про продукт:

1. «🔍 Подробнее» РЕДАКТИРУЕТ сообщение ленты в карточку (edit_message_text),
   а не шлёт новое. Значит после открытия карточки кнопок «Подробнее» в этом
   сообщении уже нет — перебирать вакансии можно только возвращаясь «🔙 Назад».
2. Индексы `.bubble.is-in` nth() ПЛЫВУТ: Telegram Web подгружает/выгружает
   историю, и бабл #12 через несколько секунд уже другой. Единственный
   стабильный якорь — атрибут `data-mid` (id сообщения). v5 фиксирует ленту
   по mid и всюду ходит через `.bubble[data-mid="..."]`.

KF1 требует ПОСТОЯННОЙ вакансии (💼) с зарплатой: на заказах (kwork/profi/fl
и прочих фриланс-источниках) бейдж скрыт намеренно — медиана рынка строится
из месячных full-time ЗП и с бюджетом разового заказа несопоставима. Поэтому
probe перебирает ленту, пока не найдёт подходящую карточку.
"""
import asyncio
import sys
import time
from pathlib import Path

SESSION = "/Users/cebers_mini/.gaechkin/tg_test_session.json"
SHOTS = Path("/tmp/vh_audit/shots_kf2")
SHOTS.mkdir(parents=True, exist_ok=True)
PROXY = "http://127.0.0.1:2081"
BOT = "VakHunter_bot"
BADGES = ("Выше рынка", "Ниже рынка", "В рынке")
RES = []


def add(name, ok, detail=""):
    RES.append(ok)
    print((("✅" if ok else "❌") + f" {name}: {detail}")[:220], flush=True)


async def shoot(page, name):
    try:
        await page.screenshot(path=str(SHOTS / f"{int(time.time())}_{name}.png"))
    except Exception:
        pass


# ── адресация по data-mid (индексы nth() плывут, mid — нет) ────────────────
def B(page, mid):
    return page.locator(f'.bubble[data-mid="{mid}"]').first


async def mid_text(page, mid):
    try:
        return await B(page, mid).inner_text()
    except Exception:
        return ""


async def mid_buttons(page, mid):
    try:
        return await B(page, mid).locator(".reply-markup-button").all_inner_texts()
    except Exception:
        return []


async def find_mid_with(page, btn_text, lookback=16):
    """data-mid последнего входящего сообщения, где есть кнопка btn_text."""
    n = await page.locator(".bubble.is-in").count()
    for i in range(n - 1, max(-1, n - 1 - lookback), -1):
        b = page.locator(".bubble.is-in").nth(i)
        try:
            if await b.locator(f'.reply-markup-button:has-text("{btn_text}")').count() > 0:
                return await b.get_attribute("data-mid")
        except Exception:
            continue
    return None


async def click_mid(page, mid, btn_text, nth=0, timeout=15000):
    btns = B(page, mid).locator(f'.reply-markup-button:has-text("{btn_text}")')
    if await btns.count() <= nth:
        raise RuntimeError(f"в сообщении mid={mid} нет «{btn_text}» (nth={nth})")
    await btns.nth(nth).click(timeout=timeout)


async def wait_mid_change(page, mid, before, timeout_s=25, markers=None):
    """Ждём, пока сообщение mid изменится (оно редактируется на месте)."""
    deadline = time.time() + timeout_s
    txt = before
    while time.time() < deadline:
        txt = await mid_text(page, mid)
        if txt and txt != before and (not markers or any(m in txt for m in markers)):
            return txt
        await page.wait_for_timeout(1200)
    return txt


async def wait_buttons(page, mid, btn_text, min_count=1, timeout_s=20):
    """Ждём, пока в сообщении появятся КНОПКИ btn_text (не просто текст).

    Критично для возврата «🔙 Назад»: текст ленты сам содержит слово
    «Подробнее» («Жми «🔍 Подробнее» под нужной вакансией»), поэтому ожидание
    по тексту срабатывает раньше, чем отрисуется инлайн-клавиатура, и
    следующий клик падает с «нет кнопки (nth=N)».
    """
    deadline = time.time() + timeout_s
    cnt = 0
    while time.time() < deadline:
        try:
            cnt = await B(page, mid).locator(
                f'.reply-markup-button:has-text("{btn_text}")').count()
        except Exception:
            cnt = 0
        if cnt >= min_count:
            return cnt
        await page.wait_for_timeout(1000)
    return cnt


async def wait_new(page, before_n, timeout_s=50, markers=None):
    """Ждём НОВОЕ входящее сообщение; возвращаем его текст."""
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        n = await page.locator(".bubble.is-in").count()
        if n > before_n:
            for i in range(n - 1, max(-1, before_n - 1), -1):
                try:
                    txt = await page.locator(".bubble.is-in").nth(i).inner_text()
                except Exception:
                    continue
                if not markers or any(m in txt for m in markers):
                    return txt
        await page.wait_for_timeout(1500)
    n = await page.locator(".bubble.is-in").count()
    try:
        return await page.locator(".bubble.is-in").nth(n - 1).inner_text() if n else ""
    except Exception:
        return ""


async def wait_new_mid(page, before_n, timeout_s=40):
    """data-mid первого НОВОГО входящего сообщения (или None)."""
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        n = await page.locator(".bubble.is-in").count()
        if n > before_n:
            try:
                return await page.locator(".bubble.is-in").nth(n - 1).get_attribute("data-mid")
            except Exception:
                pass
        await page.wait_for_timeout(1200)
    return None


async def send_text(page, inp, text):
    """Reply-кнопки шлём текстом: Enter после fill с emoji не отправляет."""
    await inp.click()
    await inp.fill(text)
    await page.wait_for_timeout(800)
    btn = page.locator(".btn-send")
    if await btn.count():
        await btn.first.click()
    else:
        await page.keyboard.press("Enter")


async def main():
    from playwright.async_api import async_playwright
    async with async_playwright() as pw:
        browser = await pw.chromium.launch(headless=True, proxy={"server": PROXY})
        ctx = await browser.new_context(storage_state=SESSION,
                                        viewport={"width": 1280, "height": 1100},
                                        locale="ru-RU")
        page = await ctx.new_page()
        try:
            await page.goto(f"https://web.telegram.org/k/#@{BOT}",
                            wait_until="domcontentloaded", timeout=60000)
            await page.wait_for_timeout(7000)
            inp = page.locator(".input-message-input").first

            before = await page.locator(".bubble.is-in").count()
            await send_text(page, inp, "/search python разработчик москва")
            await wait_new(page, before, timeout_s=70,
                           markers=("Подробнее", "Найдено", "ваканс"))
            await page.wait_for_timeout(2500)
            await shoot(page, "01_feed")

            feed_mid = await find_mid_with(page, "Подробнее")
            n_cards = 0
            if feed_mid:
                n_cards = await B(page, feed_mid).locator(
                    '.reply-markup-button:has-text("Подробнее")').count()
            add("Фид с кнопками «Подробнее»", n_cards >= 1,
                f"mid={feed_mid}, кнопок={n_cards}")

            # ── KF1: перебираем карточки, пока не найдём 💼 с ЗП и бейджем
            badge_found = False
            order_badge_leak = False
            salary_vac_seen = False
            checked = 0
            hit_card = ""
            feed_txt = await mid_text(page, feed_mid) if feed_mid else ""

            for i in range(n_cards):
                try:
                    await click_mid(page, feed_mid, "Подробнее", nth=i)
                except Exception as e:
                    print(f"   (открытие #{i+1}: {e})", flush=True)
                    break
                card = await wait_mid_change(page, feed_mid, feed_txt,
                                             timeout_s=20, markers=("Назад",))
                checked += 1
                has_badge = any(m in card for m in BADGES)
                is_order = card.lstrip().startswith("🛒")
                has_salary = "💰" in card and "Не указана" not in card

                if is_order and has_badge:
                    order_badge_leak = True          # семантический баг
                if (not is_order) and has_salary:
                    salary_vac_seen = True
                    if has_badge:
                        badge_found = True
                        hit_card = card
                        await shoot(page, "02_badge")
                        break

                try:                                  # назад к ленте
                    await click_mid(page, feed_mid, "Назад")
                    feed_txt = await wait_mid_change(page, feed_mid, card,
                                                     timeout_s=20, markers=("Подробнее",))
                    # ждём именно КНОПКИ: нужна как минимум следующая по счёту
                    got = await wait_buttons(page, feed_mid, "Подробнее",
                                             min_count=i + 2, timeout_s=20)
                    if got < i + 2:
                        print(f"   (возврат #{i+1}: кнопок «Подробнее» {got}, "
                              f"ждали ≥{i + 2})", flush=True)
                        break
                except Exception as e:
                    print(f"   (возврат #{i+1}: {e})", flush=True)
                    break

            add("KF1: бейдж НЕ утёк на 🛒-заказ", not order_badge_leak,
                f"проверено карточек={checked}")
            add("KF1: бейдж рынка на 💼-вакансии с ЗП", badge_found,
                (hit_card[:110].replace("\n", " ") if badge_found else
                 f"открыто {checked}/{n_cards}; 💼 с ЗП встречалась: {salary_vac_seen}. "
                 "На заказах бейдж скрыт намеренно — нужна ПОСТОЯННАЯ вакансия с ЗП"))

            # ── KF2: письмо по открытой карточке (тот же mid)
            btns = await mid_buttons(page, feed_mid)
            has_cl = any("Сопроводительное" in b for b in btns)
            add("KF2: кнопка «✍️ Сопроводительное»", has_cl, f"кнопки: {btns[:5]}")
            if has_cl:
                before = await page.locator(".bubble.is-in").count()
                await click_mid(page, feed_mid, "Сопроводительное")
                await wait_new(page, before, timeout_s=30,
                               markers=("профил", "резюме", "Из "))
                pick_mid = await find_mid_with(page, "Из профиля")
                letter_txt = ""
                if pick_mid:
                    before = await page.locator(".bubble.is-in").count()
                    await click_mid(page, pick_mid, "Из профиля")
                    # Бот сначала шлёт «⏳ Генерирую сопроводительное письмо…»,
                    # а потом РЕДАКТИРУЕТ это же сообщение в готовое письмо.
                    # Поэтому ждём изменения найденного mid, а не нового бабла:
                    # иначе probe читает плейсхолдер и считает это провалом.
                    LETTER = ("Здравствуйте", "Добрый", "уважением",
                              "Не удалось", "Лимит ИИ")
                    ph_mid = await wait_new_mid(page, before, timeout_s=40)
                    if ph_mid:
                        ph_txt = await mid_text(page, ph_mid)
                        letter_txt = ph_txt
                        if not any(m in ph_txt for m in LETTER):
                            letter_txt = await wait_mid_change(
                                page, ph_mid, ph_txt, timeout_s=150, markers=LETTER)
                    else:
                        letter_txt = await wait_new(page, before, timeout_s=120,
                                                    markers=LETTER)
                await shoot(page, "03_letter")
                # «Лимит ИИ-функций исчерпан» — КОРРЕКТНЫЙ ответ продукта, а не
                # поломка: тарифный гейт сработал. Отличаем его от сбоя, иначе
                # повторные прогоны probe (каждый тратит квоту) дают ложный ❌.
                # Если ловите его часто — у тест-юзера истёк tariff_expires и
                # лимиты режутся до Trial 3/день (Decisions D26); продлите его.
                quota_hit = "Лимит ИИ" in letter_txt or "лимит ии" in letter_txt.lower()
                if quota_hit:
                    add("KF2: письмо (квота исчерпана — гейт отработал)", True,
                        "письмо не проверено в этом прогоне: "
                        + letter_txt[:70].replace("\n", " "))
                else:
                    ok_letter = len(letter_txt) > 250 and "Не удалось" not in letter_txt
                    add("KF2: письмо сгенерировано", ok_letter,
                        f"len={len(letter_txt)}: {letter_txt[:80]!r}")

                has_regen = await page.locator(
                    '.bubble.is-in .reply-markup-button:has-text("🔁"), '
                    '.bubble.is-in .reply-markup-button:has-text("ороче")').count() > 0
                add("KF2: кнопки доработки письма", has_regen)

            # ── KF4: сохранить в избранное → видно в «⭐ Сохранённые».
            # Раньше здесь была воронка откликов; отклики убраны из продукта
            # 2026-07-31 (Decisions D27 — сервис не откликается за юзера),
            # «В избранное» заменило их и как основной сигнал обучения.
            fav_mid = await find_mid_with(page, "В избранное")
            if fav_mid:
                await click_mid(page, fav_mid, "В избранное")
                await page.wait_for_timeout(3000)
            before = await page.locator(".bubble.is-in").count()
            await send_text(page, inp, "⭐ Сохранённые")
            txt = await wait_new(page, before, timeout_s=30,
                                 markers=("збранн", "охранённ", "пусто"))
            await shoot(page, "04_favorites")
            add("KF4: сохранённая вакансия видна в избранном",
                bool(txt) and "пусто" not in txt.lower(),
                txt[:100].replace("\n", " "))

            leaked = await page.locator(
                '.bubble.is-in .reply-markup-button:has-text("Откликнул"), '
                '.bubble.is-in .reply-markup-button:has-text("Мои отклики")').count()
            add("KF4: следов откликов в UI нет", leaked == 0, f"найдено кнопок: {leaked}")
        finally:
            await browser.close()

    print(f"\nKF probe v5: {sum(RES)}/{len(RES)} PASS")
    return 0 if all(RES) else 1


if __name__ == "__main__":
    sys.exit(asyncio.run(main()))
