import { expect, Page } from "@playwright/test";
import { TIMEOUT } from "../constant/timeout";
interface PWData {
  testId: string;
  value: string;
  extraLocator?: string;
}
export class PW {
  private page: Page;
  constructor(page: Page) {
    this.page = page;
  }
  isVisible(
    testId: string,
    timeout: number = TIMEOUT.ACTION_LOADING,
    message?: string,
  ) {
    return expect(this.getElement(testId), { message }).toBeVisible({
      timeout,
    });
  }

  async inputWithoutSelDuAn() {
    let count = 0;
    do {
      const visibleDuAn = await this.page.getByTestId("sel-duAn").isVisible();

      if (visibleDuAn) {
        await this.inputTreeDropDown("sel-loaiBoHoSo");
      } else {
        break;
      }
      count++;
      if (count > 10) {
        break;
      }
    } while (true);
  }
  async inputWithSelDuAn() {
    let count = 0;
    do {
      const visibleDuAn = await this.page.getByTestId("sel-duAn").isVisible();

      if (visibleDuAn) {
        await this.inputDropDownList("sel-duAn");
        break;
      } else {
        await this.inputTreeDropDown("sel-loaiBoHoSo");
      }
      count++;
      if (count > 10) {
        break;
      }
    } while (true);
  }

  async isEmpty(testId: string, timeout = TIMEOUT.VALIDATE_WAITING) {
    if (testId.startsWith("tree-sel-")) {
      await this.isTreeDropDownListEmpty(testId);
    } else if (testId.startsWith("sel-tags-")) {
      await expect(
        this.page.getByTestId(testId).locator(".ant-select-selection-item"),
      ).toHaveCount(0, { timeout });
    } else if (testId.startsWith("sel-")) {
      await this.isDropDownListEmpty(testId);
    } else if (testId.startsWith("txt-") || testId.startsWith("txa-")) {
      await this.isTextInputEmpty(testId);
    } else if (testId.startsWith("pp-multi-")) {
      await expect(
        this.page.getByTestId(testId).locator(".ant-tag"),
      ).toHaveCount(0, { timeout });
    } else {
      throw new Error(`Chưa có hàm kiểm tra isEmpty cho testId ${testId}`);
    }
  }
  async valueShouldBe(
    testId: string,
    expectedValue: string,
    timeout = TIMEOUT.VALIDATE_WAITING,
  ) {
    if (testId.startsWith("sel-") || testId.startsWith("tree-sel-")) {
      await expect(
        this.getElement(testId).locator(".ant-select-selection-item"),
      ).toHaveText(expectedValue, { timeout });
    }
    if (testId.startsWith("txt-") || testId.startsWith("txa-")) {
      await expect(this.getElement(testId)).toHaveValue(expectedValue, {
        timeout,
      });
    }
  }
  async valueShouldContain(
    testId: string,
    expectedValue: string,
    timeout = TIMEOUT.VALIDATE_WAITING,
  ) {
    if (testId.startsWith("sel-") || testId.startsWith("tree-sel-")) {
      await expect(
        this.getElement(testId).locator(".ant-select-selection-item"),
      ).toContainText(expectedValue, { timeout });
    }
    if (testId.startsWith("txt-") || testId.startsWith("txa-")) {
      await expect(this.getElement(testId)).toContainText(expectedValue, {
        timeout,
      });
    }
  }
  async isTextInputEmpty(testId: string) {
    await this.isVisible(testId);
    await expect(this.getElement(testId)).toHaveValue("");
  }
  async isTreeDropDownListEmpty(testId: string) {
    await this.isVisible(testId);
    await expect(
      this.getElement(testId).locator(".ant-select-selection-placeholder"),
    ).toBeVisible();
  }
  async isDropDownListEmpty(testId: string) {
    await this.isVisible(testId);
    await expect(
      this.getElement(testId).locator(".ant-select-selection-placeholder"),
    ).toBeVisible();
  }
  getElement(testId: string) {
    return this.page.getByTestId(testId);
  }
  async arrowDown() {
    this.page.keyboard.press("ArrowDown");
  }
  async enter() {
    return this.page.keyboard.press("Enter");
  }
  wait(timeout = 500) {
    return this.page.waitForTimeout(timeout);
  }
  async clickButton(testId: string) {
    return await this.getElement(testId).click();
  }
  async inputText(testId: string, text: string) {
    await this.getElement(testId).fill(text);
    // await expect(this.getElement(testId)).toContainText(text.trim());
  }
  async checkListValueDropdownListTags(
    testId: string,
    expectedValues: string[],
  ) {
    await this.getElement(testId).click();
    await expect(
      this.getElement(testId).locator(
        ".ant-select-selection-item-content:visible",
      ),
      {
        message: "Lỗi: Số lượng tags không giống số lượng mong muốn",
      },
    ).toHaveCount(expectedValues.length);
    for (const giaTri of expectedValues) {
      // Lọc ra cái option chứa giá trị đó và ép nó phải đang hiển thị trên màn hình
      await expect(
        this.getElement(testId)
          .locator(".ant-select-selection-item-content:visible")
          .filter({ hasText: new RegExp(`^${giaTri.trim()}$`) })
          .first(),
        {
          message: "Lỗi: Giá trị mong đợi không đúng",
        },
      ).toBeVisible();
    }
  }
  async checkListValueDropdownList(testId: string, expectedValues: string[]) {
    await this.getElement(testId).click();
    await expect(
      this.page.locator(".ant-select-item-option-content:visible"),
    ).toHaveCount(expectedValues.length);
    for (const giaTri of expectedValues) {
      // Lọc ra cái option chứa giá trị đó và ép nó phải đang hiển thị trên màn hình
      await expect(
        this.page
          .locator(".ant-select-item-option-content")
          .filter({ hasText: giaTri })
          .first(),
      ).toBeVisible();
    }
  }

  async checkSearchDropdownHasOption(testId: string, keyword: string) {
    await this.isVisible(testId);

    await this.getElement(testId).getByRole("combobox").fill(keyword);
    await this.wait(2000); // Chờ dữ liệu load lại
    const danhSachOptions = this.getElement(testId).locator(
      ".ant-select-item-option-content",
    );

    // Tất cả options đều phải chứa keyword (kiểm tra cả bản gốc và bản không dấu của option)
    const total = await danhSachOptions.count();
    for (let i = 0; i < total; i++) {
      const optionText = await danhSachOptions.nth(i).innerText();
      const matched =
        optionText.includes(keyword) ||
        this.removeAccents(optionText).includes(keyword);
      expect(matched, {
        message: `Option "${optionText}" không chứa keyword "${keyword}"`,
      }).toBe(true);
    }
  }

  async checkSearchTreeDropdownHasOption(testId: string, keyword: string) {
    await this.isVisible(testId);

    await this.getElement(testId).getByRole("combobox").fill(keyword);

    const danhSachOptions = this.page.locator(".ant-select-tree-title");

    // Lọc ra các option chứa keyword, lấy cái đầu tiên tìm thấy và ép nó phải tồn tại
    await expect(
      danhSachOptions.filter({ hasText: keyword }).first(),
    ).toBeVisible();
  }

  /**
   * Chọn 1 giá trị của dropdown đơn (`sel-`).
   *
   * - Không truyền `optionText` → chọn **option đầu tiên** trong danh sách.
   * - Có `optionText` → **gõ `optionText` vào ô search của dropdown** rồi mới chọn option khớp.
   *
   * 🚨 Vì sao phải gõ search: dropdown của app **chỉ nạp ~10 option đầu tiên**, phần còn lại chỉ
   * xuất hiện sau khi gõ (app lọc ở server). Kiểm chứng 2026-07-30 trên sitdev: `sel-loaiBoHoSo`
   * mở ra chỉ có 10 option và **không** có `AUTO-TEST-TYPE`; gõ `"AUTO"` thì còn đúng option đó.
   * Cách cũ (mở dropdown rồi click thẳng option) chỉ chọn được option nằm trong 10 cái đầu.
   *
   * Ô search là `input.ant-select-selection-search-input` bên trong chính element `sel-*`
   * (element có testId chính là `div.ant-select`). Đã xác minh mọi `sel-*` của màn Kho tài liệu đều
   * có class `ant-select-show-search` và ô search **không** readonly; select nào không cho search
   * thì hàm tự bỏ qua bước gõ và chọn trực tiếp như trước.
   *
   * @param matchMode `"exact"` = nhãn option đúng bằng `optionText`; `"contains"` = chỉ cần chứa
   *   (nhãn option có tiền tố/hậu tố, vd `"A. Tên thư mục"`).
   */
  async inputDropDownList(
    testId: string,
    optionText?: string,
    matchMode: "exact" | "contains" = "exact",
  ) {
    const element = this.getElement(testId);

    if (!optionText || optionText.trim() === "") {
      await element.click();
      await this.arrowDown();
      await this.wait(TIMEOUT.CONTROL_LOADING);
      await this.enter();
      await expect(element.locator(".ant-select-selection-item"), {
        message: `Dropdown [${testId}] vẫn rỗng sau khi chọn giá trị đầu tiên`,
      }).toBeVisible({ timeout: TIMEOUT.VALIDATE_WAITING });
      return;
    }

    await element.click();
    await this.wait(TIMEOUT.CONTROL_LOADING);

    // Gõ từ khoá để app lọc (danh sách chỉ nạp 10 option đầu nếu không search)
    if (((await element.getAttribute("class")) ?? "").includes(
      "ant-select-show-search",
    )) {
      const searchInput = element.locator(
        "input.ant-select-selection-search-input",
      );
      await searchInput.pressSequentially(optionText, { delay: 100 });
      await this.wait(TIMEOUT.CONTROL_LOADING); // chờ app lọc (lọc ở server)
    }

    const option = this.findOptionDangMo(optionText, matchMode);
    try {
      await expect(option).toBeVisible({ timeout: TIMEOUT.DATA_LOADING });
    } catch {
      // Báo rõ đang có những option nào để khỏi phải mở trace mới biết vì sao không khớp
      const dangCo = await this.getOptionsDangMo();
      throw new Error(
        `Dropdown [${testId}]: không tìm thấy option ${matchMode === "exact" ? `= "${optionText}"` : `chứa "${optionText}"`} ` +
          `sau khi gõ từ khoá. Option đang hiển thị: [${dangCo.join(" | ")}]`,
      );
    }
    await option.click();

    if (matchMode === "contains") {
      await expect(element.locator(".ant-select-selection-item"), {
        message: `Dropdown [${testId}] không hiển thị giá trị chứa "${optionText}" sau khi chọn`,
      }).toContainText(optionText, { timeout: TIMEOUT.VALIDATE_WAITING });
    } else {
      await expect(element.locator(".ant-select-selection-item"), {
        message: `Dropdown [${testId}] không hiển thị giá trị "${optionText}" sau khi chọn`,
      }).toHaveText(optionText, { timeout: TIMEOUT.VALIDATE_WAITING });
    }
  }

  /**
   * Option khớp `optionText` trong **dropdown đang mở** (scope theo `.ant-select-dropdown:visible`
   * để không bắt nhầm option của select khác đang có trong DOM).
   */
  private findOptionDangMo(
    optionText: string,
    matchMode: "exact" | "contains",
  ) {
    const options = this.page
      .locator(".ant-select-dropdown:visible")
      .last()
      .locator(".ant-select-item-option-content");
    return matchMode === "contains"
      ? options.filter({ hasText: optionText }).first()
      : options
          .filter({
            hasText: new RegExp(`^\\s*${this.escapeRegex(optionText)}\\s*$`),
          })
          .first();
  }

  /** Danh sách nhãn option của dropdown đang mở — dùng cho message lỗi */
  private async getOptionsDangMo(): Promise<string[]> {
    return this.page
      .locator(".ant-select-dropdown:visible")
      .last()
      .locator(".ant-select-item-option-content")
      .allInnerTexts()
      .then((ds) => ds.map((t) => t.trim()))
      .catch(() => []);
  }

  private escapeRegex(text: string): string {
    return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  /**
   * Như `inputDropDownList(testId, optionText, matchMode)` nhưng **ép đóng dropdown** sau khi chọn
   * — dùng cho field chọn nhiều giá trị (multiple select không tự đóng sau mỗi lần chọn).
   *
   * 📌 Từ 2026-07-30 `inputDropDownList` đã **tự gõ search** trước khi chọn, nên chỉ cần dùng hàm
   * này khi thật sự cần bước đóng dropdown; dropdown đơn thì gọi thẳng `inputDropDownList`.
   */
  async inputDropDownListWithSearch(
    testId: string,
    optionText: string,
    matchMode: "exact" | "contains" = "exact",
  ) {
    await this.inputDropDownList(testId, optionText, matchMode);
    await this.page.keyboard.press("Escape");
    await expect(this.page.locator(".ant-select-dropdown:visible"), {
      message: `Dropdown [${testId}] không đóng lại sau khi chọn "${optionText}"`,
    }).toHaveCount(0, { timeout: TIMEOUT.CONTROL_LOADING });
  }
  async inputTreeDropDown(
    testId: string,
    optionText?: string,
    extraLocator?: string,
    matchMode: "exact" | "contains" = "exact",
  ) {
    if (!optionText || optionText.trim() === "") {
      await this.getElement(testId).click();
      await this.wait(TIMEOUT.CONTROL_LOADING);
      await this.arrowDown();
      await this.enter();
      await expect(
        this.getElement(testId).locator(".ant-select-selection-item"),
        {
          message: `Tree dropdown [${testId}] vẫn rỗng sau khi chọn giá trị đầu tiên`,
        },
      ).toBeVisible({ timeout: TIMEOUT.VALIDATE_WAITING });
    } else {
      await this.getElement(testId).click();
      const textSelector =
        matchMode === "contains"
          ? `:text("${optionText}")`
          : `:text-is("${optionText}")`;
      const locator = extraLocator
        ? this.page.locator(
            `.ant-select-tree-title ${extraLocator}${textSelector}`,
          )
        : this.page.locator(`.ant-select-tree-title${textSelector}`);
      await (matchMode === "contains" ? locator.first() : locator).click();
      await expect(
        this.getElement(testId).locator(".ant-select-selection-item"),
        {
          message: `Tree dropdown [${testId}] không hiển thị giá trị "${optionText}" sau khi chọn`,
        },
      ).toContainText(optionText, { timeout: TIMEOUT.VALIDATE_WAITING });
    }
  }
  async inputDropDownTagsList(testId: string, tags: string[]) {
    const element = this.getElement(testId);
    await element.click();
    // Nhập tag thứ 1
    for (const tag of tags) {
      await element.pressSequentially(tag);
      await this.wait(500); // Chờ dropdown load ra
      await this.enter();
    }
  }
  async inputTextArea(testId: string, text: string) {
    await this.getElement(testId).fill(text);
    await expect(this.getElement(testId)).toHaveValue(text);
  }
  async inputDatetime(testId: string, datetime: string) {
    await this.clickButton(testId);
    await this.getElement(testId).fill(datetime);
    await this.enter();
    await expect(this.getElement(testId), {
      message: `Ô ngày tháng [${testId}] không hiển thị giá trị "${datetime}" sau khi nhập`,
    }).toHaveValue(datetime, { timeout: TIMEOUT.VALIDATE_WAITING });
  }

  /**
   *
   * @param order thứ tự của people picker trên form, tính từ 1 trở đi. Ví dụ: 1 = người tạo, 2 = người duyệt, v.v... (cái này để xác định được selector chính xác vì có thể có nhiều people picker trên form)
   * @param name text tìm kiếm user
   */
  async inputPeoplePicker(
    testId: string,
    value: string,
    additionalCount?: number,
  ) {
    const emails = value.split(",").map((email) => email.trim());
    const peoplePicker = this.getElement(testId);
    for (const email of emails) {
      await peoplePicker.locator(".ant-select-selection-search-input").click();
      await peoplePicker
        .locator(".ant-select-selection-search-input")
        .pressSequentially(email, { delay: 200 });
      await this.wait(5000); // Chờ kết quả load ra
      await this.enter();
    }
    // await expect(peoplePicker.locator(".ant-tag"), {
    //   message: `People picker [${testId}] không hiển thị đủ ${emails.length + (additionalCount || 0)} người sau khi chọn`,
    // }).toHaveCount(emails.length + (additionalCount || 0), {
    //   timeout: TIMEOUT.VALIDATE_WAITING,
    // });
  }

  async inputRelatedECM(testId: string, order?: number) {
    //Bấm nút thêm
    await this.clickButton(testId);
    //bấm vào ecm đầu tiên

    await this.page
      .locator(".ant-modal-root")
      .last()
      .locator(".ant-table-row")
      .first()
      .hover();
    await this.page
      .locator(".ant-modal-root")
      .last()
      .locator(".ant-table-row .ant-checkbox-wrapper")
      .first()
      .click();

    await this.page.getByTestId("btn-add-related").click();
    await this.isVisible("related-item-table");
  }

  async clearLastTag(testId: string) {
    const tags = this.getElement(testId).locator(".ant-select-selection-item");
    const tagCount = await tags.count();

    const lastTag = tags.last();
    const lastTagText = await lastTag.textContent();

    const lastRemoveBtn = lastTag.locator(".ant-select-selection-item-remove");
    const btnCount = await lastRemoveBtn.count();
    const btnVisible = btnCount > 0 ? await lastRemoveBtn.isVisible() : false;

    if (btnCount === 0) {
      throw new Error(
        `[clearLastTag] Không tìm thấy nút remove trên tag cuối của "${testId}"`,
      );
    }

    await this.getElement(testId).locator("input").click();
    await this.page.waitForTimeout(200);
    await this.page.keyboard.press("Backspace");
    await this.page.keyboard.press("Backspace");
    await this.page.waitForTimeout(300);
  }

  async clearValue(testId: string) {
    if (testId.startsWith("sel-") || testId.startsWith("tree-sel-")) {
      await this.getElement(testId).locator(".ant-select-clear").click();
    }
    if (testId.startsWith("txt-") || testId.startsWith("txa-")) {
      await this.getElement(testId).fill("");
    }
  }

  async getValue(testId: string) {
    if (testId.startsWith("sel-") || testId.startsWith("tree-sel-")) {
      return await this.getElement(testId)
        .locator(".ant-select-selection-item")
        .innerText();
    }
  }

  private removeAccents(str: string): string {
    return str
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .toLowerCase();
  }

  async batchInput(data: PWData[], checkBranch = false) {
    for (const item of data) {
      // if (!item.value) continue;
      if (item.testId.startsWith("sel-tags")) {
        await this.inputDropDownTagsList(item.testId, item.value.split(","));
        continue;
      }
      if (item.testId.startsWith("txt-")) {
        await this.inputText(item.testId, item.value);
        continue;
      }
      if (item.testId.startsWith("txa-")) {
        await this.inputTextArea(item.testId, item.value);
        continue;
      }
      if (item.testId.startsWith("date-")) {
        await this.inputDatetime(item.testId, item.value);
        continue;
      }
      if (item.testId.startsWith("sel-")) {
        await this.inputDropDownList(item.testId, item.value);
        if (item.testId == "sel-loaiBoHoSo" && checkBranch) {
          //kiểm tra Loại hồ sơ có phải là loại dự án hay không
          const dropdownDuAn = this.getElement("sel-duAn");
          let dangHienThi = false;

          try {
            // Chờ tối đa 2 giây xem nó có ngoi lên không
            await dropdownDuAn.waitFor({ state: "visible", timeout: 2000 });
            dangHienThi = true;
          } catch (e) {
            dangHienThi = false; // Quá 2 giây không thấy -> Chắc chắn nó bị ẩn
          }

          if (dangHienThi) {
            await this.inputDropDownList("sel-duAn");
          }
        }
        if (item.testId == "sel-loaibohoso" && checkBranch) {
          await this.inputDropDownList("sel-duan");
        }
        continue;
      }
      if (item.testId.startsWith("tree-sel-")) {
        await this.inputTreeDropDown(
          item.testId,
          item.value,
          item.extraLocator,
        );
        continue;
      }
      if (item.testId.startsWith("pp-multi-")) {
        await this.inputPeoplePicker(item.testId, item.value);
        continue;
      }
      if (item.testId.startsWith("btn-add-related-ecm")) {
        await this.inputRelatedECM(item.testId);
        continue;
      }
    }
  }
}
