import { Locator, Page } from "@playwright/test";
import { test, expect } from "../../src/fixture/base-test";
import { TIMEOUT } from "../../src/constant/timeout";
import { PW } from "../../src/utils/PW";
import {
  openBoHoSo,
  openTabCauTrucHoSo,
  openTabThongTin,
  openTaoMoiDropdown,
  openRowActionDropdown,
  getActiveTabPane,
  getCauTrucRow,
  getRowActionTrigger,
  getVisibleDropdown,
  createThuMuc,
  openModalThemThuMuc,
  getIndexThuMuc,
  openManCapNhatThuMuc,
  getThuMucModal,
  openTabThuMuc,
  datQuyenDocLap,
  checkBadgeTabPhanQuyen,
  expectPickerPhanQuyenChiDoc,
  themQuyenTrongModalThuMuc,
  demChipKhoiQuyenThuMuc,
  xoaChipDauTienKhoiQuyenThuMuc,
  luuModalThuMuc,
  getNguoiTrongOQuyen,
  doiThuMucCapCha,
  getSelectCapCha,
  docGiaTriSelectThuMuc,
  layNguoiTrongKhoiQuyenThuMuc,
  THU_MUC_FIELD,
  FOLDER_PICKER,
  FOLDER_PICKER_LABELS,
  openPhanQuyenNangCao,
  getCodeCuaItemPhanQuyen,
  checkNguoiTrongOQuyen,
  checkQuyenGiongItemCha,
  checkKeThua,
  moItemPhanQuyen,
  ITEM_PREFIX,
  PQ_COL,
  expectToastThanhCong,
  chonOptionSelect,
  pickFirstOption,
  layDanhSachOptionSelectThuMuc,
  dongModalThuMucNeuDangMo,
} from "../../src/screen-instructions/KHO-TAI-LIEU.functions";
import { DuLieu, DuLieu177, DuLieu178, DuLieu5Quyen } from "./1.4.1.setup";

/**
 * Thao tác + assert dùng chung cho bộ case **1.4.1 — Tạo thư mục trong Cấu trúc hồ sơ** (UC22).
 *
 * Bộ case này có 2 nhánh đối xứng nhau:
 * - Nhánh **có quyền** (case 153, 154): nút "Tạo mới" + 2 mục con, và mục "Thêm thư mục" ở
 *   menu mở rộng của dòng thư mục.
 * - Nhánh **không có quyền** (case 155, 156): đúng những element trên **không hiển thị**.
 *
 * Tiền điều kiện chung của mục: xem `1.4.1.md`.
 */

/** Bước 1 của mọi case: mở màn chi tiết BHS → tab "Cấu trúc hồ sơ" */
export const moTabCauTrucHoSo = async (userPage: Page, duLieu: DuLieu) => {
  await test.step("1. Truy cập màn chi tiết BHS → tab Cấu trúc hồ sơ", async () => {
    await openBoHoSo(userPage, duLieu.recordUrl);
    await openTabCauTrucHoSo(userPage);
  });
};

/**
 * Nhánh CÓ quyền — top menu: nút "Tạo mới" hiển thị và dropdown của nó có 2 mục con
 * **Thư mục** + **Tài liệu**.
 *
 * ⚠️ Dropdown thực tế còn mục thứ 3 "Tạo hồ sơ liên quan" (đã khảo sát, `KHO-TAI-LIEU.md` mục 4.4)
 * → chỉ assert **có đủ 2 mục QA yêu cầu**, không assert "đúng 2 mục".
 */
export const expectCoNutTaoMoiVoi2MucCon = async (userPage: Page) => {
  await test.step('2. Top menu hiển thị nút "Tạo mới" với 2 button con: Thư mục, Tài liệu', async () => {
    await expect(getActiveTabPane(userPage).getByTestId("btn-tao-moi"), {
      message: 'Lỗi: không thấy nút "Tạo mới" trên toolbar tab Cấu trúc hồ sơ',
    }).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });

    const dropdown = await openTaoMoiDropdown(userPage);

    const mucThuMuc = dropdown.getByTestId("mni-them-thu-muc");
    await expect(mucThuMuc, {
      message: 'Lỗi: dropdown "Tạo mới" không có mục "Thư mục"',
    }).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });
    await expect(mucThuMuc, {
      message: 'Lỗi: mục con thứ nhất của "Tạo mới" không phải "Thư mục"',
    }).toContainText(/thư mục/i);

    const mucTaiLieu = dropdown.getByTestId("mni-them-tai-lieu");
    await expect(mucTaiLieu, {
      message: 'Lỗi: dropdown "Tạo mới" không có mục "Tài liệu"',
    }).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });
    await expect(mucTaiLieu, {
      message: 'Lỗi: mục con thứ hai của "Tạo mới" không phải "Tài liệu"',
    }).toContainText(/tài liệu/i);

    await roiChuot(userPage);
  });
};

/**
 * Nhánh KHÔNG có quyền — top menu **không** hiển thị nút "Tạo mới" (nên cũng không có 2 mục con
 * Thư mục / Tài liệu).
 *
 * Dùng `toBeHidden()` chứ không phải `toHaveCount(0)`: `toBeHidden` đạt cả khi element **không tồn
 * tại** lẫn khi có trong DOM nhưng bị ẩn — đúng với mong muốn "không hiển thị" của QA.
 * (Với vai chỉ có quyền Xem, khảo sát cho thấy `btn-tao-moi` không tồn tại —
 * `KHO-TAI-LIEU.PHAN-QUYEN-THEO-VAI.md` mục 2.)
 */
export const expectKhongCoNutTaoMoi = async (userPage: Page) => {
  await test.step('2. Top menu KHÔNG hiển thị nút "Tạo mới" (Thư mục / Tài liệu)', async () => {
    await expect(getActiveTabPane(userPage).getByTestId("btn-tao-moi"), {
      message:
        'Lỗi: vẫn thấy nút "Tạo mới" trên toolbar tab Cấu trúc hồ sơ dù vai không có quyền tạo mới',
    }).toBeHidden({ timeout: TIMEOUT.CONTROL_LOADING });

    // Không có nút "Tạo mới" thì không có dropdown để mở → không cần kiểm tra 2 mục con.
    // Nếu nút vẫn hiện (khác kỳ vọng QA), assert phía trên đã fail và báo rõ lý do.
  });
};

/** Nhánh CÓ quyền — menu mở rộng của dòng thư mục có mục "Thêm thư mục" */
export const expectCoNutThemThuMucODong = async (
  userPage: Page,
  tenThuMuc: string,
) => {
  await test.step(`2. Menu mở rộng của dòng thư mục "${tenThuMuc}" có button "Thêm thư mục"`, async () => {
    const dropdown = await openRowActionDropdown(userPage, tenThuMuc);

    const mucThemThuMuc = dropdown.getByTestId("mni-them-thu-muc");
    await expect(mucThemThuMuc, {
      message: `Lỗi: menu mở rộng của dòng thư mục "${tenThuMuc}" không có mục "Thêm thư mục"`,
    }).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });
    await expect(mucThemThuMuc, {
      message: `Lỗi: mục "Thêm thư mục" ở menu dòng "${tenThuMuc}" sai nhãn`,
    }).toContainText(/thêm\s*thư mục/i);

    await roiChuot(userPage);
  });
};

/**
 * Nhánh KHÔNG có quyền — menu mở rộng của dòng thư mục **không** có mục "Thêm thư mục".
 *
 * Dòng thư mục vẫn phải **nhìn thấy được** (vai có quyền Xem thì thấy dòng, chỉ không được tạo).
 * Vai không có quyền tạo có thể **không có luôn nút mở menu** `btn-more-action-<id>`, hoặc có nút
 * nhưng menu rỗng/thiếu mục → cả 2 trường hợp đều thoả mong muốn của QA, nên hàm này xử lý cả hai
 * thay vì bắt buộc menu phải mở được. (Hành vi menu hành động của dòng với vai không phải Owner
 * BHS hiện **chưa được khảo sát** — `KHO-TAI-LIEU.PHAN-QUYEN-THEO-VAI.md` mục 5.)
 */
export const expectKhongCoNutThemThuMucODong = async (
  userPage: Page,
  tenThuMuc: string,
) => {
  await test.step(`2. Menu mở rộng của dòng thư mục "${tenThuMuc}" KHÔNG có button "Thêm thư mục"`, async () => {
    const row = getCauTrucRow(userPage, tenThuMuc);
    await expect(row, {
      message: `Lỗi: không thấy dòng thư mục "${tenThuMuc}" trong bảng Cấu trúc hồ sơ (vai phải xem được dòng này)`,
    }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });

    const trigger = getRowActionTrigger(userPage, tenThuMuc);
    if (!(await trigger.isVisible().catch(() => false))) {
      // Không có nút mở menu → chắc chắn không có button "Thêm thư mục"
      return;
    }

    await row.hover();
    await userPage.waitForTimeout(1000);
    await trigger.hover();
    await userPage.waitForTimeout(2500); // dropdown mount

    const dropdown = getVisibleDropdown(userPage);
    if (await dropdown.isVisible().catch(() => false)) {
      await expect(dropdown.getByTestId("mni-them-thu-muc"), {
        message: `Lỗi: menu mở rộng của dòng "${tenThuMuc}" VẪN có mục "Thêm thư mục" dù vai không có quyền tạo mới ở thư mục này`,
      }).toBeHidden({ timeout: TIMEOUT.CONTROL_LOADING });
    }

    await roiChuot(userPage);
  });
};

/* -------------------------------------------------------------------------- */
/* Tạo thư mục thành công (case 163…)                                          */
/* -------------------------------------------------------------------------- */

/**
 * Thao tác 1–3 của QA: tab "Cấu trúc hồ sơ" → **Thêm Thư mục** → nhập đầy đủ trường bắt buộc
 * (Tên + Đơn vị soạn thảo) → **Xác nhận**.
 *
 * Toast `"Thành công"` được assert ngay trong `createThuMuc` (→ `luuModalThuMuc`), nên hàm này
 * đã phủ luôn mong muốn "Thông báo Thành công".
 */
export const taoThuMucDayDuThongTin = async (
  creatorPage: Page,
  duLieu: DuLieu,
  tenThuMuc: string,
  opts: { parentName?: string } = {},
) => {
  const moTaViTri = opts.parentName
    ? ` bên trong thư mục "${opts.parentName}"`
    : " ở gốc Bộ hồ sơ";

  await test.step(`1–3. Thêm Thư mục "${tenThuMuc}"${moTaViTri} → nhập đủ trường bắt buộc → Xác nhận (chờ toast "Thành công")`, async () => {
    await createThuMuc(
      creatorPage,
      new PW(creatorPage),
      duLieu.recordUrl,
      tenThuMuc,
      { parentName: opts.parentName },
    );
  });
};

/**
 * Mong muốn: thư mục **hiển thị tại vị trí tương ứng** trong cấu trúc hồ sơ.
 *
 * - Không truyền `tenCha` → thư mục ở gốc BHS: dòng phải là `ant-table-row-level-0`.
 * - Có `tenCha` → thư mục con: dòng phải là `ant-table-row-level-1` và **nằm ngay dưới** dòng cha
 *   (cây ở tab này expand sẵn nên so sánh được theo thứ tự dòng).
 */
export const expectThuMucHienThiDungViTri = async (
  page: Page,
  duLieu: DuLieu,
  tenThuMuc: string,
  opts: { tenCha?: string } = {},
) => {
  const cap = opts.tenCha ? 1 : 0;
  const moTa = opts.tenCha
    ? `ngay dưới thư mục cha "${opts.tenCha}" (cấp ${cap})`
    : `ở gốc Bộ hồ sơ (cấp ${cap})`;

  await test.step(`Mong muốn: thư mục "${tenThuMuc}" hiển thị ${moTa}`, async () => {
    await openBoHoSo(page, duLieu.recordUrl);
    await openTabCauTrucHoSo(page);

    const row = getCauTrucRow(page, tenThuMuc);
    await expect(row, {
      message: `Lỗi: không thấy thư mục "${tenThuMuc}" trong bảng Cấu trúc hồ sơ sau khi tạo`,
    }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });

    // Tên hiển thị có dạng "<mã prefix>. <tên>" (vd "A. AT-UC22-…")
    await expect(
      row.locator('[data-testid^="lnk-ten-ho-so-tai-lieu-"]').first(),
      {
        message: `Lỗi: dòng của thư mục "${tenThuMuc}" không hiển thị đúng tên`,
      },
    ).toContainText(tenThuMuc, { timeout: TIMEOUT.CONTROL_LOADING });

    await expect(row, {
      message: `Lỗi: thư mục "${tenThuMuc}" không nằm ở cấp ${cap} của cây cấu trúc`,
    }).toHaveClass(new RegExp(`ant-table-row-level-${cap}`));

    if (opts.tenCha) {
      const tenCacDong = await getActiveTabPane(page)
        .locator('[data-testid^="lnk-ten-ho-so-tai-lieu-"]')
        .allInnerTexts();
      const iCha = tenCacDong.findIndex((t) => t.includes(opts.tenCha!));
      const iCon = tenCacDong.findIndex((t) => t.includes(tenThuMuc));
      expect(iCon, {
        message:
          `Lỗi: thư mục con "${tenThuMuc}" không nằm ngay dưới cha "${opts.tenCha}". ` +
          `Thứ tự dòng đang là: [${tenCacDong.map((t) => t.trim()).join(" | ")}]`,
      }).toBe(iCha + 1);
    }
  });
};

/**
 * Mong muốn: **thuộc tính thư mục được lưu lại** — mở lại màn "Cập nhật thư mục" và kiểm tra:
 *
 * - **Tên** đúng bằng tên đã nhập (đây là dữ liệu case tự đặt nên assert chính xác được).
 * - Trường bắt buộc còn lại (**Đơn vị soạn thảo**) **có giá trị**, không assert khớp từng chuỗi —
 *   giá trị do hàm tạo tự chọn (option đầu) và nhãn hiển thị của tree-select có thể khác chuỗi lúc
 *   chọn, nên chỉ cần "đã lưu, không rỗng" là đủ cho mong muốn của QA.
 *
 * 🚨 `openManCapNhatThuMuc` đổi `page.url()` sang `itemId` của thư mục → bước sau phải mở lại BHS
 * bằng `duLieu.recordUrl` (các hàm `openBoHoSo` / `openPhanQuyenNangCao` đều tự làm).
 */
export const expectThuocTinhThuMucDaLuu = async (
  page: Page,
  duLieu: DuLieu,
  tenThuMuc: string,
) => {
  await test.step(`Mong muốn: thuộc tính thư mục "${tenThuMuc}" được lưu lại`, async () => {
    await openManCapNhatThuMuc(page, duLieu.recordUrl, tenThuMuc);
    const modal = getThuMucModal(page);

    await expect(modal.getByTestId(THU_MUC_FIELD.TEN), {
      message: `Lỗi: tên thư mục lưu lại không đúng (mong đợi "${tenThuMuc}")`,
    }).toHaveValue(tenThuMuc, { timeout: TIMEOUT.CONTROL_LOADING });

    const donViSoanThao = modal.getByTestId(THU_MUC_FIELD.DON_VI_SOAN_THAO);
    if (await donViSoanThao.isVisible().catch(() => false)) {
      await expect(donViSoanThao.locator(".ant-select-selection-item"), {
        message: `Lỗi: "Đơn vị soạn thảo" của thư mục "${tenThuMuc}" không có giá trị sau khi lưu`,
      }).not.toBeEmpty({ timeout: TIMEOUT.CONTROL_LOADING });
    }
  });
};

/**
 * Mong muốn: **người tạo thư mục mặc định là Owner** của thư mục vừa tạo — đọc từ modal
 * "Phân quyền nâng cao" (nơi duy nhất xem được quyền sau khi tạo).
 *
 * @param tenNguoiTao **tên hiển thị** của người tạo, lấy động bằng
 *   `layTenHienThiNguoiDangDangNhap(creatorPage)` — không hard-code tên vào code vì tên user có thể
 *   bị đổi trong lúc test.
 *
 * ⚠️ Truyền `checkerPage` là vai **Owner BHS** (thường là Thủ thư dựng dữ liệu): vai khác có thể
 * không có `btn-more` để mở modal này (`KHO-TAI-LIEU.PHAN-QUYEN-THEO-VAI.md` mục 2).
 */
export const expectNguoiTaoLaOwnerThuMuc = async (
  checkerPage: Page,
  duLieu: DuLieu,
  tenThuMuc: string,
  tenNguoiTao: string,
) => {
  await test.step(`Mong muốn: người tạo "${tenNguoiTao}" là Owner của thư mục "${tenThuMuc}"`, async () => {
    await openPhanQuyenNangCao(checkerPage, duLieu.recordUrl);
    const code = await getCodeCuaItemPhanQuyen(checkerPage, tenThuMuc);
    await checkNguoiTrongOQuyen(
      checkerPage,
      code,
      PQ_COL.OWNER,
      tenNguoiTao,
      tenThuMuc,
    );
  });
};

/* -------------------------------------------------------------------------- */
/* Trường "Index thư mục" trong modal Thêm thư mục (case 166…)                  */
/* -------------------------------------------------------------------------- */

/**
 * Mong muốn của case 166: mở modal "Thêm thư mục" và quan sát trường **Index thư mục** —
 * hệ thống **tự sinh** giá trị `indexMongDoi`, và **không cho phép chỉnh sửa**.
 *
 * @param indexMongDoi mã prefix mong đợi (`"A"`, `"B"`…) — thứ tự tạo thư mục trong cấu trúc hồ sơ
 * @param opts.parentName mở modal từ menu của 1 thư mục đã có (tạo thư mục con)
 */
export const expectIndexThuMucTuSinh = async (
  page: Page,
  duLieu: DuLieu,
  indexMongDoi: string,
  opts: { parentName?: string } = {},
) => {
  const moTaViTri = opts.parentName
    ? ` (mở từ menu thư mục "${opts.parentName}")`
    : " ở gốc Bộ hồ sơ";

  await test.step(`1–2. Mở màn "Thêm thư mục"${moTaViTri} → trường Index tự sinh = "${indexMongDoi}" và không sửa được`, async () => {
    const modal = await openModalThemThuMuc(page, duLieu.recordUrl, {
      parentName: opts.parentName,
    });
    const index = getIndexThuMuc(page, modal);

    await expect(index, {
      message: 'Lỗi: không thấy trường Index trong ô "Tên" của modal Thêm thư mục',
    }).toBeVisible({ timeout: TIMEOUT.CONTROL_LOADING });

    // Hệ thống tự sinh + hiển thị bằng chữ cái theo thứ tự tạo thư mục
    await expect(index, {
      message: `Lỗi: Index thư mục không phải "${indexMongDoi}" (hệ thống sinh sai thứ tự?)`,
    }).toHaveValue(indexMongDoi, { timeout: TIMEOUT.VALIDATE_WAITING });

    // Không cho phép chỉnh sửa
    await expect(index, {
      message: "Lỗi: trường Index thư mục KHÔNG bị khoá — người dùng sửa được",
    }).toBeDisabled({ timeout: TIMEOUT.CONTROL_LOADING });
  });
};

/* -------------------------------------------------------------------------- */
/* Đổi "Thư mục/HS cấp cha" trong modal Thêm thư mục (case 177…)                */
/* -------------------------------------------------------------------------- */

/** Thông tin cụ thể của 1 thư mục cha "mẫu" mà case 177 sẽ so sánh */
export type ThuMucChaQuanSat = {
  ten: string;
  doMat: string;
  trangThaiHienThi: string;
  /** Tên hiển thị của người được gán vào TẤT CẢ 5 trường quyền của thư mục cha này (lấy động) */
  tenNguoiCoQuyen: string;
};

/**
 * Case 177 (viết lại 2026-07-30): mở modal "Thêm thư mục" cho thư mục thứ 3 ở **gốc BHS**, bấm
 * **"Đặt quyền độc lập"** (quyền riêng) TRƯỚC khi chọn cấp cha, rồi lần lượt chọn cấp cha =
 * `cha1` / `cha2` và kiểm tra Độ mật, Trạng thái hiển thị, cả 5 khối quyền đổi đúng theo cha
 * đang chọn ở mỗi lần.
 *
 * ⚠️ **Chưa khảo sát bằng MCP** tổ hợp "đã Đặt quyền độc lập RỒI mới đổi cấp cha": tài liệu
 * `KHO-TAI-LIEU.MODAL-TAO-THU-MUC.md` mục 3b mới xác nhận hành vi "đổi cấp cha tự cập nhật quyền"
 * khi thư mục còn ở chế độ **Kế thừa** (mặc định lúc mở modal). Nếu chạy thật cho kết quả khác
 * (vd quyền độc lập giữ nguyên, không bị ghi đè theo cấp cha mới), đó là **kết quả test** cần báo
 * QA/DEV, không phải lỗi script (`CLAUDE.md` — "Đọc tiền điều kiện của testcase QA" mục 1).
 */
export const moThuMuc3DatQuyenRiengRoiDoiCapCha = async (
  page: Page,
  duLieu: DuLieu177,
  cha1: ThuMucChaQuanSat,
  cha2: ThuMucChaQuanSat,
) => {
  await test.step('1. Mở modal "Thêm thư mục" ở gốc BHS cho thư mục 3 → đặt quyền riêng (Đặt quyền độc lập)', async () => {
    const modal = await openModalThemThuMuc(page, duLieu.recordUrl);

    await openTabThuMuc(page, "Phân quyền", modal);
    await datQuyenDocLap(page, modal);
    await openTabThuMuc(page, "Thông tin", modal);
  });

  await test.step(`2. Chọn "Thư mục/HS cấp cha" = "${cha1.ten}" → quan sát Độ mật/Trạng thái/quyền giống thư mục 1`, async () => {
    const modal = getThuMucModal(page);
    await doiThuMucCapCha(page, cha1.ten, modal);
    await expectDoMatTrangThaiVaQuyenGiongCha(page, modal, cha1);
  });

  await test.step(`3. Đổi "Thư mục/HS cấp cha" = "${cha2.ten}" → quan sát Độ mật/Trạng thái/quyền giống thư mục 2`, async () => {
    const modal = getThuMucModal(page);
    await doiThuMucCapCha(page, cha2.ten, modal);
    await expectDoMatTrangThaiVaQuyenGiongCha(page, modal, cha2);
  });
};

/** Assert Độ mật, Trạng thái hiển thị và cả 5 khối quyền của modal đang mở giống `cha` truyền vào */
const expectDoMatTrangThaiVaQuyenGiongCha = async (
  page: Page,
  modal: Locator,
  cha: ThuMucChaQuanSat,
) => {
  await expect(getSelectCapCha(page, modal), {
    message: `Lỗi: ô "Thư mục/HS cấp cha" không đổi sang "${cha.ten}"`,
  }).toContainText(cha.ten, { timeout: TIMEOUT.DATA_LOADING });

  expect(await docGiaTriSelectThuMuc(modal, THU_MUC_FIELD.DO_MAT), {
    message: `Lỗi: "Độ mật" không giống thư mục cha "${cha.ten}" (mong đợi "${cha.doMat}")`,
  }).toBe(cha.doMat);
  expect(
    await docGiaTriSelectThuMuc(modal, THU_MUC_FIELD.TRANG_THAI_HIEN_THI),
    {
      message: `Lỗi: "Trạng thái hiển thị" không giống thư mục cha "${cha.ten}" (mong đợi "${cha.trangThaiHienThi}")`,
    },
  ).toBe(cha.trangThaiHienThi);

  await openTabThuMuc(page, "Phân quyền", modal);
  for (const permTestId of Object.values(FOLDER_PICKER)) {
    const nguoi = await layNguoiTrongKhoiQuyenThuMuc(page, permTestId, modal);
    expect(nguoi, {
      message:
        `Lỗi: khối "${FOLDER_PICKER_LABELS[permTestId]}" không giống thư mục cha "${cha.ten}" ` +
        `— thiếu "${cha.tenNguoiCoQuyen}" (đang có: [${nguoi.join(" | ")}])`,
    }).toContain(cha.tenNguoiCoQuyen);
  }
  await openTabThuMuc(page, "Thông tin", modal);
};

/* -------------------------------------------------------------------------- */
/* Tìm kiếm trong ô "Thư mục/HS cấp cha" của modal Thêm thư mục (case 178…)     */
/* -------------------------------------------------------------------------- */

/**
 * Gõ `tenThuMuc` vào ô search của "Thư mục/HS cấp cha" (đang mở) và kiểm tra kết quả tìm kiếm
 * hiển thị **đúng** thư mục đó (option `.ant-select-tree-title` khớp tên nằm trong dropdown).
 *
 * Dùng cùng cách xác định dropdown/option đã được khảo sát MCP cho field này (xem
 * `doiThuMucCapCha` / `KHO-TAI-LIEU.MODAL-TAO-THU-MUC.md` mục 3b), chỉ khác là chỉ **gõ tìm** rồi
 * assert kết quả, không click chọn — vì case 178 chỉ kiểm tra chức năng tìm kiếm, không đổi cấp cha.
 */
const timThuMucCapChaTheoTen = async (
  page: Page,
  modal: Locator,
  tenThuMuc: string,
) => {
  const select = getSelectCapCha(page, modal);
  const searchInput = select.locator(".ant-select-selection-search-input");

  // fill() tự xoá nội dung cũ trước khi gõ mới → không cần clear thủ công giữa 2 lần tìm
  await searchInput.fill(tenThuMuc);
  await page.waitForTimeout(TIMEOUT.DATA_LOADING); // chờ app lọc kết quả

  const dropdown = page.locator(".ant-select-dropdown:visible").last();
  await expect(
    dropdown
      .locator(".ant-select-tree-title")
      .filter({ hasText: tenThuMuc })
      .first(),
    {
      message: `Lỗi: tìm kiếm "${tenThuMuc}" trong ô "Thư mục/HS cấp cha" không ra đúng kết quả`,
    },
  ).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });
};

/**
 * Case 178: mở modal "Thêm thư mục" cho thư mục thứ 3 ở gốc BHS → trong ô "Thư mục/HS cấp cha"
 * tìm kiếm lần lượt theo tên thư mục 1 rồi thư mục 2 → mỗi lần đều phải ra đúng kết quả.
 */
export const moThuMuc3TimKiemCapCha = async (
  page: Page,
  duLieu: DuLieu178,
) => {
  await test.step('1-2. Tạo BHS + 2 thư mục → bấm tạo thư mục thứ 3 → mở ô "Thư mục/HS cấp cha"', async () => {
    const modal = await openModalThemThuMuc(page, duLieu.recordUrl);
    const select = getSelectCapCha(page, modal);
    await select.click();
    await page.waitForTimeout(TIMEOUT.CONTROL_LOADING);
  });

  await test.step(`3a. Tìm kiếm theo tên thư mục 1 "${duLieu.tenThuMuc1}"`, async () => {
    const modal = getThuMucModal(page);
    await timThuMucCapChaTheoTen(page, modal, duLieu.tenThuMuc1);
  });

  await test.step(`3b. Tìm kiếm theo tên thư mục 2 "${duLieu.tenThuMuc2}"`, async () => {
    const modal = getThuMucModal(page);
    await timThuMucCapChaTheoTen(page, modal, duLieu.tenThuMuc2);
  });
};

/**
 * Rời chuột khỏi trigger để dropdown mở bằng **hover** tự đóng — bắt buộc trước khi mở dropdown
 * khác, tránh 2 `.ant-dropdown:visible` cùng lúc làm `getVisibleDropdown` trỏ nhầm.
 */
const roiChuot = async (userPage: Page) => {
  await userPage.mouse.move(0, 0);
  await userPage.waitForTimeout(1000);
};

/* -------------------------------------------------------------------------- */
/* Trường kế thừa từ cấp cha: Độ mật (182), Trạng thái hiển thị (188)          */
/* -------------------------------------------------------------------------- */

/*
 * Case 182 dùng bộ tên riêng cho trường Độ mật (khai báo ở cuối mục này, bọc các hàm generic
 * bên dưới) — spec 182 giữ nguyên, không phải sửa khi thêm case 188.
 */

/**
 * 1 trường của modal thư mục vừa **kế thừa giá trị từ cấp cha**, vừa bị ràng buộc
 * **"con không được lỏng hơn cha"**.
 */
export type TruongKeThua = {
  nhan: string;
  /** testId trong modal Thêm / Cập nhật thư mục */
  testIdThuMuc: string;
  /** testId của trường tương ứng trong form BHS */
  testIdBoHoSo: string;
  /** Các mức theo thứ tự **lỏng → chặt**; con phải có mức ≥ cha */
  mucDo: readonly string[];
};

/** Case 182 — Độ mật: `Thường` (lỏng nhất) < `Mật` < `Tuyệt mật` */
export const TRUONG_DO_MAT: TruongKeThua = {
  nhan: "Độ mật",
  testIdThuMuc: THU_MUC_FIELD.DO_MAT,
  testIdBoHoSo: "sel-securityLevel",
  mucDo: ["Thường", "Mật", "Tuyệt mật"],
};

/** Case 188 — Trạng thái hiển thị: `Public` (lỏng hơn) < `Private` */
export const TRUONG_TRANG_THAI_HIEN_THI: TruongKeThua = {
  nhan: "Trạng thái hiển thị",
  testIdThuMuc: THU_MUC_FIELD.TRANG_THAI_HIEN_THI,
  testIdBoHoSo: "sel-trangThai",
  mucDo: ["Public", "Private"],
};

/** Các mức được phép dùng bên trong 1 cha đang ở mức `mucCha` (con phải ≥ cha) */
export const mucChoPhepTrong = (
  truong: TruongKeThua,
  mucCha: string,
): string[] => {
  const capCha = truong.mucDo.indexOf(mucCha);
  expect(capCha, {
    message: `Lỗi: "${mucCha}" không phải giá trị hợp lệ của trường ${truong.nhan} (${truong.mucDo.join(" | ")})`,
  }).toBeGreaterThanOrEqual(0);
  return truong.mucDo.filter((_, i) => i >= capCha);
};

/**
 * Mở modal "Thêm thư mục" ở **gốc BHS** → assert giá trị mặc định của `truong` (kế thừa từ BHS)
 * → đóng modal bằng nút "Hủy" (không lưu gì).
 */
export const expectGiaTriMacDinhRoiDong = async (
  page: Page,
  recordUrl: string,
  truong: TruongKeThua,
  mongDoi: string,
) => {
  await test.step(
    `Mở modal "Thêm thư mục" ở gốc BHS → ${truong.nhan} mặc định = "${mongDoi}" → đóng modal`,
    async () => {
      const modal = await openModalThemThuMuc(page, recordUrl);
      const giaTri = await docGiaTriSelectThuMuc(modal, truong.testIdThuMuc);
      expect(giaTri, {
        message: `Lỗi: ${truong.nhan} mặc định của modal "Thêm thư mục" không phải "${mongDoi}" (đang là "${giaTri}")`,
      }).toBe(mongDoi);

      await dongModalThuMucNeuDangMo(page);
    },
  );
};

/**
 * Mở lại BHS → tab "Thông tin" → đổi `truong` → Lưu lại.
 *
 * ⚠️ **Chưa khảo sát bằng MCP** toast của thao tác **sửa** BHS qua `btn-save` ở header — tài liệu
 * mới xác nhận toast cho luồng **tạo mới** (`KHO-TAI-LIEU.md` mục 9.8). Script giả định dùng chung.
 */
export const doiGiaTriBoHoSo = async (
  page: Page,
  recordUrl: string,
  truong: TruongKeThua,
  giaTri: string,
) => {
  await test.step(`Chỉnh sửa BHS: đổi ${truong.nhan} thành "${giaTri}" → Lưu lại`, async () => {
    await openBoHoSo(page, recordUrl);
    await openTabThongTin(page);

    const pw = new PW(page);
    await pw.inputDropDownList(truong.testIdBoHoSo, giaTri);
    await pw.clickButton("btn-save");
    await expectToastThanhCong(page, `Đổi ${truong.nhan} BHS thành "${giaTri}"`);
  });
};

export type RangBuocThuMucConOptions = {
  /** Mức hiện tại của cấp cha */
  mucCha: string;
  /** Tên thư mục cha; **bỏ trống** → cấp cha là gốc BHS */
  tenCha?: string;
  /** Tiền tố đặt tên thư mục con sẽ thử tạo (phải unique trong BHS) */
  tenCon: string;
  /** Mặc định `false` — giả định BHS đang mở sẵn ở tab "Cấu trúc hồ sơ" */
  moLaiBoHoSo?: boolean;
};

/**
 * Ràng buộc `truong` của thư mục con theo cấp cha (thư mục cha, hoặc chính gốc BHS).
 *
 * Tiêu chí fail là **lúc lưu**: mức "lỏng" hơn cha mà lưu **thành công** thì lỗi. Việc dropdown
 * có lọc bớt mức lỏng hơn hay không chỉ là cách app chặn sớm → **không** coi là fail, chỉ khiến
 * vòng thử lưu không có gì để chạy.
 *
 * ⚠️ **Chưa khảo sát bằng MCP** ràng buộc này — chưa rõ app chặn ở đâu, hay hiện chưa chặn.
 */
export const expectRangBuocThuMucCon = async (
  page: Page,
  recordUrl: string,
  truong: TruongKeThua,
  opts: RangBuocThuMucConOptions,
) => {
  const { mucCha, tenCha, tenCon, moLaiBoHoSo = false } = opts;
  const moTaCha = tenCha ? `thư mục "${tenCha}"` : "gốc BHS";
  const choPhep = mucChoPhepTrong(truong, mucCha);
  const khongChoPhep = truong.mucDo.filter((d) => !choPhep.includes(d));

  const chonDuoc: string[] = [];

  await test.step(
    `Mở modal tạo thư mục con trong ${moTaCha} (${truong.nhan} "${mucCha}") → đọc các mức chọn được`,
    async () => {
      const modal = await openModalThemThuMuc(page, recordUrl, {
        parentName: tenCha,
        moLaiBoHoSo,
      });
      const pane = modal.locator('div[role="tabpanel"][aria-hidden="false"]');

      chonDuoc.push(
        ...(await layDanhSachOptionSelectThuMuc(
          page,
          pane.getByTestId(truong.testIdThuMuc),
        )),
      );

      // Mức được phép (≥ cha) phải luôn chọn được — thiếu thì app đang chặn quá tay
      for (const muc of choPhep) {
        expect(chonDuoc, {
          message:
            `Lỗi: thư mục con trong ${moTaCha} (${truong.nhan} "${mucCha}") phải chọn được ` +
            `"${muc}" nhưng dropdown chỉ có [${chonDuoc.join(" | ")}]`,
        }).toContain(muc);
      }

      await dongModalThuMucNeuDangMo(page);
    },
  );

  // Mức lỏng hơn cha mà app vẫn cho chọn → phải chặn khi lưu, lưu được là lỗi
  for (const mucCon of khongChoPhep.filter((d) => chonDuoc.includes(d))) {
    const ten = `${tenCon}-Con-${mucCon.replace(/\s+/g, "")}`;

    await test.step(
      `Chọn được ${truong.nhan} "${mucCon}" (lỏng hơn cha) → thử lưu "${ten}", phải KHÔNG lưu được`,
      async () => {
        const modal = await openModalThemThuMuc(page, recordUrl, {
          parentName: tenCha,
          moLaiBoHoSo: false,
        });
        const pane = modal.locator('div[role="tabpanel"][aria-hidden="false"]');

        await pane.getByTestId(THU_MUC_FIELD.TEN).fill(ten);
        await chonOptionSelect(
          page,
          pane.getByTestId(truong.testIdThuMuc),
          mucCon,
        );

        const donViSoanThao = pane.getByTestId(THU_MUC_FIELD.DON_VI_SOAN_THAO);
        if (await donViSoanThao.isVisible().catch(() => false)) {
          await pickFirstOption(page, donViSoanThao);
        }

        await modal.getByRole("button", { name: /Xác nhận/ }).click();
        await page.waitForTimeout(TIMEOUT.ACTION_LOADING);
        await dongModalThuMucNeuDangMo(page);

        await expect(getCauTrucRow(page, ten), {
          message:
            `Lỗi: LƯU THÀNH CÔNG thư mục con "${ten}" với ${truong.nhan} "${mucCon}" bên trong ` +
            `${moTaCha} (${truong.nhan} "${mucCha}") — trong "${mucCha}" chỉ được chứa [${choPhep.join(" | ")}]`,
        }).toHaveCount(0, { timeout: TIMEOUT.DATA_LOADING });
      },
    );
  }
};

/* -------------------------------------------------------------------------- */
/* Kế thừa quyền từ cấp cha (247) & ngắt kế thừa (248)                         */
/* -------------------------------------------------------------------------- */

/** 1 dòng trong bảng "quyền nào của ai" — dùng cho case 247, 248 */
export type QuyenPhanCong = {
  /** Cột trong bảng Phân quyền nâng cao — dùng hằng `PQ_COL` */
  cot: string;
  /** Nhãn cột, chỉ để đưa vào message lỗi */
  nhanQuyen: string;
  /** Account (hoặc tên nhóm người dùng) được cấp quyền này */
  account: string;
  /**
   * **Tên hiển thị** của đối tượng — lấy động qua `layTenDoiTuongPhanQuyen` (đừng hard-code).
   * Nhóm người dùng không đăng nhập được → tên hiển thị chính là tên nhóm.
   */
  tenHienThi: string;
};

/**
 * Thao tác của case 247: tạo 1 thư mục ở gốc BHS, rồi 1 thư mục con bên trong nó.
 * Cả hai đều để **mặc định = kế thừa quyền** (không đặt quyền độc lập).
 */
export const taoThuMucChaVaCon247 = async (page: Page, duLieu: DuLieu5Quyen) => {
  const pw = new PW(page);

  await test.step(`2. Tạo thư mục "${duLieu.tenThuMucCha}" ở gốc BHS (giữ mặc định kế thừa quyền)`, async () => {
    await createThuMuc(page, pw, duLieu.recordUrl, duLieu.tenThuMucCha);
  });

  await test.step(`3. Tạo thư mục con "${duLieu.tenThuMucCon}" bên trong "${duLieu.tenThuMucCha}"`, async () => {
    await createThuMuc(page, pw, duLieu.recordUrl, duLieu.tenThuMucCon, {
      parentName: duLieu.tenThuMucCha,
    });
  });
};

/** Assert 1 item có đúng bộ quyền `quyen` (mỗi đối tượng nằm đúng cột quyền của mình) */
const expectDungBoQuyen = async (
  page: Page,
  code: string,
  tenItem: string,
  quyen: readonly QuyenPhanCong[],
) => {
  for (const q of quyen) {
    await checkNguoiTrongOQuyen(
      page,
      code,
      q.cot,
      q.tenHienThi,
      `${tenItem} · ${q.nhanQuyen} · ${q.account}`,
    );
  }
};

/**
 * Mong muốn của case 247 — kiểm tra trên modal **Phân quyền nâng cao** (nơi duy nhất xem được
 * quyền sau khi tạo):
 *
 * 1. BHS giữ đúng 5 quyền đã phân lúc tạo.
 * 2. Thư mục ở gốc BHS đang **Kế thừa** và có **đúng 5 quyền giống BHS**.
 * 3. Thư mục con đang **Kế thừa** và có **đúng 5 quyền giống thư mục cha**.
 *
 * Mỗi cấp kiểm 2 lớp: (a) từng account nằm đúng cột quyền của mình, (b) so **toàn bộ 5 cột** với
 * cấp cha bằng `checkQuyenGiongItemCha` — lớp (b) bắt luôn trường hợp item con **thừa** người so
 * với cha.
 *
 * ⚠️ Cột "Quyền Owner" của BHS còn có **người tạo BHS** ngoài account được cấp OWNER; vì so sánh
 * là con-với-cha nên không cần liệt kê người tạo ở `quyen`.
 */
export const expectKeThuaQuyenTuCapCha247 = async (
  page: Page,
  duLieu: DuLieu5Quyen,
  quyen: readonly QuyenPhanCong[],
) => {
  await test.step('Mở modal "Phân quyền nâng cao" của BHS', async () => {
    await openPhanQuyenNangCao(page, duLieu.recordUrl);
  });

  await test.step("Mong muốn 1: BHS giữ đúng 5 quyền đã phân", async () => {
    await expectDungBoQuyen(page, ITEM_PREFIX.BHS, "Bộ hồ sơ", quyen);
  });

  let codeThuMucCha = "";

  await test.step(`Mong muốn 2: thư mục "${duLieu.tenThuMucCha}" kế thừa nguyên 5 quyền của BHS`, async () => {
    codeThuMucCha = await getCodeCuaItemPhanQuyen(page, duLieu.tenThuMucCha);
    await expectKeThuaVaGiongCha(
      page,
      { code: ITEM_PREFIX.BHS, ten: duLieu.tenBoHoSo },
      { code: codeThuMucCha, ten: duLieu.tenThuMucCha },
      quyen,
    );
  });

  await test.step(`Mong muốn 3: thư mục con "${duLieu.tenThuMucCon}" kế thừa nguyên 5 quyền của thư mục cha`, async () => {
    // Bảng là CÂY: dòng con chưa có trong DOM tới khi expand cha
    // (KHO-TAI-LIEU.MODAL-PHAN-QUYEN-NANG-CAO.md mục 4b)
    await moItemPhanQuyen(page, codeThuMucCha);

    const codeThuMucCon = await getCodeCuaItemPhanQuyen(
      page,
      duLieu.tenThuMucCon,
    );
    await expectKeThuaVaGiongCha(
      page,
      { code: codeThuMucCha, ten: duLieu.tenThuMucCha },
      { code: codeThuMucCon, ten: duLieu.tenThuMucCon },
      quyen,
    );
  });
};

/** 1 item trong bảng Phân quyền nâng cao: mã prefix + tên (chỉ để in vào message lỗi) */
type ItemPhanQuyen = { code: string; ten: string };

/**
 * Assert 1 item đang **Kế thừa**, có đúng bộ quyền `quyen` và **giống hệt cha ở cả 5 cột**.
 * Modal Phân quyền nâng cao phải đang mở, dòng của cả 2 item phải đang hiển thị.
 */
const expectKeThuaVaGiongCha = async (
  page: Page,
  cha: ItemPhanQuyen,
  con: ItemPhanQuyen,
  quyen: readonly QuyenPhanCong[],
) => {
  await checkKeThua(page, con.code, "Kế thừa", con.ten);
  await expectDungBoQuyen(page, con.code, con.ten, quyen);
  await checkQuyenGiongItemCha(page, cha.code, con.code, cha.ten, con.ten);
};

/* --- Case 248: ngắt kế thừa rồi sửa quyền của thư mục ------------------------------------ */

/** Thao tác của case 248: tạo 1 thư mục ở gốc BHS, giữ **mặc định (kế thừa quyền)** */
export const taoThuMucOGocBoHoSo248 = async (
  page: Page,
  duLieu: DuLieu5Quyen,
) => {
  await test.step(`2. Tạo thư mục "${duLieu.tenThuMucCha}" ở gốc BHS (giữ mặc định kế thừa quyền)`, async () => {
    await createThuMuc(
      page,
      new PW(page),
      duLieu.recordUrl,
      duLieu.tenThuMucCha,
    );
  });
};

/**
 * Mong muốn đầu của case 248: thư mục vừa tạo đang **Kế thừa** và có **đúng 5 quyền của BHS**
 * (giống bước 2 của case 247, nhưng chỉ 1 cấp).
 */
export const expectThuMucKeThuaQuyenBoHoSo = async (
  page: Page,
  duLieu: DuLieu5Quyen,
  quyen: readonly QuyenPhanCong[],
) => {
  await test.step('Mở modal "Phân quyền nâng cao" của BHS', async () => {
    await openPhanQuyenNangCao(page, duLieu.recordUrl);
  });

  await test.step("Mong muốn 1: BHS giữ đúng 5 quyền đã phân", async () => {
    await expectDungBoQuyen(page, ITEM_PREFIX.BHS, "Bộ hồ sơ", quyen);
  });

  await test.step(`Mong muốn 2: thư mục "${duLieu.tenThuMucCha}" kế thừa nguyên 5 quyền của BHS`, async () => {
    const code = await getCodeCuaItemPhanQuyen(page, duLieu.tenThuMucCha);
    await expectKeThuaVaGiongCha(
      page,
      { code: ITEM_PREFIX.BHS, ten: duLieu.tenBoHoSo },
      { code, ten: duLieu.tenThuMucCha },
      quyen,
    );
  });
};

/** Mô tả thao tác "bớt 1 người, thêm 1 người" trên **cùng 1 khối quyền** của thư mục (case 248) */
export type ThemBotQuyen248 = {
  /** Khối quyền thao tác — dùng hằng `FOLDER_PICKER` */
  permTestId: string;
  /** Cột tương ứng trong bảng Phân quyền nâng cao — dùng hằng `PQ_COL` */
  cot: string;
  /** Nhãn quyền, chỉ để in vào message lỗi */
  nhanQuyen: string;
  /** Đối tượng **bị bớt** — đang có quyền này nhờ kế thừa từ BHS */
  bot: { account: string; tenHienThi: string };
  /** Đối tượng **được thêm** vào khối quyền đó */
  them: { account: string; tenHienThi: string };
};

/**
 * Thao tác chính của case 248, làm trên **modal "Cập nhật thư mục"** (nơi duy nhất vừa sửa được
 * chip quyền vừa có nút Lưu — modal Phân quyền nâng cao chỉ *thêm* quyền, không bớt được):
 *
 *   Ngắt kế thừa ("Đặt quyền độc lập") → bớt 1 người → thêm 1 người → Xác nhận (toast "Thành công").
 *
 * 🚨 Bước "bớt" xoá **chip đầu tiên** của khối quyền (chip không mang tên trong DOM nên không xoá
 * theo tên được) → hàm **bắt buộc khối đó chỉ có đúng 1 chip** trước khi xoá, để chắc chắn người
 * bị xoá là `bot`. Chọn khối quyền nào chỉ có 1 người ở BHS (vd Quyền tải file).
 *
 * ⚠️ Đặt quyền độc lập **giữ nguyên các chip đã kế thừa** (`MODAL-TAO-THU-MUC.md` mục 4b) — đó là
 * lý do bước "bớt" có cái để xoá.
 */
export const ngatKeThuaRoiSuaQuyenThuMuc248 = async (
  page: Page,
  duLieu: DuLieu5Quyen,
  thaoTac: ThemBotQuyen248,
) => {
  const ten = duLieu.tenThuMucCha;

  await test.step(`3. Mở màn "Cập nhật thư mục ${ten}" → tab Phân quyền → Ngắt kế thừa (Đặt quyền độc lập)`, async () => {
    await openManCapNhatThuMuc(page, duLieu.recordUrl, ten);
    const modal = getThuMucModal(page);
    await openTabThuMuc(page, "Phân quyền", modal);

    // Trước khi ngắt: đang kế thừa → chỉ đọc, không có ô nhập nào
    await checkBadgeTabPhanQuyen(page, "Kế thừa", modal);
    await expectPickerPhanQuyenChiDoc(page, modal);

    // Ngắt kế thừa (hàm tự assert badge đổi sang "Độc lập")
    await datQuyenDocLap(page, modal);
  });

  await test.step(`4. Bớt "${thaoTac.bot.account}" khỏi khối "${thaoTac.nhanQuyen}"`, async () => {
    const modal = getThuMucModal(page);
    const truoc = await demChipKhoiQuyenThuMuc(page, thaoTac.permTestId, modal);
    expect(truoc, {
      message:
        `Lỗi: khối "${thaoTac.nhanQuyen}" của thư mục "${ten}" đang có ${truoc} chip — ` +
        `case cần đúng 1 chip (của "${thaoTac.bot.account}", kế thừa từ BHS) mới xoá đúng người`,
    }).toBe(1);

    await xoaChipDauTienKhoiQuyenThuMuc(page, thaoTac.permTestId, modal);
  });

  await test.step(`5. Thêm "${thaoTac.them.account}" vào khối "${thaoTac.nhanQuyen}"`, async () => {
    const modal = getThuMucModal(page);
    await themQuyenTrongModalThuMuc(
      page,
      thaoTac.them.account,
      [thaoTac.permTestId],
      modal,
    );

    const sau = await demChipKhoiQuyenThuMuc(page, thaoTac.permTestId, modal);
    expect(sau, {
      message:
        `Lỗi: thêm "${thaoTac.them.account}" vào khối "${thaoTac.nhanQuyen}" nhưng khối đang có ` +
        `${sau} chip (mong đợi 1 — đã xoá 1, thêm 1)`,
    }).toBe(1);
  });

  await test.step(`6. Bấm "Xác nhận" → lưu thư mục thành công`, async () => {
    await luuModalThuMuc(page, getThuMucModal(page), `Sửa quyền thư mục ${ten}`);
  });
};

/**
 * Mong muốn cuối của case 248: sau khi lưu, thư mục thật sự **đứt kế thừa** và giữ đúng thay đổi —
 * đọc lại từ modal Phân quyền nâng cao.
 *
 * - Cột "Kế thừa" đổi thành **"Quyền riêng tư"**
 * - Khối quyền vừa sửa: **có** người được thêm, **không còn** người bị bớt
 * - 4 khối quyền còn lại **giữ nguyên** người kế thừa từ BHS
 */
export const expectQuyenRiengSauKhiLuu248 = async (
  page: Page,
  duLieu: DuLieu5Quyen,
  quyen: readonly QuyenPhanCong[],
  thaoTac: ThemBotQuyen248,
) => {
  const ten = duLieu.tenThuMucCha;

  await test.step(`Mong muốn 3: thư mục "${ten}" chuyển "Quyền riêng tư" và giữ đúng thay đổi vừa lưu`, async () => {
    await openPhanQuyenNangCao(page, duLieu.recordUrl);
    const code = await getCodeCuaItemPhanQuyen(page, ten);

    await checkKeThua(page, code, "Quyền riêng tư", ten);

    await checkNguoiTrongOQuyen(
      page,
      code,
      thaoTac.cot,
      thaoTac.them.tenHienThi,
      `${ten} · ${thaoTac.nhanQuyen} · vừa thêm ${thaoTac.them.account}`,
    );

    const dsSau = await getNguoiTrongOQuyen(page, code, thaoTac.cot);
    expect(dsSau, {
      message:
        `Lỗi: đã bớt "${thaoTac.bot.account}" khỏi "${thaoTac.nhanQuyen}" của thư mục "${ten}" ` +
        `nhưng vẫn còn trong bảng (đang có: [${dsSau.join(" | ")}])`,
    }).not.toContain(thaoTac.bot.tenHienThi);

    // 4 khối quyền không đụng tới phải giữ nguyên người kế thừa từ BHS
    await expectDungBoQuyen(
      page,
      code,
      ten,
      quyen.filter((q) => q.cot !== thaoTac.cot),
    );
  });
};

/* --- Case 182: bộ tên riêng cho trường Độ mật (wrapper của các hàm generic ở trên) --------- */

/** 3 mức Độ mật theo thứ tự **lỏng → chặt**: `Thường` < `Mật` < `Tuyệt mật` */
export const DO_MAT = TRUONG_DO_MAT.mucDo;

/** Các mức Độ mật được phép dùng bên trong 1 cha có Độ mật `doMatCha` (con phải ≥ cha) */
export const doMatChoPhepTrong = (doMatCha: string): string[] =>
  mucChoPhepTrong(TRUONG_DO_MAT, doMatCha);

export const expectDoMatMacDinhRoiDong = (
  page: Page,
  recordUrl: string,
  doMatMongDoi: string,
) => expectGiaTriMacDinhRoiDong(page, recordUrl, TRUONG_DO_MAT, doMatMongDoi);

export const doiDoMatBoHoSo = (page: Page, recordUrl: string, doMat: string) =>
  doiGiaTriBoHoSo(page, recordUrl, TRUONG_DO_MAT, doMat);

export const expectRangBuocDoMatThuMucCon = (
  page: Page,
  recordUrl: string,
  tenCha: string,
  doMatCha: string,
) =>
  expectRangBuocThuMucCon(page, recordUrl, TRUONG_DO_MAT, {
    mucCha: doMatCha,
    tenCha,
    tenCon: tenCha,
  });
