/*******************************
NE PAS MODIFIER, FICHIER GENERE
*******************************/

/**
 * Software Name : UUV
 *
 * SPDX-License-Identifier: MIT
 *
 * This software is distributed under the MIT License,
 * see the "LICENSE" file for more details
 *
 * Authors: NJAKO MOLOM Louis Fredice & SERVICAL Stanley
 * Software description: Make test writing fast, understandable by any human
 * understanding English or French.
 */

import { DEFAULT_TIMEOUT, fs, KEY_PRESS } from "@uuv/runner-commons";

import { checkA11y, injectAxe } from "axe-playwright";
import { devices, expect } from "@playwright/test";
import { Locator, Page } from "playwright";
import { DataTable } from "@cucumber/cucumber";
import {
    addCookie,
    click,
    COOKIE_NAME,
    deleteCookieByName,
    FILTER_TYPE,
    findWithRoleAndName,
    findWithRoleAndNameAndContent,
    findWithRoleAndNameAndContentDisable,
    findWithRoleAndNameAndContentEnable,
    findWithRoleAndNameFocused,
    getCookie,
    getPageOrElement,
    getTimeout,
    MockCookie,
    notFoundWithRoleAndName,
    SelectedElementCookie,
    TimeoutCookie,
    withinRoleAndName
} from "../core-engine";
import { Given, Then, When, World } from "../../../preprocessor/run/world";
import { ContextObject, RunOptions } from "axe-core";
import path from "path";

/**
 * Sets the viewport dimensions with one of the presets defined by your runtime engine as Cypress: [Link](https://docs.cypress.io/api/commands/viewport#Arguments) or Playwright: [Link](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json)
 * */
Given(`I set viewport to preset {string}`, async function(viewportPreset: string) {
  await this.page.setViewportSize(devices[viewportPreset].viewport);
});

/**
 * Sets the viewport dimensions to the specified width and length
 * */
Given(
 `I set viewport with width {int} and height {int}`, async function
 (width: number, height: number) {
   await this.page.setViewportSize({ width: width, height: height });
 }
);

/**
 * Start a keyboard navigation session from the top of the page<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
Given(
 `I start a keyboard navigation from the top of the page`,
 async function() {
   await this.page.mouse.click(0.5, 0.5);
 }
);

/**
 * Navigate to the Uri passed as a argument (full url consisting of the BASE_URL + Uri) or navigate to Url if begin with http:// or https://
 * */
Given(`I visit path {string}`, async function(siteUrl: string) {
  await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT);
  await this.page.goto(`${siteUrl}`);
});

/**
 * Triggers a click on the selected element.<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases.
 * */
When(`I click`, async function() {
  const keyBoardFocusTargetObj = keyBoardFocusTarget(this);
  if ((await keyBoardFocusTargetObj.count()) === 1) {
    await keyBoardFocusTargetObj.click({ timeout: DEFAULT_TIMEOUT });
  } else {
    await getPageOrElement(this).then((element: Locator) => element.click({ timeout: DEFAULT_TIMEOUT }));
  }
});

/**
 * Triggers a click on the element with given role and specific name
 * */
When(`I click on element with role {string} and name {string}`, async function(role: string, name: string) {
  await click(this, role, name);
});

// TODO : permet de gérer les label accessibles donc pas que les aria : https://playwright.dev/docs/api/class-locator#locator-get-by-label
/**
 * Selects the element whose aria-label is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it
 * */
When(`within the element with aria-label {string}`, async function(expectedAriaLabel: string) {
  const sanitizedExpectedAriaLabel = encodeURIComponent(expectedAriaLabel).replaceAll("%20", " ");
  await getPageOrElement(this).then(async (element) => {
    const locator = element.getByLabel(sanitizedExpectedAriaLabel, { exact: true });
    await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) });
    await locator.focus({ timeout: 10000 });
  });
  await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.ARIA_LABEL, sanitizedExpectedAriaLabel));
});

/**
 * Deletes selected element and timeout
 * */
When(`I reset context`, async function() {
  await deleteCookieByName(this, COOKIE_NAME.SELECTED_ELEMENT);
  await deleteCookieByName(this, COOKIE_NAME.TIMEOUT);
  const keyBoardFocusTargetObj = keyBoardFocusTarget(this);
  if ((await keyBoardFocusTargetObj.count()) === 1) {
    await keyBoardFocusTargetObj.blur();
  }
});

/**
 * Selects the element whose selector is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it
 * */
When(`within the element with selector {string}`, async function(selector: string) {
  await getPageOrElement(this).then(async (element) => {
    const locator = element.locator(selector);
    await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) });
    await locator.focus({ timeout: 10000 });
  });
  await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.SELECTOR, selector));
});

/**
 * Writes the sentence passed as a parameter (useful for example to fill in a form field).<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases.
 * */
When(`I type the sentence {string}`, async function(textToType: string) {
    await type(this, textToType);
});

/**
 * Writes the sentence passed as a parameter (useful for example to fill in a form field).<br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases.
 * */
When(`I enter the value {string}`, async function(textToType: string) {
    await type(this, textToType);
});

/**
 * Press specified key multiple times using | as: num|{key}
 * */
When(`I press {int} times on {string}`, async function(nbTimes: number, key: string) {
  for (let i = 1; i <= nbTimes; i++) {
    await pressKey(this, key);
  }
});

/**
 * Press specified key: <table><thead><tr><th>Key</th><th>Description</th></tr></thead><tbody><tr><td>{tab}</td><td>Tabulation</td></tr><tr><td>{reverseTab}</td><td>Reverse tabulation</td></tr><tr><td>{down}</td><td>Arrow Down</td></tr><tr><td>{right}</td><td>Arrow Right</td></tr><tr><td>{left}</td><td>Arrow Left</td></tr><tr><td>{up}</td><td>Arrow Up</td></tr></tbody></table><br/>Make sure you've selected an element beforehand with the <strong>within...</strong> phrases.
 * */
When(`I press {string}`, async function(key: string) {
  await pressKey(this, key);
});

/**
 * Move to the previous html element that can be reached with back Tab<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
When(`I go to previous keyboard element`, async function() {
  await this.page.keyboard.press("ShiftLeft+Tab");
});

/**
 * Move to the next html element that can be reached with Tab<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
When(`I go to next keyboard element`, async function() {
  await this.page.keyboard.press("Tab");
});

/**
 * Sets the timeout value (in millisecond) for finding element in the DOM <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it
 * */
When(`I set timeout with value {int}`, async function(newTimeout: number) {
  await addCookie(this, COOKIE_NAME.TIMEOUT, new TimeoutCookie("timeout", newTimeout));
});

/**
 * Selects the element whose [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and accessible [name](https://russmaxdesign.github.io/html-elements-names/) are specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it
 * */
When(`within the element with role {string} and name {string}`, async function(role: string, name: string) {
  await withinRoleAndName(this, role, name);
});

/**
 * Selects the element whose data-testId attribute is specified <br />⚠ remember to deselect the element with <b>[I reset context](#i-reset-context)</b> if you're no longer acting on it
 * */
When(`within the element with testId {string}`, async function(testId: string) {
  testId = encodeURIComponent(testId);
  await getPageOrElement(this).then(async (element) => {
    const locator = element.getByTestId(testId);
    await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) });
    await locator.focus({ timeout: 10000 });
  });
  await addCookie(this, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.TEST_ID, testId));
});

/**
 * Mock a named API response with body. <i>If you use Playwright as runtime engine, <b>request</b> and <b>named</b> are unused.</i>
 * */
When(
 `I mock a request {} on url {string} named {string} with content {}`,
 async function(verb: string, url: string, name: string, body: any) {
   await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb));
   await this.page.route(url, async route => {
     await route.fulfill({ body });
     await afterMock(this, url, verb, name);
   });
 }
);

/**
 * Mock a named API response with status code
 * */
When(
 `I mock a request {} on url {string} named {string} with status code {int}`,
 async function(verb: string, url: string, name: string, statusCode: number) {
   await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb));
   await this.page.route(url, async route => {
     await route.fulfill({ status: statusCode });
     await afterMock(this, url, verb, name);
   });
 }
);

/**
 * Mock a named API response with file's extension .json, .js, .coffee, .html, .txt, .csv, .png, .jpg, .jpeg, .gif, .tif, .tiff, .zip
 * */
When(
 `I mock a request {} on url {string} named {string} with fixture {}`,
 async function(verb: string, url: string, name: string, fixture: any) {
   await addCookie(this, COOKIE_NAME.MOCK_URL, new MockCookie(name, url, verb));
   const data = fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${fixture}`), "utf8");
   await this.page.route(url, async route => {
     await route.fulfill({ body: data });
     await afterMock(this, url, verb, name);

   });
 }
);

/**
 * Checks that an Html element exists with the specified selector
 * */
Then(`I should see an element with selector {string}`, async function(selector: string) {
  await getPageOrElement(this).then(async (element) => expect(element.locator(selector)).toHaveCount(1, { timeout: await getTimeout(this) }));
});

/**
 * Sets one or more headers to the indicated http request and only for the Http method (GET / POST / etc...) passed as a argument.<i> If you use Playwright as execution engine, <b>method</b> isn't used.</i>
 * */
When(
 `I set header(s) for uri {string} and method {string}`,
 async function(url: string, method: string, headersToSet: DataTable) {
   await this.page.route(url, async route => {
     const headers = route.request().headers();
     await route.continue({ headers: { ...headers, ...headersToSet.rowsHash() } });
   });
 }
);

/**
 * Sets one or more headers to the indicated http request
 * */
When(
 `I set header(s) for uri {string}`,
 async function(url: string, headersToSet: DataTable) {
   await this.page.route(url, async route => {
     const headers = route.request().headers();
     await route.continue({ headers: { ...headers, ...headersToSet.rowsHash() } });
   });
 }
);

/**
 * Checks the current html page have the specified title
 * */
Then(`I should see the page title {string}`, async function(pageTitle: string) {
    await expect(this.page).toHaveTitle(pageTitle);
});

/**
 * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/)
 * */
Then(`I should see an element with role {string} and name {string}`, async function(role: string, name: string) {
  await findWithRoleAndName(this, role, name);
});

/**
 * Checks that an Html element exists with the specified content
 * */
Then(`I should see an element with content {string}`, async function(textContent: string) {
  // TODO partie pris de faire en exactitude. A voir si on doit faire 2 phrases https://playwright.dev/docs/api/class-locator#locator-get-by-text
  await getPageOrElement(this).then(async (element) => expect(element.getByText(textContent, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) }));
});

/**
 * Checks that an Html element does not exists with the specified content
 * */
Then(`I should not see an element with content {string}`, async function(textContent: string) {
  await getPageOrElement(this).then(async (element) => expect(element.getByText(textContent, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) }));
});

/**
 * Checks that an Html element exists with the specified data-testid attribute
 * */
Then(`I should see an element with testId {string}`, async function(testId: string) {
  testId = encodeURIComponent(testId);
  await getPageOrElement(this).then(async (element) => expect(element.getByTestId(testId, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) }));
});

/**
 * Checks that an Html element does not exists with the specified data-testid attribute
 * */
Then(`I should not see an element with testId {string}`, async function(testId: string) {
  testId = encodeURIComponent(testId);
  await getPageOrElement(this).then(async (element) => expect(element.getByTestId(testId, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) }));
});

/**
 * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md)
 * */
Then(
 `I should not have any axe-core accessibility issue`,
 async function() {
   await injectAxe(this.page as Page);
   await checkA11y(this.page as Page);
 });

/**
 * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with an option](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#options-parameter)
 * */
Then(
 `I should not have any axe-core accessibility issue with option json fixture {}`,
 async function(option: any) {
   await injectAxe(this.page as Page);
   const optionFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${option}`));
   const optionJson = JSON.parse(optionFile.toString());
   await checkA11y(this.page as Page, undefined, {
     axeOptions: optionJson as RunOptions
   });
 });

function getConfigDir(): string {
  // eslint-disable-next-line dot-notation
  return process.env["CONFIG_DIR"] ? process.env["CONFIG_DIR"] : "";
}

/**
 * Check that the current page have no accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with an option](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#options-parameter) on the specific [context](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#context-parameter)
 * */
Then(
 `I should not have any axe-core accessibility issue with context json fixture {} and option json fixture {}`,
 async function(context: any, option: any) {
   await injectAxe(this.page as Page);
   const contextFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${context}`));
   const optionFile = await fs.readFileSync(path.join(getConfigDir(), `playwright/fixtures/${option}`));
   const optionJson = JSON.parse(optionFile.toString());
   await checkA11y(this.page as Page, JSON.parse(contextFile.toString()) as ContextObject, {
     axeOptions: optionJson as RunOptions
   });
 });

/**
 * Check that the current page have not critical accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md)
 * */
Then(
 `I should not have any axe-core critical accessibility issue`,
 async function() {
   await injectAxe(this.page as Page);
   await checkA11y(this.page as Page, undefined, {
     includedImpacts: ["critical"]
   });
 });

/**
 * Check that the current page have not accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) with one or more impacts in: 'minor','moderate','serious','critical'
 * */
Then(
 `I should not have any axe-core accessibility issue with {} impact(s)`,
 async function(impacts: any) {
   await injectAxe(this.page as Page);
   await checkA11y(this.page as Page, undefined, {
     includedImpacts: [impacts]
   });
 });

/**
 * Check that the current page have not accessibility issue for [axe-core](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md) [with one or more Accessibility Standards](https://github.com/dequelabs/axe-core/blob/HEAD/doc/API.md#axe-core-tags)
 * */
Then(
 `I should not have any axe-core accessibility issue with accessibility standard(s) {}`,
 async function(tags: any) {
   await injectAxe(this.page as Page);
   await checkA11y(this.page as Page, undefined, {
     axeOptions: {
       runOnly: {
         type: "tag",
         values: [tags]
       }
     }
   });
 });

/**
 * Checks that an Html element does not exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/)
 * */
Then(
 `I should not see an element with role {string} and name {string}`,
 async function(role: string, name: string) {
   await notFoundWithRoleAndName(this, role, name);
 }
);

/**
 * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content
 * */
Then(
 `I should see an element with role {string} and name {string} and content {string}`,
 async function(expectedRole: string, name: string, expectedTextContent: string) {
   await findWithRoleAndNameAndContent(this, expectedRole, name, expectedTextContent);
 }
);

/**
 * Checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
Then(
 `the element with role {string} and name {string} should be keyboard focused`,
 async function(expectedRole: string, name: string) {
   await findWithRoleAndNameFocused(this, expectedRole, name);
 }
);

/**
 * Checks that the Html element with the specified selector is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
Then(
 `the element with selector {string} should be keyboard focused`,
 async function(selector: string) {
   await getPageOrElement(this).then(async (element) => {
     const locator = element.locator(selector);
     await locator.focus({ timeout: 10000 });
     await expect(locator).toHaveCount(1, { timeout: await getTimeout(this) });
     await expect(locator).toBeFocused();
   });
 });

/**
 * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content, and with the disabled attribute set to true
 * */
Then(
 `I should see an element with role {string} and name {string} and content {string} disabled`,
 async function(expectedRole: string, name: string, expectedTextContent: string) {
   await findWithRoleAndNameAndContentDisable(this, expectedRole, name, expectedTextContent);
 }
);

/**
 * Checks that an Html element exists with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types), [name](https://russmaxdesign.github.io/html-elements-names/) and content, and with the disabled attribute set to false
 * */
Then(
 `I should see an element with role {string} and name {string} and content {string} not disabled`,
 async function(expectedRole: string, name: string, expectedTextContent: string) {
   await findWithRoleAndNameAndContentEnable(this, expectedRole, name, expectedTextContent);
 }
);

/**
 * Checks that an Html element exists with the specified aria-label attribute
 * */
Then(`I should see an element with aria-label {string}`, async function(expectedAriaLabel: string) {
  expectedAriaLabel = encodeURIComponent(expectedAriaLabel);
  await getPageOrElement(this).then(async (element) => expect(element.getByLabel(expectedAriaLabel, { exact: true })).toHaveCount(1, { timeout: await getTimeout(this) }));
});

/**
 * Checks that an Html element does not exists with the specified aria-label attribute
 * */
Then(`I should not see an element with aria-label {string}`, async function(expectedAriaLabel: string) {
  expectedAriaLabel = encodeURIComponent(expectedAriaLabel);
  await getPageOrElement(this).then(async (element) => expect(element.getByLabel(expectedAriaLabel, { exact: true })).toHaveCount(0, { timeout: await getTimeout(this) }));
});

/**
 * Checks that an Html element exists with the specified aria-label attribute and content
 * */
Then(`I should see an element with aria-label {string} and content {string}`, async function(expectedAriaLabel: string, expectedTextContent: string) {
  expectedAriaLabel = encodeURIComponent(expectedAriaLabel);
  await getPageOrElement(this).then(async (element) => {
    const byLabel = element.getByLabel(expectedAriaLabel, { exact: true });
    await expect(byLabel).toHaveCount(1, { timeout: await getTimeout(this) });
    await expect(byLabel.filter({ hasText: expectedTextContent })).toHaveCount(1);
  });
});

/**
 * Move to the previous html element that can be reached with Tab and checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
When(`the previous keyboard element focused should have name {string} and role {string}`, async function(expectedRole: string, name: string) {
  await this.page.keyboard.press("ShiftLeft+Tab");
  await findWithRoleAndNameFocused(this, expectedRole, name);
});

/**
 * "Move to the next html element that can be reached with Tab and checks that the Html element with the specified [accessible role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles#aria_role_types) and [name](https://russmaxdesign.github.io/html-elements-names/) is focused<br/><a target='_blank' href='https://github.com/e2e-test-quest/uuv/blob/main/example/en-keyboard.feature'>Examples</a>
 * */
When(`the next keyboard element focused should have name {string} and role {string}`, async function(expectedRole: string, name: string) {
  await this.page.keyboard.press("Tab");
  await findWithRoleAndNameFocused(this, expectedRole, name);
});


/**
 * Wait that a named mock has been consumed until timeout
 * */
Then(`I should consume a mock named {string}`, async function(name: string) {
  const cookie = await getCookie(this, COOKIE_NAME.MOCK_URL);
  expect(() => {
    if (!cookie.isValid()) {
      throw new Error(`Mock ${name} must be defined before use this sentence`);
    }
  }).not.toThrow();

  await expect.poll(async () => {
    const cookie = await getCookie(this, COOKIE_NAME.MOCK_URL);
    const mockUrls: MockCookie[] = JSON.parse(cookie.value);
    const mockUrl = mockUrls.find(mock => mock.name === name);
    if (mockUrl) {
      return mockUrl?.isConsumed;
    }
    return false;
  }).toBeDefined();
});

/**
 * Wait milliseconds. <b>Warning:</b> use this sentence <b>in production</b> can make your <b>test flaky</b>.
 * */
Then(`I wait {int} ms`, async function(ms: number) {
  await this.page.waitForTimeout(ms);
});

/**
 * Checks Html attributes of the selected element
 * */
Then(
 `I should see these attributes with values`,
 async function(expectedAttributeList: DataTable) {
   await getPageOrElement(this).then(async (element) => {
     // console.debug("expectedAttributeList.raw(),", expectedAttributeList.raw())
     for (const expectedAttribute of expectedAttributeList.raw()) {
       const attributeName = expectedAttribute[0];
       const attributeValue = expectedAttribute[1];
       // await showAttributesInLocator(element);
       await expect(element).toHaveAttribute(attributeName, attributeValue);
     }
   });
 }
);

/**
 * Checks that there is a list with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nThen I should see a list named "test-list" and containing\n| First element   |\n| Second element  |\n| Third element   |\n```
 * */
Then(
 `I should see a list named {string} and containing`,
 async function(expectedListName: string, expectedElementsOfList: DataTable) {
   await withinRoleAndName(this, "list", expectedListName);
   await getPageOrElement(this).then(async (element) => {
     const listitem = await element.getByRole("listitem", { exact: true }).all();
     const foundedElement: any[] = [];
     for (const element of listitem) {
       const textContent = await element.textContent();
       foundedElement.push([textContent]);
     }
     await expect(foundedElement.length).toBeGreaterThan(0);
     // console.debug(`expected [${expectedElementsOfList.raw()}] to be [${foundedElement}]`);
     await expect(listitem.length).toEqual(expectedElementsOfList.raw().length);
     await expect(foundedElement).toEqual(expectedElementsOfList.raw());
   });
 }
);

/**
 * Checks that there is a grid with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/grid.html"\nThen I should see a grid named "HTML Grid Example" and containing\n| Make         | Model   | Price  |\n| ------------ | ------- | ------ |\n| Toyota       | Celica  | 35000  |\n| Ford         | Mondeo  | 32000  |\n| Porsche      | Boxster | 72000  |\n| BMW          | M50     | 60000  |\n| Aston Martin | DBX     | 190000 |\n```
 * */
Then(
    `I should see a grid named {string} and containing`,
    async function(expectedListName: string, pExpectedElementsOfList: DataTable) {
        const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList);
        await findWithRoleAndName(this, "grid", expectedListName);
        await getPageOrElement(this).then(async (element) => {
            await expectTableToHaveContent(element, expectedElementsOfList, "gridcell");
        });
    }
);

/**
 * Checks that there is a treegrid with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/treegrid.html"\nThen I should see a treegrid named "HTML Treegrid Example" and containing\n| Make         | Model   | Price  |\n| ------------ | ------- | ------ |\n| Toyota       | Celica  | 35000  |\n| Ford         | Mondeo  | 32000  |\n| Porsche      | Boxster | 72000  |\n| BMW          | M50     | 60000  |\n| Aston Martin | DBX     | 190000 |\n```
 * */
Then(
    `I should see a treegrid named {string} and containing`,
    async function(expectedListName: string, pExpectedElementsOfList: DataTable) {
        const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList);
        await findWithRoleAndName(this, "treegrid", expectedListName);
        await getPageOrElement(this).then(async (element) => {
            await expectTableToHaveContent(element, expectedElementsOfList, "gridcell");
        });
    }
);

/**
 * Checks that there is a table with the specified [name](https://russmaxdesign.github.io/html-elements-names/) containing list items.<br/> <u>Example</u>\n```gherkin\nWhen I visit path "https://e2e-test-quest.github.io/simple-webapp/table.html"\nThen I should see a table named "test-list" and containing\n| Company                       | Contact          | Country |\n| ----------------------------- | ---------------- | ------- |\n| Alfreds Futterkiste           | Maria Anders     | Germany |\n| Centro comercial Moctezuma    | Francisco Chang  | Mexico  |\n| Ernst Handel                  | Roland Mendel    | Austria |\n| Island Trading                | Helen Bennett    | UK      |\n| Laughing Bacchus Winecellars  | Yoshi Tannamuri  | Canada  |\n| Magazzini Alimentari Riuniti  | Giovanni Rovelli | Italy   |\n```
 * */
Then(
    `I should see a table named {string} and containing`,
    async function(expectedListName: string, pExpectedElementsOfList: DataTable) {
        const expectedElementsOfList = removeHeaderSeparatorLine(pExpectedElementsOfList);
        await findWithRoleAndName(this, "table", expectedListName);
        await getPageOrElement(this).then(async (element) => {
            await expectTableToHaveContent(element, expectedElementsOfList, "cell");
        });
    }
);

function removeHeaderSeparatorLine(pExpectedElementsOfList: DataTable) {
    const expectedElementsOfList = pExpectedElementsOfList.raw();
    if (expectedElementsOfList.length > 1) {
        expectedElementsOfList.splice(1, 1);
    }
    return expectedElementsOfList;
}

async function expectTableToHaveContent(element: Locator, expectedElementsOfList: string[][], pCellAccessibleRole: string) {
    const rows = await element.getByRole("row", { exact: true }).all();
    return await Promise.all(rows.map(async (row: Locator, rowNumber: number) => {
        const cellAccessibleRole = rowNumber === 0 ? "columnheader" : pCellAccessibleRole;
        const cellsElement = await row.getByRole(cellAccessibleRole as any, { exact: true }).all();
        let cellNumber = 0;
        return await Promise.all(cellsElement.map((cell: Locator) => {
            const expectedValue = expectedElementsOfList[rowNumber][cellNumber];
            expect(cell, { message: `${cellAccessibleRole} at index [${rowNumber}, ${cellNumber}] should be ${expectedValue}` }).toHaveAccessibleName(expectedValue);
            cellNumber++;
        }));
    }));
}

async function pressKey(world: World, key: string) {
  switch (key) {
    case KEY_PRESS.TAB:
      await world.page.keyboard.press("Tab");
      break;
    case KEY_PRESS.REVERSE_TAB:
      await world.page.keyboard.press("ShiftLeft+Tab");
      break;
    case KEY_PRESS.UP:
      await world.page.keyboard.press("ArrowUp");
      break;
    case KEY_PRESS.DOWN:
      await world.page.keyboard.press("ArrowDown");
      break;
    case KEY_PRESS.LEFT:
      await world.page.keyboard.press("ArrowLeft");
      break;
    case KEY_PRESS.RIGHT:
      await world.page.keyboard.press("ArrowRight");
      break;
    default:
      console.error("the command" + key + " is unrecognized.");
      break;
  }
  await deleteCookieByName(world, COOKIE_NAME.SELECTED_ELEMENT);
  await addCookie(world, COOKIE_NAME.SELECTED_ELEMENT, new SelectedElementCookie(FILTER_TYPE.SELECTOR_PARENT, "*:focus"));
}

function keyBoardFocusTarget(world: World) {
  return world.page.locator(":focus");
}

async function setMockAsConsumed(name: string, mock: MockCookie, world: World) {
  const newMockCookie = new MockCookie(name, mock.url, mock.verb);
  newMockCookie.isConsumed = true;
  await addCookie(world, COOKIE_NAME.MOCK_URL, newMockCookie);
}

async function afterMock(world: World, url: string, verb: string, name: string) {
  await world.page.waitForResponse(url);
  const cookie = await getCookie(world, COOKIE_NAME.MOCK_URL);
  const mockCookie: MockCookie[] = JSON.parse(cookie.value);
  for (const mock of mockCookie) {
    if (mock.name === name && mock.verb === verb && mock.url === url) {
      await setMockAsConsumed(name, mock, world);
    }
  }
}

async function type(world: World, textToType: string) {
    const keyBoardFocusTargetObj = keyBoardFocusTarget(world);
    if ((await keyBoardFocusTargetObj.count()) === 1) {
        await keyBoardFocusTargetObj.type(textToType);
    } else {
        await getPageOrElement(world).then(async (element: Locator) => {
            // console.debug(element);
            await element.focus({ timeout: 10000 });
            await element.type(textToType);
        });
    }
}
