import path from "path";
import { expect, Page } from "@playwright/test";
import { test } from "../../src/fixture/base-test";
import { TIMEOUT } from "../../src/constant/timeout";
import { PW } from "../../src/utils/PW";
import {
  checkNguoiTrongOQuyen,
  createTaiLieu,
  docGiaTriTruongTaiLieu,
  docThongTinFile,
  getCauTrucRow,
  getCodeCuaItemPhanQuyen,
  getDongFileTaiLieu,
  getPaneTaiLieu,
  getRecordModal,
  layDanhSachTenTep,
  layTinhTrangTuTieuDe,
  moItemPhanQuyen,
  openBoHoSo,
  openChiTietTaiLieu,
  openPhanQuyenNangCao,
  openTabCauTrucHoSo,
  openTabTaiLieuSo,
  PQ_COL,
  uploadFileTaiLieu,
} from "../../src/screen-instructions/KHO-TAI-LIEU.functions";

/**
 * Chuỗi bước dùng chung cho bộ case **1.4.2 — Thêm mới tài liệu trong Cấu trúc hồ sơ** (UC23).
 * Đọc `tests/1.4.2/1.4.2.md` trước khi sửa file này.
 */

/** testId 2 trường **bắt buộc** của modal "Tạo mới tài liệu" (MODAL-TAO-TAI-LIEU.md mục 2) */
export const TL_FIELD = {
  TEN: "txt-ten-tai-lieu",
  LOAI: "sel-loai-tai-lieu",
} as const;

/** Tình trạng của BHS / Tài liệu hiển thị ở `lbl-modal-title` */
export const TINH_TRANG = {
  KHAI_BAO: "Khai báo",
  HOAT_DONG: "Hoạt động",
} as const;

/** Dữ liệu **đã nhập** khi tạo 1 tài liệu — dùng để so lại sau khi lưu */
export type TaiLieuDaTao = {
  ten: string;
  /**
   * Nhãn "Loại tài liệu" đọc lại **ngay trong modal trước khi Lưu** — `createTaiLieu` chọn
   * option đầu tiên nên giá trị này không cố định giữa các lần chạy.
   */
  loaiTaiLieu: string;
  /** Tên thư mục chứa tài liệu; rỗng = nằm ở gốc BHS */
  tenThuMucCha: string;
};

/**
 * **Thao tác**: tạo 1 tài liệu, nhập đầy đủ **các trường bắt buộc** (Tên tài liệu + Loại tài liệu)
 * rồi Lưu (`createTaiLieu` đã assert toast "Thành công").
 *
 * @param opts.tenThuMucCha         bỏ trống → tài liệu nằm ở **gốc BHS**
 * @param opts.moTuMenuDongThuMuc   mở modal từ **menu dòng** của thư mục thay vì nút "Tạo mới" của
 *   toolbar — bắt buộc với vai **chỉ có quyền tại thư mục đó** (không có `btn-tao-moi`, xem
 *   `KHO-TAI-LIEU.PHAN-QUYEN-THEO-VAI.md` mục 2)
 */
export const taoTaiLieuDuTruongBatBuoc = async (
  page: Page,
  recordUrl: string,
  ten: string,
  opts: { tenThuMucCha?: string; moTuMenuDongThuMuc?: boolean } = {},
): Promise<TaiLieuDaTao> => {
  const pw = new PW(page);
  const viTri = opts.tenThuMucCha
    ? `trong thư mục "${opts.tenThuMucCha}"${opts.moTuMenuDongThuMuc ? " (mở từ menu dòng)" : ""}`
    : "ở gốc Bộ hồ sơ";
  const ketQua: TaiLieuDaTao = {
    ten,
    loaiTaiLieu: "",
    tenThuMucCha: opts.tenThuMucCha ?? "",
  };

  await test.step(`Thao tác: tạo tài liệu "${ten}" ${viTri} — nhập đủ trường bắt buộc`, async () => {
    const values = await createTaiLieu(page, pw, recordUrl, ten, {
      folderName: opts.tenThuMucCha,
      moTuMenuDongThuMuc: opts.moTuMenuDongThuMuc,
      readFields: [TL_FIELD.LOAI],
    });

    ketQua.loaiTaiLieu = values[TL_FIELD.LOAI] ?? "";
    expect(ketQua.loaiTaiLieu, {
      message: `Lỗi: không đọc được "Loại tài liệu" đã chọn khi tạo tài liệu "${ten}" → không có gì để so sau khi lưu`,
    }).toBeTruthy();
  });

  return ketQua;
};

/**
 * **Mong muốn**: mở lại tài liệu → các trường dữ liệu **khớp dữ liệu đã nhập** và **Tình trạng
 * ăn theo trạng thái của Bộ hồ sơ** lúc tạo.
 *
 * Chỉ so 2 trường bắt buộc (Tên tài liệu, Loại tài liệu) — đúng phạm vi "thông tin bắt buộc" của QA.
 *
 * ⚠️ **Chưa khảo sát** màn chi tiết Tài liệu render 2 trường này thế nào sau khi lưu (input hay
 * text đã khoá như một số field của BHS — `KHO-TAI-LIEU.md` mục 4.5). `docGiaTriTruongTaiLieu` đọc
 * được cả 2 dạng; nếu testId biến mất hẳn thì hàm trả `""` và assert dưới đây fail với thông báo rõ.
 */
export const expectTaiLieuDaLuuDungDuLieu = async (
  page: Page,
  recordUrl: string,
  tl: TaiLieuDaTao,
  tinhTrangMongDoi: string,
) => {
  await test.step(`Mong muốn: tài liệu "${tl.ten}" lưu đúng dữ liệu đã nhập + Tình trạng "${tinhTrangMongDoi}"`, async () => {
    await openChiTietTaiLieu(page, recordUrl, tl.ten);
    const modal = getRecordModal(page);

    const ten = await docGiaTriTruongTaiLieu(modal, TL_FIELD.TEN);
    expect(ten, {
      message: `Lỗi: "Tên tài liệu" đã lưu là "${ten}", khác giá trị đã nhập "${tl.ten}"`,
    }).toBe(tl.ten);

    const loai = await docGiaTriTruongTaiLieu(modal, TL_FIELD.LOAI);
    expect(loai, {
      message: `Lỗi: "Loại tài liệu" của "${tl.ten}" đã lưu là "${loai}", khác giá trị đã nhập "${tl.loaiTaiLieu}"`,
    }).toBe(tl.loaiTaiLieu);

    const tinhTrang = await layTinhTrangTuTieuDe(page);
    expect(tinhTrang, {
      message:
        `Lỗi: Tình trạng của tài liệu "${tl.ten}" là "${tinhTrang}", không ăn theo trạng thái ` +
        `"${tinhTrangMongDoi}" của Bộ hồ sơ`,
    }).toBe(tinhTrangMongDoi);
  });
};

/**
 * **Mã prefix** của 1 dòng trong bảng Cấu trúc hồ sơ, đọc từ text của link tên.
 *
 * Text hiển thị dạng `"<mã prefix>. <tên>"` với item ở gốc BHS (`"A. Thư mục 1"`) và
 * `"<mã prefix> <tên>"` với item con (`"A.1 Tài liệu 1"`) — xem `KHO-TAI-LIEU.md` mục 4.4.
 * Hàm lấy token đầu rồi bỏ dấu `.` cuối nên trả về `"A"` / `"A.1"` cho cả 2 dạng.
 */
const layMaPrefixDong = async (page: Page, tenItem: string): Promise<string> => {
  const link = getCauTrucRow(page, tenItem)
    .locator('[data-testid^="lnk-ten-ho-so-tai-lieu-"]')
    .first();
  await expect(link, {
    message: `Lỗi: không đọc được tên hiển thị của dòng "${tenItem}" trong bảng Cấu trúc hồ sơ`,
  }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });

  const token = (await link.innerText()).trim().split(/\s+/)[0] ?? "";
  return token.replace(/\.$/, "");
};

/** Cấp (level) của 1 dòng trong bảng Cấu trúc hồ sơ, đọc từ class `ant-table-row-level-<n>` */
const layCapDong = async (page: Page, tenItem: string): Promise<number> => {
  const cls = (await getCauTrucRow(page, tenItem).getAttribute("class")) ?? "";
  const khop = cls.match(/ant-table-row-level-(\d+)/);
  expect(khop, {
    message: `Lỗi: không đọc được cấp (ant-table-row-level-*) của dòng "${tenItem}". Class đang là "${cls}"`,
  }).toBeTruthy();
  return Number(khop![1]);
};

/**
 * **Mong muốn**: tài liệu hiển thị **đúng vị trí** trong bảng Cấu trúc hồ sơ.
 *
 * Tiêu chí (chốt sau lần chạy 2026-08-04):
 * - Có `tl.tenThuMucCha` → **mã prefix của tài liệu bắt đầu bằng `<mã thư mục cha>.`**
 *   (vd cha `A` → con `A.1`, `A.2`…) **và** cấp của dòng = cấp thư mục cha + 1.
 * - Không có `tl.tenThuMucCha` → tài liệu ở **cấp 0** và mã prefix **không chứa dấu `.`**
 *   (mã có dấu `.` nghĩa là item đang nằm trong 1 thư mục).
 *
 * 🚨 **Không** assert "tài liệu là dòng ngay dưới dòng thư mục cha": 1 thư mục có thể chứa nhiều
 * item nên item mới là con thứ n, không nằm liền kề cha. Đây chính là chỗ bản đầu tiên (mượn logic
 * của `1.4.1.steps.ts` → `expectThuMucHienThiDungViTri`, nơi thư mục cha chỉ có 1 con) bị fail oan
 * khi thư mục 1 đã có sẵn 1 tài liệu: `A. TM1 | A.1 TL1 | A.2 TL4` → TL4 là con **thứ 2**.
 *
 * Cây ở tab này **expand sẵn** nên dòng con có luôn trong DOM (`KHO-TAI-LIEU.md` mục 4.4).
 */
export const expectTaiLieuDungViTri = async (
  page: Page,
  recordUrl: string,
  tl: TaiLieuDaTao,
) => {
  const moTa = tl.tenThuMucCha
    ? `bên trong thư mục "${tl.tenThuMucCha}"`
    : "ở gốc Bộ hồ sơ";

  await test.step(`Mong muốn: tài liệu "${tl.ten}" hiển thị ${moTa}`, async () => {
    await openBoHoSo(page, recordUrl);
    await openTabCauTrucHoSo(page);

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

    const maTaiLieu = await layMaPrefixDong(page, tl.ten);

    if (!tl.tenThuMucCha) {
      await expect(row, {
        message: `Lỗi: tài liệu "${tl.ten}" phải ở gốc Bộ hồ sơ nhưng không nằm ở cấp 0 của cây cấu trúc`,
      }).toHaveClass(/ant-table-row-level-0/);

      expect(maTaiLieu.includes("."), {
        message:
          `Lỗi: tài liệu "${tl.ten}" phải ở gốc Bộ hồ sơ nhưng mã prefix là "${maTaiLieu}" — ` +
          "mã có dấu \".\" là mã của item nằm trong 1 thư mục",
      }).toBe(false);
      return;
    }

    const maCha = await layMaPrefixDong(page, tl.tenThuMucCha);
    expect(maTaiLieu.startsWith(`${maCha}.`), {
      message:
        `Lỗi: tài liệu "${tl.ten}" có mã prefix "${maTaiLieu}", không thuộc nhánh của thư mục ` +
        `"${tl.tenThuMucCha}" (mã "${maCha}") — mã con phải có dạng "${maCha}.<n>"`,
    }).toBe(true);

    const capCha = await layCapDong(page, tl.tenThuMucCha);
    await expect(row, {
      message:
        `Lỗi: tài liệu "${tl.ten}" không nằm ở cấp ${capCha + 1} của cây cấu trúc ` +
        `(thư mục cha "${tl.tenThuMucCha}" đang ở cấp ${capCha})`,
    }).toHaveClass(new RegExp(`ant-table-row-level-${capCha + 1}`));
  });
};

/* -------------------------------------------------------------------------- */
/* Upload file mềm ở tab "Tài liệu số" (KHO-TAI-LIEU.TAB-TAI-LIEU-SO.md)        */
/* -------------------------------------------------------------------------- */

/**
 * **Thao tác**: mở tài liệu → tab "Tài liệu số" → **upload lần đầu** 1 file.
 *
 * Trước khi upload assert bảng file **đang rỗng** (`empty-file-dinh-kem`) — đó là nghĩa "lần đầu"
 * của case; và truyền `expectKhongTrung` để **dialog "Danh sách tài liệu trùng" bị coi là fail**
 * (file phải chưa từng có trong hệ thống → dùng `taoFilePdfDuyNhat` của `1.4.2.setup.ts`).
 *
 * @returns tên tệp đã upload
 */
export const uploadFileLanDau = async (
  page: Page,
  recordUrl: string,
  tl: TaiLieuDaTao,
  duongDanFile: string,
): Promise<string> => {
  const tenTep = path.basename(duongDanFile);

  await test.step(`Thao tác: mở tài liệu "${tl.ten}" → tab "Tài liệu số" → upload "${tenTep}"`, async () => {
    await openChiTietTaiLieu(page, recordUrl, tl.ten);
    await openTabTaiLieuSo(page);

    await expect(getPaneTaiLieu(page).getByTestId("empty-file-dinh-kem"), {
      message: `Lỗi: tài liệu "${tl.ten}" đã có file sẵn nên không kiểm chứng được "upload lần đầu"`,
    }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });

    await uploadFileTaiLieu(page, duongDanFile, { expectKhongTrung: true });
  });

  return tenTep;
};

/**
 * **Mong muốn**: file vừa upload **hiển thị trong bảng "Tài liệu số"** với đủ thông tin — đúng tên
 * tệp, Dung lượng / Thời gian upload **không rỗng**, và cột Người upload chứa `tenNguoiUpload`.
 *
 * ⚠️ **Không** assert theo toast `"Tải tài liệu lên thành công."`: toast sống ~4 s và app còn tự mở
 * modal xem trước file ngay sau đó nên có lần upload thành công vẫn không bắt được toast
 * (`KHO-TAI-LIEU.TAB-TAI-LIEU-SO.md` mục 6) — `uploadFileTaiLieu` chỉ chờ toast kiểu "mềm".
 */
export const expectFileHienThiTrongBang = async (
  page: Page,
  tenTep: string,
  tenNguoiUpload: string,
) => {
  await test.step(`Mong muốn: bảng "Tài liệu số" hiển thị file "${tenTep}" (người upload "${tenNguoiUpload}")`, async () => {
    await expect(getDongFileTaiLieu(page, tenTep), {
      message: `Lỗi: không thấy dòng file "${tenTep}" trong bảng "Tài liệu số"`,
    }).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });

    const tt = await docThongTinFile(page, tenTep);
    expect(tt.dungLuong, {
      message: `Lỗi: file "${tenTep}" không có Dung lượng`,
    }).toBeTruthy();
    expect(tt.thoiGianUpload, {
      message: `Lỗi: file "${tenTep}" không có Thời gian upload`,
    }).toBeTruthy();
    expect(tt.nguoiUpload, {
      message: `Lỗi: cột Người upload của "${tenTep}" là "${tt.nguoiUpload}", không chứa "${tenNguoiUpload}"`,
    }).toContain(tenNguoiUpload);
  });
};

/**
 * **Mong muốn**: file vẫn còn sau khi **mở lại tài liệu** — chứng minh đã lưu, không chỉ hiển thị
 * tạm trên UI.
 */
export const expectFileConSauKhiMoLai = async (
  page: Page,
  recordUrl: string,
  tl: TaiLieuDaTao,
  tenTep: string,
) => {
  await test.step(`Mong muốn: mở lại tài liệu "${tl.ten}" vẫn thấy file "${tenTep}"`, async () => {
    await openChiTietTaiLieu(page, recordUrl, tl.ten);
    await openTabTaiLieuSo(page);
    const danhSach = await layDanhSachTenTep(page);
    expect(danhSach, {
      message: `Lỗi: mở lại tài liệu "${tl.ten}" không còn file "${tenTep}". Đang có: [${danhSach.join(", ")}]`,
    }).toContain(tenTep);
  });
};

/**
 * **Mong muốn**: cột "Quyền Owner" của tài liệu **có chứa** `tenOwner` — đọc từ modal
 * **Phân quyền nâng cao** của BHS. Đây là nơi duy nhất xem được quyền của item sau khi tạo: khối
 * phân quyền trên màn chi tiết bị app ẩn (`KHO-TAI-LIEU.md` mục 4.6).
 *
 * 📌 **Chỉ assert "có chứa", KHÔNG assert danh sách Owner đúng bằng 1 người** (người dùng chốt
 * 2026-08-04): tài liệu mới có thể **còn kế thừa** quyền của cấp cha nên cột Owner thường có thêm
 * người khác (người tạo BHS, người được cấp Owner ở BHS / thư mục cha).
 * `checkNguoiTrongOQuyen` tự bấm nút `+ n người khác` khi ô gom bớt avatar nên người cần kiểm tra
 * vẫn tìm được dù ô có nhiều người.
 *
 * 🚨 `page` truyền vào phải là phiên **Owner của BHS**: vai khác không có `btn-more` nên không mở
 * được modal này (`KHO-TAI-LIEU.PHAN-QUYEN-THEO-VAI.md` mục 2) → dùng phiên người tạo BHS để
 * kiểm tra cả những tài liệu do vai khác tạo.
 *
 * @param tenOwner **tên hiển thị** (lấy động qua `layTenHienThiNguoiDangDangNhap` /
 *   `layTenDoiTuongPhanQuyen`) — bảng quyền không hiển thị account id.
 */
export const expectOwnerTaiLieu = async (
  page: Page,
  recordUrl: string,
  tl: TaiLieuDaTao,
  tenOwner: string,
) => {
  await test.step(`Mong muốn: Owner của tài liệu "${tl.ten}" là "${tenOwner}"`, async () => {
    await openPhanQuyenNangCao(page, recordUrl);

    // Bảng Phân quyền nâng cao là CÂY: dòng con chỉ có trong DOM sau khi expand cha
    if (tl.tenThuMucCha) {
      const codeCha = await getCodeCuaItemPhanQuyen(page, tl.tenThuMucCha);
      await moItemPhanQuyen(page, codeCha);
    }

    const code = await getCodeCuaItemPhanQuyen(page, tl.ten);
    await checkNguoiTrongOQuyen(
      page,
      code,
      PQ_COL.OWNER,
      tenOwner,
      `Owner của tài liệu "${tl.ten}"`,
    );
  });
};
