import { expect, Locator } from '@playwright/test';
import { UITypes } from 'nocodb-sdk';
import { GridPage } from '../../Grid';
import BasePage from '../../../Base';
import { AttachmentCellPageObject } from './AttachmentCell';
import { SelectOptionCellPageObject } from './SelectOptionCell';
import { SharedFormPage } from '../../../SharedForm';
import { CheckboxCellPageObject } from './CheckboxCell';
import { RatingCellPageObject } from './RatingCell';
import { DateCellPageObject } from './DateCell';
import { DateTimeCellPageObject } from './DateTimeCell';
import { GeoDataCellPageObject } from './GeoDataCell';
import { getTextExcludeIconText } from '../../../../tests/utils/general';
import { YearCellPageObject } from './YearCell';
import { TimeCellPageObject } from './TimeCell';
import { GroupPageObject } from '../../Grid/Group';
import { UserOptionCellPageObject } from './UserOptionCell';
import { SurveyFormPage } from '../../SurveyForm';
import { ButtonCellPageObject } from './ButtonCell';

export interface CellProps {
  indexMap?: Array<number>;
  index?: number;
  columnHeader: string;
}

export class CellPageObject extends BasePage {
  readonly parent: GridPage | SharedFormPage | SurveyFormPage | GroupPageObject;
  readonly selectOption: SelectOptionCellPageObject;
  readonly attachment: AttachmentCellPageObject;
  readonly checkbox: CheckboxCellPageObject;
  readonly rating: RatingCellPageObject;
  readonly year: YearCellPageObject;
  readonly time: TimeCellPageObject;
  readonly geoData: GeoDataCellPageObject;
  readonly date: DateCellPageObject;
  readonly dateTime: DateTimeCellPageObject;
  readonly userOption: UserOptionCellPageObject;
  readonly button: ButtonCellPageObject;

  constructor(parent: GridPage | SharedFormPage | SurveyFormPage | GroupPageObject) {
    super(parent.rootPage);
    this.parent = parent;
    this.selectOption = new SelectOptionCellPageObject(this);
    this.attachment = new AttachmentCellPageObject(this);
    this.checkbox = new CheckboxCellPageObject(this);
    this.rating = new RatingCellPageObject(this);
    this.year = new YearCellPageObject(this);
    this.time = new TimeCellPageObject(this);
    this.geoData = new GeoDataCellPageObject(this);
    this.date = new DateCellPageObject(this);
    this.dateTime = new DateTimeCellPageObject(this);
    this.userOption = new UserOptionCellPageObject(this);
    this.button = new ButtonCellPageObject(this);
  }

  get({ indexMap, index, columnHeader }: CellProps): Locator {
    if (this.parent instanceof SharedFormPage) {
      return this.parent.get().locator(`[data-testid="nc-form-input-cell-${columnHeader}"]`).first();
    } else if (this.parent instanceof SurveyFormPage) {
      return this.parent
        .get()
        .locator(`[data-testid="nc-survey-form__input-${columnHeader.replace(' ', '')}"]`)
        .first();
    } else if (this.parent instanceof GridPage) {
      return this.parent.get().locator(`td[data-testid="cell-${columnHeader}-${index}"]`).first();
    } else {
      return this.parent.get({ indexMap }).locator(`td[data-testid="cell-${columnHeader}-${index}"]`).first();
    }
  }

  async click({ index, columnHeader }: CellProps, ...options: Parameters<Locator['click']>) {
    await this.get({ index, columnHeader }).click(...options);
    await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');
  }

  async dblclick({ index, columnHeader }: CellProps) {
    return await this.get({ index, columnHeader }).dblclick();
  }

  async fillText({ index, columnHeader, text, type }: CellProps & { text: string; type?: UITypes }) {
    await this.dblclick({
      index,
      columnHeader,
    });
    const isInputBox = async () => (await this.get({ index, columnHeader }).locator('input').count()) > 0;

    for (let i = 0; i < 10; i++) {
      if (await isInputBox()) {
        break;
      }
      await this.rootPage.waitForTimeout(200);
    }

    if (await isInputBox()) {
      await this.get({ index, columnHeader }).locator('input').fill(text);

      if (type && [UITypes.Date, UITypes.Time, UITypes.Year, UITypes.DateTime].includes(type)) {
        await this.rootPage.keyboard.press('Enter');
        await this.rootPage.locator('.nc-dropdown.active').waitFor({ state: 'hidden' });
      }
    } else {
      await this.get({ index, columnHeader }).locator('textarea').fill(text);
    }
  }

  async inCellExpand({ index, columnHeader }: CellProps) {
    // await this.get({ index, columnHeader }).hover();
    await this.waitForResponse({
      uiAction: () => this.get({ index, columnHeader }).locator('.nc-datatype-link').click(),
      requestUrlPathToMatch: '/api/v1/db/data/noco',
      httpMethodsToMatch: ['GET'],
    });
  }

  async inCellAdd({ index, columnHeader }: CellProps) {
    await this.get({ index, columnHeader }).hover();
    await this.get({ index, columnHeader }).locator('.nc-action-icon.nc-plus').click();
  }

  async verifyCellActiveSelected({ index, columnHeader }: CellProps) {
    await expect(this.get({ index, columnHeader })).toHaveClass(/active/);
  }

  async verifyCellEditable({ index, columnHeader }: CellProps) {
    await this.get({ index, columnHeader }).isEditable();
  }

  async verify({ index, columnHeader, value }: CellProps & { value: string | string[] }) {
    if (this.parent instanceof GridPage) {
      await this.parent.waitForRowSaveSpinnerToDisappear(index);
    }

    const _verify = async text => {
      // await expect
      //   .poll(async () => {
      //     const innerTexts = await this.get({
      //       index,
      //       columnHeader,
      //     }).allInnerTexts();
      //     return typeof innerTexts === 'string' ? [innerTexts] : innerTexts;
      //   })
      //   .toContain(text);

      // retrieve text from cell
      // loop for 5 seconds
      // if text is found, return
      // if text is not found, throw error
      let count = 0;

      if (!(this.parent instanceof SharedFormPage)) {
        await this.rootPage.locator(`td[data-testid="cell-${columnHeader}-${index}"]`).waitFor({ state: 'visible' });
      }

      await this.get({
        index,
        columnHeader,
      }).waitFor({ state: 'visible' });

      await this.get({
        index,
        columnHeader,
      }).scrollIntoViewIfNeeded();
      while (count < 5) {
        const innerTexts = await getTextExcludeIconText(this.get({ index, columnHeader }));
        const cellText = typeof innerTexts === 'string' ? [innerTexts] : innerTexts;

        if (cellText) {
          if (cellText?.includes(text) || cellText[0]?.includes(text)) {
            return;
          }
        }
        await this.rootPage.waitForTimeout(1000);
        count++;
        if (count === 5) {
          console.log('cellText', cellText);
          console.log('text', text);

          throw new Error(`Cell text "${text}" not found`);
        }
      }
    };

    if (Array.isArray(value)) {
      for (const text of value) {
        await _verify(text);
      }
    } else {
      await _verify(value);
    }
  }

  async verifyGeoDataCell({
    index,
    columnHeader,
    lat,
    long,
  }: {
    index: number;
    columnHeader: string;
    lat: string;
    long: string;
  }) {
    const _verify = async expectedValue => {
      await expect
        .poll(async () => {
          const cell = await this.get({
            index,
            columnHeader,
          }).locator(`[data-testid="nc-geo-data-lat-long-set"]`);
          return await cell.textContent(); //.getAttribute('title');
        })
        .toEqual(expectedValue);
    };

    const value = `${lat}; ${long}`;
    await _verify(value);
  }

  async verifyDateCell({ index, columnHeader, value }: { index: number; columnHeader: string; value: string }) {
    const _verify = async expectedValue => {
      await expect
        .poll(async () => {
          const cell = await this.get({
            index,
            columnHeader,
          }).locator('.nc-date-picker');
          return await cell.getAttribute('title');
        })
        .toEqual(expectedValue);
    };

    await _verify(value);
  }

  async verifyQrCodeCell({
    index,
    columnHeader,
    expectedSrcValue,
  }: CellProps & {
    expectedSrcValue: string;
  }) {
    const _verify = async expectedQrCodeImgSrc => {
      await expect
        .poll(async () => {
          const qrCell = await this.get({
            index,
            columnHeader,
          });
          const qrImg = await qrCell.getByRole('img');
          const qrImgSrc = await qrImg.getAttribute('src');
          return qrImgSrc;
        })
        .toEqual(expectedQrCodeImgSrc);
    };

    await _verify(expectedSrcValue);
  }

  async verifyBarcodeCellShowsInvalidInputMessage({ index, columnHeader }: { index: number; columnHeader: string }) {
    const _verify = async expectedInvalidInputMessage => {
      await expect
        .poll(async () => {
          const barcodeCell = await this.get({
            index,
            columnHeader,
          });
          const barcodeInvalidInputMessage = await barcodeCell.getByTestId('barcode-invalid-input-message');
          return await barcodeInvalidInputMessage.textContent();
        })
        .toEqual(expectedInvalidInputMessage);
    };

    await _verify('Barcode error - please check compatibility between input and barcode type');
  }

  async verifyBarcodeCell({
    index,
    columnHeader,
    expectedSvgValue,
  }: {
    index: number;
    columnHeader: string;
    expectedSvgValue: string;
  }) {
    const _verify = async (expectedBarcodeSvg: unknown) => {
      await expect
        .poll(async () => {
          const barcodeCell = this.get({ index, columnHeader });
          const barcodeSvg = barcodeCell.getByTestId('barcode');
          return await barcodeSvg.innerHTML();
        })
        .toEqual(expectedBarcodeSvg);
    };

    await _verify(expectedSvgValue);
  }

  // todo: Improve param names (i.e value => values)
  // verifyVirtualCell
  //  : virtual relational cell- HM, BT, MM
  //  : verify link count & cell value
  //
  async verifyVirtualCell({
    index,
    columnHeader,
    type,
    count,
    value,
    verifyChildList = false,
    options,
  }: CellProps & {
    count?: number;
    type?: string;
    value?: string[];
    verifyChildList?: boolean;
    options?: { singular?: string; plural?: string };
  }) {
    const cell = this.get({ index, columnHeader });
    const linkText = cell.locator('.nc-datatype-link');

    await cell.scrollIntoViewIfNeeded();

    // lazy load - give enough time for cell to load
    await this.rootPage.waitForTimeout(1000);

    if (type === 'bt') {
      const chips = cell.locator('.chips > .chip');
      expect(await chips.count()).toBe(count);

      for (let i = 0; i < value.length; ++i) {
        await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
        await chips.nth(i).locator('.name').scrollIntoViewIfNeeded();
        await expect(chips.nth(i).locator('.name')).toHaveText(value[i]);
      }
      return;
    }

    // verify chip count & contents
    if (count) {
      const expectedText = `${count} ${count === 1 ? options.singular : options.plural}`;
      let retryCount = 0;
      while (retryCount < 5) {
        const receivedText = await linkText.innerText();
        if (receivedText.includes(expectedText)) {
          break;
        }
        retryCount++;
        // add delay of 100ms
        await this.rootPage.waitForTimeout(100 * retryCount);
      }
      expect(await cell.innerText()).toContain(expectedText);
    }

    if (verifyChildList) {
      // open child list
      await this.get({ index, columnHeader }).hover();

      // arrow expand doesn't exist for bt columns
      if (await linkText.count()) {
        await this.waitForResponse({
          uiAction: () => linkText.click(),
          requestUrlPathToMatch: '/api/v1',
          httpMethodsToMatch: ['GET'],
        });

        // wait for child list to open
        await this.rootPage.waitForSelector('.nc-modal-child-list:visible');

        // verify child list count & contents
        await expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count);

        // close child list
        // await this.rootPage.locator('.nc-modal-child-list').locator('.nc-close-btn').last().click();
        await this.rootPage.locator('.nc-modal-child-list').getByTestId('nc-link-count-info').click();
        await this.rootPage.keyboard.press('Escape');
      }
    }
  }

  async unlinkVirtualCell({ index, columnHeader }: CellProps) {
    const cell = this.get({ index, columnHeader });
    const isLink = await cell.locator('.nc-datatype-link').count();

    // Count will be 0 for BT columns
    if (!isLink) {
      await cell.click();
      await cell.locator('.nc-icon.unlink-icon').click();
      // await cell.click();
    }

    // For HM/MM columns
    else {
      await cell.locator('.nc-datatype-link').click();
      await this.rootPage.locator('.nc-links-dropdown.active').waitFor({ state: 'visible' });
      await this.rootPage
        .locator(`[data-testid="nc-child-list-item"]`)
        .last()
        .waitFor({ state: 'visible', timeout: 3000 });

      await this.waitForResponse({
        uiAction: () =>
          this.rootPage
            .locator(`[data-testid="nc-child-list-item"]`)
            .last()
            .locator('button.nc-list-item-link-unlink-btn')
            .click({ force: true, timeout: 3000 }),
        requestUrlPathToMatch: '/api/v1/db/data/noco',
        httpMethodsToMatch: ['DELETE'],
      });

      await this.rootPage.keyboard.press('Escape');
      await this.rootPage.keyboard.press('Escape');
    }
  }

  async verifyRoleAccess(param: { role: string }) {
    const role = param.role.toLowerCase();
    const isEditAccess = role === 'creator' || role === 'editor' || role === 'owner';
    // normal text cell
    const cell = this.get({ index: 0, columnHeader: 'Country' });
    // editable cell
    await cell.dblclick();
    await expect(cell.locator(`input`)).toHaveCount(isEditAccess ? 1 : 0);

    // press escape to close the input
    await cell.press('Escape');
    await cell.press('Escape');

    await cell.click({ button: 'right', clickCount: 1 });
    await expect(this.rootPage.locator(`.nc-dropdown-grid-context-menu:visible`)).toHaveCount(1);

    // virtual cell
    const vCell = this.get({ index: 0, columnHeader: 'Cities' });
    await vCell.hover();
    // in-cell add
    await expect(vCell.locator('.nc-action-icon.nc-plus:visible')).toHaveCount(isEditAccess ? 1 : 0);

    // virtual cell link text
    const linkText = await getTextExcludeIconText(vCell);
    expect(linkText).toContain('1 City');
  }

  async copyCellToClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) {
    if (this.parent instanceof GridPage) await this.parent.renderColumn(columnHeader);
    await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
    await this.get({ index, columnHeader }).click(...clickOptions);
    await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');

    await this.get({ index, columnHeader }).press((await this.isMacOs()) ? 'Meta+C' : 'Control+C');
    await this.verifyToast({ message: 'Copied to clipboard' });
  }

  async pasteFromClipboard({ index, columnHeader }: CellProps, ...clickOptions: Parameters<Locator['click']>) {
    await this.get({ index, columnHeader }).scrollIntoViewIfNeeded();
    await this.get({ index, columnHeader }).click(...clickOptions);
    await (await this.get({ index, columnHeader }).elementHandle()).waitForElementState('stable');

    await this.get({ index, columnHeader }).press((await this.isMacOs()) ? 'Meta+V' : 'Control+V');

    // kludge: wait for paste to complete
    await this.rootPage.waitForTimeout(1000);
  }
}
