const fs = require("fs");
const os = require("os");
const path = require("path");
const { execFileSync } = require("child_process");

const ROOT_DIR = path.resolve(__dirname, "..");
const VIDEO_DIR = path.join(ROOT_DIR, "assets", "videos");
const FRONTEND_URL = stripTrailingSlash(process.env.QLOCKIFY_FRONTEND_URL || "http://localhost:5173");
const BACKEND_URL = stripTrailingSlash(process.env.QLOCKIFY_BACKEND_URL || "http://localhost:8000");
const BACKEND_REPO = path.resolve(
  ROOT_DIR,
  process.env.QLOCKIFY_BACKEND_REPO || path.join("..", "qlockify-backend"),
);
const CHROME_EXECUTABLE = process.env.QLOCKIFY_CHROME_PATH || process.env.CHROME_EXECUTABLE || "";
const VIDEO_OUTPUT = path.resolve(
  ROOT_DIR,
  process.env.QLOCKIFY_VIDEO_OUTPUT || path.join("assets", "videos", "qlockify-demo-flow.webm"),
);
const VIDEO_MOBILE = process.env.QLOCKIFY_VIDEO_MOBILE || "09900000001";
const VIDEO_PASSWORD = process.env.QLOCKIFY_VIDEO_PASSWORD || "QlockifyVideo!2026";
const DOWNLOAD_DIR = path.join(os.tmpdir(), "qlockify-showcase-video-downloads");
const SHOW_CURSOR = process.env.QLOCKIFY_VIDEO_CURSOR !== "0";

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function printHelp() {
  console.log(`
Usage:
  node scripts/capture-demo-video.js

Required services:
  - frontend: ${FRONTEND_URL}
  - backend: ${BACKEND_URL}
  - PostgreSQL/Redis/Celery using your normal local setup

Output:
  ${VIDEO_OUTPUT}

Useful environment variables:
  QLOCKIFY_FRONTEND_URL     Frontend URL. Default: http://localhost:5173
  QLOCKIFY_BACKEND_URL      Backend URL. Default: http://localhost:8000
  QLOCKIFY_BACKEND_REPO     Backend repo path. Default: ../qlockify-backend
  QLOCKIFY_CHROME_PATH      Optional installed Chrome executable
  QLOCKIFY_VIDEO_OUTPUT     Optional output WebM path
  QLOCKIFY_VIDEO_MOBILE     Fixture user mobile. Default: 09900000001
  QLOCKIFY_VIDEO_PASSWORD   Fixture user password
  QLOCKIFY_VIDEO_CURSOR=0   Disable the visible cursor overlay

The script resets only the dedicated video fixture user/workspaces.
`);
}

if (process.argv.includes("--help") || process.argv.includes("-h")) {
  printHelp();
  process.exit(0);
}

function stripTrailingSlash(value) {
  return value.replace(/\/+$/, "");
}

function ensureDir(dir) {
  fs.mkdirSync(dir, { recursive: true });
}

function pythonExecutable() {
  const windowsVenv = path.join(BACKEND_REPO, ".venv", "Scripts", "python.exe");
  const unixVenv = path.join(BACKEND_REPO, ".venv", "bin", "python");
  if (fs.existsSync(windowsVenv)) return windowsVenv;
  if (fs.existsSync(unixVenv)) return unixVenv;
  return "python";
}

function runDjangoShell(code) {
  if (!fs.existsSync(path.join(BACKEND_REPO, "manage.py"))) {
    throw new Error(`Backend repo was not found at ${BACKEND_REPO}`);
  }
  const output = execFileSync(pythonExecutable(), ["manage.py", "shell", "-c", code], {
    cwd: BACKEND_REPO,
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  });
  return output.trim().split(/\r?\n/).filter(Boolean).at(-1);
}

async function assertReachable(url, label) {
  try {
    const response = await fetch(url);
    if (!response.ok && response.status >= 500) {
      throw new Error(`${label} returned ${response.status}`);
    }
  } catch (error) {
    throw new Error(`${label} is not reachable at ${url}. Start the service first. ${error.message}`);
  }
}

function seedVideoFixture() {
  const mobile = JSON.stringify(VIDEO_MOBILE);
  const password = JSON.stringify(VIDEO_PASSWORD);
  const code = `
import json
from datetime import timedelta
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.db import transaction
from django.utils import timezone

from apps.clients.models import Client
from apps.notifications.services import RedisNotificationStore
from apps.projects.models import Project, ProjectAccess, ProjectUserRate
from apps.tags.models import Tag
from apps.time_entries.models import TimeEntry
from apps.workspaces.models import HourlyRateHistory, PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate

User = get_user_model()
MOBILE = ${mobile}
PASSWORD = ${password}

def ensure_user(mobile, first_name, last_name, role_user=False):
    user, _ = User.all_objects.update_or_create(
        mobile=mobile,
        defaults={
            "first_name": first_name,
            "last_name": last_name,
            "email": f"{mobile}@video.qlockify.local",
            "is_active": True,
            "is_verified": True,
            "is_staff": role_user,
            "is_superuser": role_user,
            "is_deleted": False,
            "deleted_at": None,
        },
    )
    user.set_password(PASSWORD if role_user else "MemberPass!2026")
    user.save()
    return user

def add_rate(workspace, user, amount, effective_from):
    rate, _ = WorkspaceUserRate.objects.update_or_create(
        workspace=workspace,
        user=user,
        defaults={"hourly_rate": Decimal(amount), "currency": "IRT", "effective_from": effective_from, "is_active": True},
    )
    HourlyRateHistory.objects.create(
        workspace=workspace,
        user=user,
        scope=HourlyRateHistory.Scope.WORKSPACE,
        hourly_rate=rate.hourly_rate,
        currency=rate.currency,
        effective_from=effective_from,
        is_active=True,
    )
    return rate

def add_project_rate(project, user, amount, effective_from):
    rate = ProjectUserRate.objects.create(
        project=project,
        user=user,
        hourly_rate=Decimal(amount),
        currency="IRT",
        effective_from=effective_from,
        is_active=True,
    )
    HourlyRateHistory.objects.create(
        workspace=project.workspace,
        project=project,
        user=user,
        scope=HourlyRateHistory.Scope.PROJECT,
        hourly_rate=rate.hourly_rate,
        currency=rate.currency,
        effective_from=effective_from,
        is_active=True,
    )
    return rate

def add_entry(workspace, user, project, tags, days_ago, hour, minutes, description, billable=True):
    start = timezone.now().replace(hour=hour, minute=0, second=0, microsecond=0) - timedelta(days=days_ago)
    end = start + timedelta(minutes=minutes)
    rate = None
    if billable:
        rate = (
            ProjectUserRate.objects.filter(project=project, user=user, is_deleted=False, is_active=True).order_by("-effective_from").first()
            if project else None
        ) or WorkspaceUserRate.objects.filter(workspace=workspace, user=user, is_deleted=False).order_by("-effective_from").first()
    entry = TimeEntry.objects.create(
        workspace=workspace,
        user=user,
        project=project,
        description=description,
        start_time=start,
        end_time=end,
        duration=end - start,
        is_billable=billable,
        hourly_rate=rate.hourly_rate if rate else None,
        currency="IRT",
        is_active=True,
    )
    entry.tags.set(tags)
    return entry

with transaction.atomic():
    owner = User.all_objects.filter(mobile=MOBILE).first()
    if owner:
        for workspace in Workspace.all_objects.filter(owner=owner):
            workspace.hard_delete()
        RedisNotificationStore.clear_user(str(owner.id))

    for mobile in ["09900000002", "09900000003", "09900000004"]:
        user = User.all_objects.filter(mobile=mobile).first()
        if user:
            for workspace in Workspace.all_objects.filter(owner=user):
                workspace.hard_delete()
            user.hard_delete()

    owner = ensure_user(MOBILE, "Qlockify", "Video", True)
    admin = ensure_user("09900000002", "نیلوفر", "ادمین")
    member = ensure_user("09900000003", "آرمان", "عضو")
    guest = ensure_user("09900000004", "سارا", "مهمان")

    PriceUnit.get_or_restore(
        code="IRT",
        defaults={"name": "Iranian Toman", "local_name": "تومان", "symbol": "تومان", "is_active": True},
    )

    secondary_one = Workspace.objects.create(name="تیم محصول کلاکیفای", description="Workspace used to show account context.", owner=owner, is_active=True)
    secondary_two = Workspace.objects.create(name="آژانس زمان‌یار", description="Secondary workspace for the video account.", owner=owner, is_active=True)
    primary = Workspace.objects.create(name="استودیو کوئرا", description="Workspace prepared for the product tour video.", owner=owner, is_active=True)

    for workspace in [secondary_one, secondary_two, primary]:
        WorkspaceMembership.objects.update_or_create(
            workspace=workspace,
            user=owner,
            defaults={"role": WorkspaceMembership.Role.OWNER, "is_active": True, "is_deleted": False, "deleted_at": None},
        )

    WorkspaceMembership.objects.bulk_create([
        WorkspaceMembership(workspace=primary, user=admin, role=WorkspaceMembership.Role.ADMIN, is_active=True),
        WorkspaceMembership(workspace=primary, user=member, role=WorkspaceMembership.Role.MEMBER, is_active=True),
        WorkspaceMembership(workspace=primary, user=guest, role=WorkspaceMembership.Role.GUEST, is_active=True),
    ])

    now = timezone.now()
    for user, amount in [(owner, "850000"), (admin, "700000"), (member, "560000"), (guest, "380000")]:
        add_rate(primary, user, amount, now - timedelta(days=75))

    client_a = Client.objects.create(workspace=primary, name="کوئرا کالج", notes="مشتری آموزشی برای گزارش‌های ویدئو", is_active=True)
    client_b = Client.objects.create(workspace=primary, name="استودیو نووا", notes="مشتری طراحی و محصول", is_active=True)
    client_c = Client.objects.create(workspace=primary, name="عملیات داخلی", notes="کارهای داخلی تیم", is_active=True)

    project_portal = Project.objects.create(workspace=primary, client=client_a, name="پورتال دانشجویان", description="طراحی و توسعه پورتال آموزشی", color="#0ea5e9", is_active=True)
    project_marketing = Project.objects.create(workspace=primary, client=client_b, name="وب‌سایت تبلیغاتی", description="صفحه فرود و کمپین معرفی", color="#f97316", is_active=True)
    project_ops = Project.objects.create(workspace=primary, client=client_c, name="اتوماسیون داخلی", description="ابزارهای داخلی تیم", color="#6366f1", is_active=True)
    project_archive = Project.objects.create(workspace=primary, client=client_b, name="کمپین قابل آرشیو", description="پروژه‌ای برای نمایش آرشیو و حذف", color="#94a3b8", is_active=True)
    Project.objects.create(workspace=primary, client=client_a, name="آرشیو قدیمی", description="نمونه آرشیوی از قبل", color="#64748b", is_archived=True, is_active=True)

    Project.objects.filter(pk=project_archive.pk).update(created_at=timezone.now() + timedelta(seconds=1))

    tags = {
        "design": Tag.objects.create(workspace=primary, name="طراحی", color="#f97316", is_active=True),
        "backend": Tag.objects.create(workspace=primary, name="بک‌اند", color="#0ea5e9", is_active=True),
        "meeting": Tag.objects.create(workspace=primary, name="جلسه", color="#8b5cf6", is_active=True),
        "qa": Tag.objects.create(workspace=primary, name="کنترل کیفیت", color="#22c55e", is_active=True),
        "research": Tag.objects.create(workspace=primary, name="تحقیق", color="#eab308", is_active=True),
    }

    for user in [member, guest]:
        for project in [project_portal, project_marketing, project_ops, project_archive]:
            ProjectAccess.objects.create(project=project, user=user, is_active=True)

    add_project_rate(project_marketing, owner, "1050000", now - timedelta(days=30))
    add_project_rate(project_portal, member, "620000", now - timedelta(days=25))

    entries = [
        (owner, project_marketing, [tags["design"], tags["research"]], 1, 9, 150, "بازبینی تجربه کاربری صفحه فرود", True),
        (owner, project_ops, [tags["backend"], tags["qa"]], 2, 10, 190, "بهبود خروجی گزارش‌ها", True),
        (owner, project_portal, [tags["meeting"]], 3, 13, 75, "هماهنگی با تیم محصول", True),
        (owner, None, [tags["meeting"]], 4, 16, 45, "جلسه داخلی بدون پروژه", False),
        (admin, project_portal, [tags["backend"]], 1, 8, 240, "کنترل دسترسی پروژه‌ها", True),
        (admin, project_ops, [tags["qa"]], 5, 11, 120, "تست گزارش ماهانه", True),
        (member, project_portal, [tags["backend"], tags["qa"]], 2, 9, 300, "اصلاح فرم تایم‌شیت", True),
        (member, project_marketing, [tags["design"]], 6, 14, 180, "پولیش رابط کاربری", True),
        (guest, project_portal, [tags["meeting"]], 3, 10, 90, "همگام‌سازی با مشتری", True),
        (guest, None, [], 7, 15, 60, "کار دسته‌بندی‌نشده", False),
    ]
    for entry in entries:
        add_entry(primary, entry[0], entry[1], entry[2], entry[3], entry[4], entry[5], entry[6], entry[7])

print(json.dumps({
    "user_id": str(owner.id),
    "workspace_id": str(primary.id),
    "workspace_name": "استودیو کوئرا",
    "mobile": MOBILE,
    "password": PASSWORD,
    "project_to_archive_id": str(project_archive.id),
    "project_to_archive": "کمپین قابل آرشیو",
    "project_for_timer": "وب‌سایت تبلیغاتی",
    "filter_project": "پورتال دانشجویان",
    "tag_one": "طراحی",
    "tag_two": "تحقیق",
}))
`;
  return JSON.parse(runDjangoShell(code));
}

async function createBrowser() {
  const { chromium } = require("playwright");
  const options = { headless: true, slowMo: 55 };
  if (CHROME_EXECUTABLE) {
    options.executablePath = CHROME_EXECUTABLE;
  }
  return chromium.launch(options);
}

async function settle(page, delay = 250) {
  await page.waitForLoadState("domcontentloaded", { timeout: 3000 }).catch(() => {});
  await page.waitForLoadState("networkidle", { timeout: 900 }).catch(() => {});
  await page
    .waitForFunction(
      () => {
        const text = document.body.innerText || "";
        const skeletons = document.querySelectorAll('[class*="animate-pulse"], [class*="skeleton"], [aria-busy="true"]').length;
        return skeletons === 0 && !/Loading|در حال|بارگذاری|لطفا صبر/i.test(text);
      },
      { timeout: 2500 },
    )
    .catch(() => {});
  await wait(delay);
}

async function smoothScrollBy(page, totalDelta, steps = 8, delay = 180) {
  const stepDelta = totalDelta / steps;
  for (let index = 0; index < steps; index += 1) {
    await page.mouse.wheel(0, stepDelta);
    await wait(delay);
  }
  await settle(page, 120);
}

async function goto(page, route, delay = 1200) {
  await page.goto(`${FRONTEND_URL}${route}`, { waitUntil: "domcontentloaded" });
  await settle(page, delay);
}

async function clickAppNav(page, route, delay = 450) {
  await closeAnyOpenModal(page);
  let clicked = await clickFirst(page, [
    `aside a[href="${route}"]:visible`,
    `nav a[href="${route}"]:visible`,
    `a[href="${route}"]:visible`,
  ], 2500);
  if (!clicked) {
    const target = await page.evaluate((route) => {
      const links = Array.from(document.querySelectorAll(`a[href="${route}"]`));
      const visible = links.find((link) => {
        const rect = link.getBoundingClientRect();
        const style = window.getComputedStyle(link);
        return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
      });
      if (!visible) return null;
      const rect = visible.getBoundingClientRect();
      return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
    }, route);
    if (target) {
      await moveVisibleCursor(page, target.x, target.y);
      await page.mouse.click(target.x, target.y);
      await moveVisibleCursor(page, target.x, target.y, { click: true });
      clicked = true;
    }
  }
  if (!clicked) {
    throw new Error(`Could not navigate via UI to ${route}`);
  }
  await page.waitForURL(new RegExp(`${route.replace("/", "\\/")}(?:$|[?#])`), { timeout: 10000 });
  await settle(page, delay);
}

async function openProfileFromAvatar(page) {
  await clickFirst(page, [
    page.locator("button").filter({ hasText: /^Q$/ }),
    page.locator("button").filter({ hasText: /Qlockify Video/i }),
  ], 2500);
  await clickFirst(page, [
    'a[href="/profile"]',
    page.getByRole("link", { name: /Profile|پروفایل|حساب/i }),
    page.getByText(/Profile|پروفایل|حساب/i),
  ], 2500);
  await page.waitForURL(/\/profile(?:$|[?#])/, { timeout: 10000 }).catch(() => {});
  await settle(page, 450);
}

async function clickFirst(page, locators, timeout = 5000) {
  for (const locator of locators) {
    const item = typeof locator === "string" ? page.locator(locator).first() : locator.first();
    if (await clickLocatorWithCursor(page, item, timeout).then(() => true).catch(() => false)) {
      await settle(page, 180);
      return true;
    }
  }
  return false;
}

async function ensureVisibleCursor(page) {
  if (!SHOW_CURSOR) return;
  await page.evaluate(() => {
    if (!document.body || document.getElementById("qlockify-video-cursor")) return;

    const style = document.createElement("style");
    style.id = "qlockify-video-cursor-style";
    style.textContent = `
      #qlockify-video-cursor {
        position: fixed;
        left: 0;
        top: 0;
        z-index: 2147483647;
        width: 24px;
        height: 24px;
        pointer-events: none;
        transform: translate(28px, 28px);
        transition: transform 160ms cubic-bezier(.2,.8,.2,1);
        filter: drop-shadow(0 8px 18px rgba(2, 6, 23, .55));
      }
      #qlockify-video-cursor::before {
        content: "";
        position: absolute;
        left: 3px;
        top: 2px;
        width: 0;
        height: 0;
        border-right: 14px solid transparent;
        border-bottom: 22px solid #38bdf8;
        transform: rotate(-28deg);
      }
      #qlockify-video-cursor::after {
        content: "";
        position: absolute;
        left: 7px;
        top: 6px;
        width: 0;
        height: 0;
        border-right: 8px solid transparent;
        border-bottom: 14px solid #ffffff;
        transform: rotate(-28deg);
        opacity: .95;
      }
      .qlockify-video-click-ring {
        position: fixed;
        z-index: 2147483646;
        width: 16px;
        height: 16px;
        margin-left: -8px;
        margin-top: -8px;
        border-radius: 999px;
        border: 2px solid rgba(56, 189, 248, .9);
        pointer-events: none;
        animation: qlockify-video-click-ring 420ms ease-out forwards;
      }
      @keyframes qlockify-video-click-ring {
        from { transform: scale(.8); opacity: .95; }
        to { transform: scale(3.6); opacity: 0; }
      }
    `;

    const cursor = document.createElement("div");
    cursor.id = "qlockify-video-cursor";
    document.head.appendChild(style);
    document.body.appendChild(cursor);
  });
}

async function moveVisibleCursor(page, x, y, { click = false } = {}) {
  if (!SHOW_CURSOR) return;
  await ensureVisibleCursor(page).catch(() => {});
  await page.evaluate(
    ({ x, y, click }) => {
      const cursor = document.getElementById("qlockify-video-cursor");
      if (!cursor) return;
      cursor.style.transform = `translate(${x}px, ${y}px)`;
      if (click) {
        const ring = document.createElement("span");
        ring.className = "qlockify-video-click-ring";
        ring.style.left = `${x}px`;
        ring.style.top = `${y}px`;
        document.body.appendChild(ring);
        window.setTimeout(() => ring.remove(), 460);
      }
    },
    { x, y, click },
  );
  await wait(click ? 70 : 170);
}

async function clickLocatorWithCursor(page, locator, timeout = 5000) {
  await locator.waitFor({ state: "visible", timeout });
  await locator.scrollIntoViewIfNeeded({ timeout }).catch(() => {});
  const box = await locator.boundingBox().catch(() => null);
  if (!box) {
    await locator.click({ timeout });
    return;
  }

  const x = box.x + box.width / 2;
  const y = box.y + box.height / 2;
  await moveVisibleCursor(page, x, y);
  await locator.click({ timeout });
  await moveVisibleCursor(page, x, y, { click: true });
}

async function fillFirst(page, locators, value, timeout = 5000) {
  for (const locator of locators) {
    const item = typeof locator === "string" ? page.locator(locator).first() : locator.first();
    if (await item.waitFor({ state: "visible", timeout }).then(() => true).catch(() => false)) {
      const box = await item.boundingBox().catch(() => null);
      if (box) {
        await moveVisibleCursor(page, box.x + Math.min(24, box.width / 2), box.y + box.height / 2);
      }
      if (!(await item.fill(value, { timeout }).then(() => true).catch(() => false))) {
        continue;
      }
      await wait(120);
      return true;
    }
  }
  throw new Error(`Could not fill input with value: ${value}`);
}

async function clickText(page, text, timeout = 5000) {
  return clickFirst(page, [
    page.getByRole("button", { name: text }),
    page.getByRole("link", { name: text }),
    page.getByText(text, { exact: false }),
  ], timeout);
}

async function chooseDropdownOption(page, triggerText, optionText) {
  await clickFirst(page, [
    page.getByRole("button", { name: new RegExp(triggerText, "i") }),
    page.getByText(new RegExp(triggerText, "i")),
  ]);
  await clickFirst(page, [
    page.getByRole("option", { name: optionText }),
    page.getByRole("button", { name: optionText }),
    page.getByText(optionText, { exact: true }),
  ]);
}

async function clickActiveTimerControl(page, controlType) {
  const target = await page.evaluate((controlType) => {
    const inputs = Array.from(document.querySelectorAll("input"));
    const descriptionInput = inputs.find((input) => /روی چه چیزی|What are you working on/i.test(input.placeholder || ""));
    if (!descriptionInput) return null;

    let container = descriptionInput.parentElement;
    while (container && container !== document.body) {
      const buttons = Array.from(container.querySelectorAll("button"));
      const hasTimerAction = buttons.some((button) => {
        const title = button.getAttribute("title") || button.getAttribute("aria-label") || "";
        return /شروع|توقف|Start|Stop/i.test(title);
      });
      if (buttons.length >= 3 && hasTimerAction) break;
      container = container.parentElement;
    }
    if (!container) return null;

    const buttons = Array.from(container.querySelectorAll("button"));
    let button = null;
    if (controlType === "tags") {
      button = buttons.find((candidate) => candidate.querySelector("svg.lucide-tag"));
    } else {
      button = buttons.find((candidate) => {
        const title = candidate.getAttribute("title") || candidate.getAttribute("aria-label") || "";
        const text = candidate.textContent || "";
        if (/شروع|توقف|حذف|دور ریختن|Billable|Start|Stop|Delete|Discard/i.test(title + text)) return false;
        if (candidate.querySelector("svg.lucide-tag, svg.lucide-dollar-sign, svg.lucide-play, svg.lucide-square, svg.lucide-trash-2")) return false;
        return /پروژه|Project/i.test(title + text) || candidate.querySelector("svg.lucide-chevron-down");
      });
    }

    if (!button) return null;
    const rect = button.getBoundingClientRect();
    return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  }, controlType);

  if (!target) {
    throw new Error(`Could not find active timer ${controlType} control.`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 350);
}

async function clickVisibleTimerMenuButton(page, text) {
  const target = await page.evaluate((text) => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const candidates = Array.from(document.querySelectorAll("button"))
      .filter((button) => isVisible(button) && (button.textContent || "").trim() === text)
      .map((button) => {
        const rect = button.getBoundingClientRect();
        const isCompactMenu = rect.width <= 320 && rect.height <= 52 && rect.x < window.innerWidth * 0.58 && rect.y > 260 && rect.y < window.innerHeight - 40;
        return {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2,
          score: (isCompactMenu ? 0 : 100000) + rect.x + rect.y,
        };
      })
      .sort((a, b) => a.score - b.score);
    return candidates[0] || null;
  }, text);

  if (!target) {
    throw new Error(`Could not find timer dropdown option: ${text}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 350);
}

async function clickActiveTimerStop(page) {
  const stopButton = page.locator("button:visible").filter({ has: page.locator("svg.lucide-square") }).first();
  if (await stopButton.waitFor({ state: "visible", timeout: 5000 }).then(() => true).catch(() => false)) {
    const box = await stopButton.boundingBox();
    if (!box) {
      throw new Error("Could not resolve active timer stop button position.");
    }
    const x = box.x + box.width / 2;
    const y = box.y + box.height / 2;
    await moveVisibleCursor(page, x, y);
    await page.mouse.click(x, y);
    await moveVisibleCursor(page, x, y, { click: true });
    await settle(page, 700);
    return;
  }

  const target = await page.evaluate(() => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const candidates = Array.from(document.querySelectorAll("button"))
      .filter((button) => {
        if (!isVisible(button)) return false;
        const label = [button.getAttribute("title"), button.getAttribute("aria-label"), button.textContent].filter(Boolean).join(" ");
        return /Stop|توقف/.test(label) || button.querySelector("svg.lucide-square");
      })
      .map((button) => {
        const rect = button.getBoundingClientRect();
        const isActiveTimerRow = rect.y > 180 && rect.y < 300 && rect.width >= 40 && rect.height >= 40;
        return {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2,
          score: isActiveTimerRow ? 0 : 100000 + rect.y,
        };
      })
      .sort((a, b) => a.score - b.score);
    return candidates[0] || null;
  });

  if (!target) {
    throw new Error("Could not find active timer stop button.");
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 700);
}

async function selectActiveTimerProject(page, projectName) {
  await clickActiveTimerControl(page, "project");
  await clickVisibleTimerMenuButton(page, projectName);
}

async function selectActiveTimerTags(page, tagNames) {
  await clickActiveTimerControl(page, "tags");
  for (const tagName of tagNames) {
    await clickVisibleTimerMenuButton(page, tagName);
  }
  await moveVisibleCursor(page, 720, 180);
  await page.mouse.click(720, 180);
  await moveVisibleCursor(page, 720, 180, { click: true });
  await settle(page, 220);
}

async function clickCardAction(page, cardText, actionTitles) {
  const target = await page.evaluate(({ cardText, actionTitles }) => {
    const actionSvgNames = actionTitles.some((value) => /delete|حذف/i.test(value))
      ? ["svg.lucide-trash-2", "svg.lucide-trash"]
      : ["svg.lucide-pencil", "svg.lucide-edit-2", "svg.lucide-edit"];
    const matchesTitle = (button) => {
      const title = button.getAttribute("title") || button.getAttribute("aria-label") || button.textContent || "";
      return actionTitles.some((value) => title.includes(value)) || actionSvgNames.some((selector) => button.querySelector(selector));
    };

    const cards = Array.from(document.querySelectorAll("div, article, section"))
      .filter((node) => {
        const text = node.textContent || "";
        if (!text.includes(cardText)) return false;
        const buttons = node.querySelectorAll("button");
        if (!buttons.length) return false;
        const rect = node.getBoundingClientRect();
        return rect.width > 120 && rect.height > 80;
      })
      .sort((a, b) => {
        const rectA = a.getBoundingClientRect();
        const rectB = b.getBoundingClientRect();
        return rectA.width * rectA.height - rectB.width * rectB.height;
      });

    for (const card of cards) {
      const button = Array.from(card.querySelectorAll("button")).find(matchesTitle);
      if (button) {
        const rect = button.getBoundingClientRect();
        return {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2,
        };
      }
    }
    return null;
  }, { cardText, actionTitles });

  if (!target) {
    throw new Error(`Could not click ${actionTitles.join("/")} for card: ${cardText}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 650);
}

async function clickProjectCardAction(page, projectName, action) {
  const target = await page.evaluate(({ projectName, action }) => {
    const isDelete = action === "delete";
    const iconSelectors = isDelete
      ? ["svg.lucide-trash-2", "svg.lucide-trash2", "svg.lucide-trash"]
      : ["svg.lucide-pencil", "svg.lucide-edit-2", "svg.lucide-edit"];
    const titlePatterns = isDelete ? [/delete/i, /حذف/] : [/edit/i, /ویرایش/];
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const matchesAction = (button) => {
      const label = [
        button.getAttribute("title"),
        button.getAttribute("aria-label"),
        button.textContent,
      ].filter(Boolean).join(" ");
      return titlePatterns.some((pattern) => pattern.test(label))
        || iconSelectors.some((selector) => button.querySelector(selector));
    };

    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const textElements = [];
    while (walker.nextNode()) {
      const node = walker.currentNode;
      if ((node.nodeValue || "").includes(projectName) && node.parentElement) {
        textElements.push(node.parentElement);
      }
    }

    const candidates = [];
    for (const textElement of textElements) {
      let node = textElement;
      for (let depth = 0; node && node !== document.body && depth < 10; depth += 1) {
        const rect = node.getBoundingClientRect();
        if (isVisible(node) && rect.width > 150 && rect.height > 80 && rect.width < window.innerWidth * 0.92) {
          const button = Array.from(node.querySelectorAll("button")).find((candidate) => isVisible(candidate) && matchesAction(candidate));
          if (button) {
            const buttonRect = button.getBoundingClientRect();
            candidates.push({
              x: buttonRect.left + buttonRect.width / 2,
              y: buttonRect.top + buttonRect.height / 2,
              area: rect.width * rect.height,
            });
          }
        }
        node = node.parentElement;
      }
    }
    candidates.sort((a, b) => a.area - b.area);
    return candidates[0] || null;
  }, { projectName, action });

  if (!target) {
    throw new Error(`Could not click ${action} for project: ${projectName}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 650);
}

async function clickProjectRowAction(page, projectName, action) {
  const target = await page.evaluate(({ projectName, action }) => {
    const isDelete = action === "delete";
    const iconSelectors = isDelete
      ? ["svg.lucide-trash-2", "svg.lucide-trash2", "svg.lucide-trash"]
      : ["svg.lucide-pencil", "svg.lucide-edit-2", "svg.lucide-edit"];
    const titlePatterns = isDelete ? [/delete/i, /حذف/] : [/edit/i, /ویرایش/];
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const matchesAction = (button) => {
      const label = [button.getAttribute("title"), button.getAttribute("aria-label"), button.textContent].filter(Boolean).join(" ");
      return titlePatterns.some((pattern) => pattern.test(label))
        || iconSelectors.some((selector) => button.querySelector(selector));
    };

    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const titleRects = [];
    while (walker.nextNode()) {
      const node = walker.currentNode;
      if (!(node.nodeValue || "").includes(projectName) || !node.parentElement) continue;
      const rect = node.parentElement.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0) {
        titleRects.push(rect);
      }
    }
    if (!titleRects.length) return null;

    const buttons = Array.from(document.querySelectorAll("button"))
      .filter((button) => isVisible(button) && matchesAction(button))
      .map((button) => {
        const rect = button.getBoundingClientRect();
        const centerY = rect.top + rect.height / 2;
        const distance = Math.min(...titleRects.map((titleRect) => Math.abs(centerY - (titleRect.top + titleRect.height / 2))));
        return {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2,
          distance,
        };
      })
      .filter((candidate) => candidate.distance < 90)
      .sort((a, b) => a.distance - b.distance);

    return buttons[0] || null;
  }, { projectName, action });

  if (!target) {
    throw new Error(`Could not click ${action} for project row: ${projectName}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 650);
}

async function clickProjectActionByOrder(page, action, index = 0) {
  const waitSelector = action === "delete" ? "svg.lucide-trash-2" : "svg.lucide-pencil";
  await page.locator("button").filter({ has: page.locator(waitSelector) }).first().waitFor({ state: "visible", timeout: 12000 });
  const target = await page.evaluate(({ action, index }) => {
    const isDelete = action === "delete";
    const iconSelectors = isDelete
      ? ["svg.lucide-trash-2", "svg.lucide-trash2", "svg.lucide-trash"]
      : ["svg.lucide-pencil", "svg.lucide-edit-2", "svg.lucide-edit"];
    const titlePatterns = isDelete ? [/delete/i, /حذف/] : [/edit/i, /ویرایش/];
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const matchesAction = (button) => {
      const label = [button.getAttribute("title"), button.getAttribute("aria-label"), button.textContent].filter(Boolean).join(" ");
      return titlePatterns.some((pattern) => pattern.test(label))
        || iconSelectors.some((selector) => button.querySelector(selector));
    };
    const buttons = Array.from(document.querySelectorAll("button"))
      .filter((button) => isVisible(button) && matchesAction(button))
      .map((button) => {
        const rect = button.getBoundingClientRect();
        return {
          x: rect.left + rect.width / 2,
          y: rect.top + rect.height / 2,
          top: rect.top,
          left: rect.left,
        };
      })
      .sort((a, b) => (a.top - b.top) || (b.left - a.left));
    return buttons[index] || null;
  }, { action, index });

  if (!target) {
    throw new Error(`Could not click ${action} project action at index ${index}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 650);
}

async function clickIconNearestText(page, text, iconSelector, label) {
  const target = await page.evaluate(({ text, iconSelector }) => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const textCenters = [];
    while (walker.nextNode()) {
      const node = walker.currentNode;
      if (!(node.nodeValue || "").includes(text) || !node.parentElement || !isVisible(node.parentElement)) continue;
      const rect = node.parentElement.getBoundingClientRect();
      textCenters.push({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
    }
    if (!textCenters.length) return null;
    return Array.from(document.querySelectorAll("button"))
      .filter((button) => isVisible(button) && button.querySelector(iconSelector))
      .map((button) => {
        const rect = button.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        const distance = Math.min(...textCenters.map((center) => Math.abs(center.y - y) + Math.abs(center.x - x) * 0.08));
        return { x, y, distance };
      })
      .sort((a, b) => a.distance - b.distance)[0] || null;
  }, { text, iconSelector });

  if (!target) {
    throw new Error(`Could not click ${label} near text: ${text}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
  await settle(page, 550);
}

async function clickVisibleEnabledSelector(page, selector, label) {
  const target = await page.evaluate((selector) => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    };
    const element = Array.from(document.querySelectorAll(selector)).find((candidate) => {
      return isVisible(candidate) && !candidate.disabled;
    });
    if (!element) return null;
    const rect = element.getBoundingClientRect();
    return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  }, selector);

  if (!target) {
    throw new Error(`Could not click visible enabled element: ${label}`);
  }
  await moveVisibleCursor(page, target.x, target.y);
  await page.mouse.click(target.x, target.y);
  await moveVisibleCursor(page, target.x, target.y, { click: true });
}

async function closeAnyOpenModal(page) {
  const target = await page.evaluate(() => {
    const overlays = Array.from(document.querySelectorAll(".fixed.inset-0"));
    const overlay = overlays.reverse().find((element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    });
    if (!overlay) return null;
    const buttons = Array.from(overlay.querySelectorAll("button"));
    const closeButton = buttons.find((button) => button.querySelector("svg.lucide-x"))
      || buttons.find((button) => /cancel|close/i.test([button.textContent, button.getAttribute("title"), button.getAttribute("aria-label")].filter(Boolean).join(" ")))
      || buttons[0];
    if (!closeButton) return null;
    const rect = closeButton.getBoundingClientRect();
    return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  });
  if (target) {
    await moveVisibleCursor(page, target.x, target.y);
    await page.mouse.click(target.x, target.y);
    await moveVisibleCursor(page, target.x, target.y, { click: true });
  }
  await settle(page, 250);
}

async function deleteFixtureProjectByApiIfNeeded(page, fixture) {
  if (!fixture.project_to_archive_id) return;
  const stillOpen = await page.locator("#delete-project-form").isVisible().catch(() => false);
  if (!stillOpen) return;
  await page.evaluate(async ({ backendUrl, projectId }) => {
    const accessToken = localStorage.getItem("accessToken");
    await fetch(`${backendUrl}/api/projects/${projectId}/`, {
      method: "DELETE",
      headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
    });
  }, { backendUrl: BACKEND_URL, projectId: fixture.project_to_archive_id });
  await closeAnyOpenModal(page);
}

async function clickPagePlusButton(page) {
  const plusButton = page.locator("button:visible").filter({ has: page.locator("svg.lucide-plus") }).first();
  await clickLocatorWithCursor(page, plusButton, 8000);
  await settle(page, 200);
}

async function loginThroughUi(page, fixture) {
  await goto(page, "/", 700);
  await wait(3300);
  await clickFirst(page, [
    page.getByRole("link", { name: /پنل کاربری|ورود|شروع|Dashboard|Login/i }),
    page.getByRole("button", { name: /پنل کاربری|ورود|شروع|Dashboard|Login/i }),
    'a[href="/auth"]',
  ], 4000);
  if (!page.url().includes("/auth")) {
    await goto(page, "/auth", 800);
  }

  await fillFirst(page, ['#login-mobile', 'input[type="tel"]'], fixture.mobile);
  await clickFirst(page, [
    page.getByRole("button", { name: /ادامه|Continue/i }),
    'button[type="submit"]',
  ]);
  await fillFirst(page, ['#login-password', 'input[type="password"]'], fixture.password);
  await clickFirst(page, [
    page.getByRole("button", { name: /ورود|Sign in|Login/i }),
    'button[type="submit"]',
  ]);
  await page.waitForURL(/\/profile|\/timesheet|\/workspaces/, { timeout: 25000 }).catch(() => {});
  await settle(page, 450);
}

async function showProfile(page) {
  await settle(page, 500);
}

async function showTimesheetAndTimerFlow(page, fixture) {
  await clickAppNav(page, "/timesheet", 400);
  await clickFirst(page, [
    page.getByRole("button", { name: /شروع|Start/i }),
    'button[title="شروع"]',
    'button[title="Start"]',
  ]);
  await settle(page, 250);

  await fillFirst(page, [
    page.getByPlaceholder(/روی چه چیزی|What are you working on/i),
    'input[type="text"]',
  ], "ضبط ویدئوی معرفی محصول");
  await selectActiveTimerProject(page, fixture.project_for_timer);
  await selectActiveTimerTags(page, [fixture.tag_one, fixture.tag_two]);
  await wait(5000);
  await clickActiveTimerStop(page);
  await settle(page, 400);
  await wait(3000);
}

async function showWorkspaceOverviewAndEdit(page, fixture) {
  await clickAppNav(page, "/workspaces", 450);
  await clickIconNearestText(page, fixture.workspace_name, "svg.lucide-eye", "workspace view");
  await page.waitForURL(/\/workspaces\/[^/]+(?:$|[?#])/, { timeout: 10000 });
  await settle(page, 450);

  await smoothScrollBy(page, 2600, 13, 170);
  await smoothScrollBy(page, -1700, 9, 150);

  await clickFirst(page, [
    page.getByRole("button", { name: /مدیریت اعضا|Manage members/i }),
    page.locator("button:visible").filter({ has: page.locator("svg.lucide-edit-2") }).first(),
  ], 8000);
  await page.waitForURL(/\/workspaces\/[^/]+\/edit(?:$|[?#])/, { timeout: 10000 });
  await settle(page, 350);

  await fillFirst(page, [
    page.locator("textarea").first(),
  ], "فضای کاری دمو برای نمایش مدیریت اعضا، نرخ‌ها و داده‌های گزارش‌گیری.");
  await clickFirst(page, [
    page.locator('button[type="submit"]').first(),
    page.getByRole("button", { name: /ذخیره|Save/i }),
  ], 5000);
  await settle(page, 400);
}

async function createClient(page) {
  await clickAppNav(page, "/clients", 500);
  await clickFirst(page, [
    'button[title="افزودن مشتری"]',
    'button[title="Add Client"]',
    page.getByRole("button", { name: /افزودن مشتری|Add Client/i }),
    page.locator("button").filter({ has: page.locator("svg.lucide-plus") }),
  ]);
  await fillFirst(page, [
    page.getByPlaceholder(/نام مشتری|Client name/i),
    page.locator("#create-client-form input").nth(1),
  ], "مشتری ویدئویی");
  await fillFirst(page, [
    page.getByPlaceholder(/یادداشت|Notes/i),
    page.locator("#create-client-form textarea"),
  ], "این مشتری در جریان ضبط ویدئو ساخته شده است.");
  await wait(1000);
  await clickFirst(page, [
    page.getByRole("button", { name: /ایجاد|Create/i }),
    '#create-client-form button[type="submit"]',
  ]);
  await settle(page, 300);
}

async function archiveAndDeleteProject(page, fixture) {
  await clickAppNav(page, "/projects", 500);
  await clickFirst(page, [
    page.locator("button:visible").filter({ has: page.locator("svg.lucide-pencil") }).first(),
    page.locator("button:visible").filter({ has: page.locator("svg.lucide-edit-2") }).first(),
  ], 8000);
  await page.locator("#edit-project-form").waitFor({ timeout: 10000 });
  await clickFirst(page, [
    page.locator("button").filter({ has: page.locator("svg.lucide-archive") }),
    page.getByRole("button", { name: /Archive/i }),
  ]);
  await page.locator("#edit-project-form").waitFor({ state: "detached", timeout: 12000 }).catch(() => {});
  await settle(page, 120);

  await clickFirst(page, [
    page.getByRole("switch", { name: /Archived/i }),
    page.locator('button[role="switch"]').first(),
  ]);
  await settle(page, 250);
  await clickFirst(page, [
    page.getByRole("button", { name: /Clear filters/i }),
    page.locator('button[aria-label*="Clear"], button[aria-label*="Ù¾Ø§Ú©"]').first(),
  ], 3000).catch(() => {});
  await settle(page, 350);
  return;
  await clickIconNearestText(page, fixture.project_to_archive, "svg.lucide-trash-2", "project delete");
  await page.locator("#delete-project-form input").waitFor({ timeout: 10000 });
  await fillFirst(page, [
    page.locator("#delete-project-form input"),
  ], fixture.project_to_archive);
  await page.waitForFunction(() => {
    const button = document.querySelector('button[form="delete-project-form"][type="submit"]');
    return button && !button.disabled;
  }, { timeout: 5000 });
  const deleteResponse = page.waitForResponse(
    (response) => response.request().method() === "DELETE" && response.url().includes("/api/projects/"),
    { timeout: 15000 },
  );
  await clickVisibleEnabledSelector(page, 'button[form="delete-project-form"][type="submit"]', "project delete submit");
  await deleteResponse.catch(() => null);
  await page.locator("#delete-project-form").waitFor({ state: "detached", timeout: 2500 }).catch(() => {});
  await deleteFixtureProjectByApiIfNeeded(page, fixture);
  await closeAnyOpenModal(page);
  await settle(page, 500);
  await clickFirst(page, [
    page.getByRole("button", { name: /Clear filters/i }),
    page.locator('button[aria-label*="Clear"], button[aria-label*="پاک"]').first(),
  ], 3000).catch(() => {});
  await settle(page, 350);
  return;
  await clickCardAction(page, fixture.project_to_archive, ["ویرایش", "Edit"]);
  await clickFirst(page, [
    page.getByRole("button", { name: /آرشیو|Archive/i }),
  ]);
  await settle(page, 900);

  await clickFirst(page, [
    page.getByRole("switch", { name: /آرشیو|Archived/i }),
    page.locator('button[role="switch"]').first(),
  ]);
  await settle(page, 900);

  await page.getByText(fixture.project_to_archive).first().waitFor({ timeout: 10000 });
  await clickCardAction(page, fixture.project_to_archive, ["حذف", "Delete"]);
  await page.locator("#delete-project-form input").waitFor({ timeout: 10000 });
  await fillFirst(page, [
    page.locator('#delete-project-form input'),
  ], fixture.project_to_archive);
  await clickFirst(page, [
    page.getByRole("button", { name: /حذف|Delete/i }),
  ]);
  await settle(page, 900);

  await clickFirst(page, [
    page.getByRole("button", { name: /پاک کردن فیلتر|Clear filters/i }),
    page.locator('button[aria-label="پاک کردن فیلترها"], button[aria-label="Clear filters"]').first(),
  ]);
  await settle(page, 700);
}

async function createTag(page) {
  await clickAppNav(page, "/tags", 500);
  await clickPagePlusButton(page);
  await page.locator("#tag-form").waitFor({ state: "visible", timeout: 8000 });
  await fillFirst(page, [
    page.locator("#tag-form input").first(),
  ], "ویدئو");
  await wait(1000);
  await clickFirst(page, [
    page.locator('button[form="tag-form"][type="submit"]:not([disabled])'),
    page.getByRole("button", { name: /ایجاد|Create/i }),
  ], 5000);
  await page.locator("#tag-form").waitFor({ state: "detached", timeout: 8000 }).catch(() => {});
  await settle(page, 300);
}

async function waitForReportDetailModalLoaded(page, timeout = 12000) {
  await page.waitForFunction(() => {
    const overlays = Array.from(document.querySelectorAll(".fixed.inset-0"));
    const overlay = overlays.reverse().find((element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
    });
    if (!overlay) return false;

    const text = overlay.innerText || "";
    const loadingNodes = overlay.querySelectorAll('[class*="animate-spin"], [class*="animate-pulse"], [aria-busy="true"]').length;
    const hasLoadingText = /Loading|در حال|بارگذاری|لطفا صبر/i.test(text);
    return text.trim().length > 120 && loadingNodes === 0 && !hasLoadingText;
  }, { timeout }).catch(() => {});
}

async function showReportsAndExports(page) {
  await clickAppNav(page, "/reports", 900);
  await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight * 0.45, behavior: "smooth" }));
  await wait(400);
  await clickFirst(page, [
    page.getByRole("button", { name: /جدول|Table/i }),
  ]);
  await settle(page, 650);
  await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight * 0.35, behavior: "smooth" }));
  await wait(350);
  await clickFirst(page, [
    page.getByRole("button", { name: /جزئیات|Details|مشاهده/i }),
    page.locator("button").filter({ has: page.locator("svg.lucide-eye") }),
  ], 3000).catch(() => {});
  await waitForReportDetailModalLoaded(page);
  await wait(2200);
  await closeAnyOpenModal(page);
  await settle(page, 250);

  await clickFirst(page, [page.getByRole("button", { name: /PDF|پی‌دی‌اف/i })]);
  await waitForNotificationText(page, /PDF|گزارش|آماده|خروجی/i, 90000);
  await clickFirst(page, [page.getByRole("button", { name: /Excel|اکسل/i })]);
  await waitForNotificationText(page, /EXCEL|Excel|گزارش|آماده|خروجی/i, 90000);

  await clickFirst(page, [
    page.getByRole("button", { name: /اعلان|Notifications/i }),
    page.locator('button[aria-label*="اعلان"], button[aria-label*="notification" i]').first(),
  ]);
  await settle(page, 450);
  await clickFirst(page, [
    page.getByRole("link", { name: /همه اعلان‌ها|View all notifications/i }),
    page.getByText(/مشاهده همه|View all/i),
  ]);
  await settle(page, 500);
  const downloadPromise = page.waitForEvent("download", { timeout: 15000 }).catch(() => null);
  await clickFirst(page, [
    page.getByText(/آماده دانلود|ready/i).first(),
    page.locator("button").filter({ hasText: /PDF|Excel|گزارش|report/i }).first(),
  ]);
  const download = await downloadPromise;
  if (download) {
    await download.saveAs(path.join(DOWNLOAD_DIR, await download.suggestedFilename()));
  }
  await wait(400);
}

async function waitForNotificationText(page, pattern, timeout) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    const text = await page.locator("body").innerText().catch(() => "");
    if (pattern.test(text)) {
      await wait(700);
      return;
    }
    await wait(1000);
  }
  throw new Error(`Timed out waiting for export notification: ${pattern}`);
}

async function showLogsAndLogout(page) {
  await clickAppNav(page, "/logs", 700);
  for (let i = 0; i < 3; i += 1) {
    await page.mouse.wheel(0, 1800);
    await settle(page, 250);
    const loadMore = page.getByRole("button", { name: /بارگذاری بیشتر|Load more/i });
    if (await loadMore.isVisible().catch(() => false)) {
      await clickLocatorWithCursor(page, loadMore, 5000);
      await settle(page, 350);
    }
  }
  await clickFirst(page, [
    page.locator("button").filter({ hasText: /ایجاد|ویرایش|حذف|Create|Update|Delete/i }).first(),
    page.locator("button").nth(5),
  ], 4000).catch(() => {});
  await wait(500);
  await clickFirst(page, [
    page.getByRole("button", { name: /خروج|Logout/i }),
    page.locator('button[title="خروج"], button[title="Logout"]').first(),
    page.locator("button").filter({ hasText: /خروج|Logout/i }).first(),
  ], 5000);
  await wait(300);
}

async function runStep(label, action) {
  const startedAt = Date.now();
  console.log(`[video] ${label}`);
  await action();
  console.log(`[video] ${label} done in ${Math.round((Date.now() - startedAt) / 1000)}s`);
}

async function runVideoFlow(fixture) {
  ensureDir(VIDEO_DIR);
  ensureDir(DOWNLOAD_DIR);
  fs.rmSync(DOWNLOAD_DIR, { recursive: true, force: true });
  ensureDir(DOWNLOAD_DIR);

  const browser = await createBrowser();
  const tempVideoDir = fs.mkdtempSync(path.join(os.tmpdir(), "qlockify-video-"));
  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    colorScheme: "dark",
    locale: "fa-IR",
    timezoneId: "Asia/Tehran",
    acceptDownloads: true,
    recordVideo: { dir: tempVideoDir, size: { width: 1440, height: 900 } },
  });
  await context.addInitScript(() => {
    localStorage.setItem("language", "fa");
    localStorage.setItem("theme", "dark");
  });
  const page = await context.newPage();
  await ensureVisibleCursor(page).catch(() => {});

  try {
    await runStep("login", () => loginThroughUi(page, fixture));
    await runStep("profile", () => showProfile(page));
    await runStep("timesheet", () => showTimesheetAndTimerFlow(page, fixture));
    await runStep("workspace overview", () => showWorkspaceOverviewAndEdit(page, fixture));
    await runStep("clients", () => createClient(page));
    await runStep("projects", () => archiveAndDeleteProject(page, fixture));
    await runStep("tags", () => createTag(page));
    await runStep("reports and exports", () => showReportsAndExports(page));
    await runStep("logs and logout", () => showLogsAndLogout(page));
  } finally {
    await context.close();
    await browser.close();
  }

  const videoPath = await page.video().path();
  ensureDir(path.dirname(VIDEO_OUTPUT));
  fs.copyFileSync(videoPath, VIDEO_OUTPUT);
  console.log(`saved ${VIDEO_OUTPUT}`);
}

async function main() {
  await assertReachable(`${FRONTEND_URL}/`, "Frontend");
  await assertReachable(`${BACKEND_URL}/api/`, "Backend");
  const fixture = seedVideoFixture();
  await runVideoFlow(fixture);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
