// @ts-check
import {expect} from '@playwright/test';

/**
 * Call this, say in your test.beforeEach() method, to capture all
 * console messages and copy them to the playwright console.
 * @param {import("playwright-core").Page} page
 */
export function captureConsoleMessages(page) {
  page.on('console', async msg => {
    // ignore warnings for now.  There are tons of them.
    if (msg.type() === 'warning') {
      return;
    }

    // Get time before await on arg values.
    const now = new Date();
    const minutes = now.getUTCMinutes().toString().padStart(2, '0');
    const seconds = now.getUTCSeconds().toString().padStart(2, '0');
    const millis = now.getUTCMilliseconds().toString().padStart(3, '0');
    const time = `${minutes}:${seconds}:${millis}`;

    const values = [];
    for (const arg of msg.args()) {
      let argString = '';
      try {
        // Sometimes this fails with something like:
        //  "Protocol error (Runtime.callFunctionOn): Target closed."
        argString = (await arg.jsonValue()).toString();
      } catch {
        argString = arg.toString();
      }
      // Simplify tons of "SameSite" warnings.
      if (
        argString.match(/does not have a proper “SameSite” attribute value/)
      ) {
        argString = argString
          .replace('JavaScript Warning: ', 'SameSite ')
          .replace(
            'does not have a proper “SameSite” attribute value. Soon, cookies without the “SameSite” attribute or with an invalid value will be treated as “Lax”. This means that the cookie will no longer be sent in third-party contexts. If your application depends on this cookie being available in such contexts, please add the “SameSite=None“ attribute to it. To know more about the “SameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite',
            ''
          );
      }
      values.push(argString);
    }
    const valuesString = values.join(' ');
    console.log(`${time}: console.${msg.type()}: ${valuesString}`);
  });
}

/**
 * @param {import("playwright-core").Page} page
 */
export function capturePageEvents(page) {
  page.on('close', async () => {
    console.log(`close: ${page.url()}`);
  });
  page.on('pageerror', async (/** @type {Error} */ error) => {
    console.log(`pageerror: ${error}`);
  });
  page.on('crash', async () => {
    console.log(`crash: ${page.url()}`);
  });
  page.on('domcontentloaded', async () => {
    console.log(`domcontentloaded: ${page.url()}`);
  });
}

/**
 * @param {import("playwright-core").Page} page
 */
export async function decodeCookies(page) {
  const cookies = await page.context().cookies();
  cookies.forEach(cookie => {
    console.log('Decoded Cookie:', cookie);
  });
}

/**
 * @param {import("playwright-core").Page} page
 */
export async function isMobile(page) {
  const viewportSize = page.viewportSize();
  return viewportSize && viewportSize.width <= 700;
}

/**
 * Handle beforeunload by accepting it.
 * @param {import('@playwright/test').Page} page
 */
export function acceptBeforeUnloadDialogs(page) {
  page.on('dialog', async dialog => {
    if (dialog.type() === 'beforeunload') {
      await dialog.accept();
    }
  });
}

/**
 * Handle confirm dialog by accepting it.
 * @param {import('@playwright/test').Page} page
 */
export function acceptConfirmDialogs(page) {
  // Setup handler for confirm dialog
  page.on('dialog', async dialog => {
    if (dialog.type() === 'confirm') {
      await dialog.accept();
    }
  });
}

/**
 * Handle alert dialog by accepting it.
 * @param {import('@playwright/test').Page} page
 */
export function acceptAlertDialogs(page) {
  // Setup handler for confirm dialog
  page.on('dialog', async dialog => {
    if (dialog.type() === 'alert') {
      await dialog.accept();
    }
  });
}

/**
 * @param {import("playwright-core").Page} page
 */
export async function login(page) {
  await setupFakeNow(page);
  page.exposeFunction('isPlaywright', () => {});

  // Always reset to the roadmap page.
  // But first accept alert dialogs, which
  // can occur in Chrome when not logged in.
  acceptAlertDialogs(page);

  await page.goto('/', {timeout: 20000});
  await page.waitForURL('**/roadmap', {timeout: 20000});

  await expect(page).toHaveTitle(/Chrome Status/);

  // Check whether we are already or still logged in.
  // We use the presence of the 'dev-mode-sign-in-button' as the definitive
  // indicator of "Not Logged In" because the Account Indicator layout varies.
  const loginButton = page.getByTestId('dev-mode-sign-in-button');

  if (await loginButton.isHidden()) {
    // If the login button is hidden, we assume we are logged in.
    return;
  }

  // Expect login button to be present now.
  await expect(loginButton).toBeVisible();

  // Click login and wait for the page reload that the button triggers.
  // We cannot use waitForURL because the URL might be the same.
  // Waiting for 'domcontentloaded' ensures the reload has actually happened.
  await Promise.all([
    page.waitForEvent('domcontentloaded'),
    loginButton.click(),
  ]);

  // Validate successful login.
  await expect(page).toHaveTitle(/Chrome Status/);

  if (await isMobile(page)) {
    // On mobile, the account indicator is hidden inside the drawer.
    // Verifying that the login button is GONE is sufficient proof of login.
    await expect(loginButton).not.toBeVisible();
  } else {
    // On desktop, we can verify the account indicator is visible in the header.
    const accountIndicator = page.getByTestId('account-indicator');
    try {
      await expect(accountIndicator).toBeVisible({timeout: 10000});
    } catch {
      console.log('Account indicator not found immediately. Reloading...');
      await page.reload();
      await expect(accountIndicator).toBeVisible({timeout: 10000});
    }
  }
}

/**
 * @param {import("playwright-core").Page} page
 */
export async function logout(page) {
  // Attempt to sign out after running each test.
  // First reset to the roadmap page, so that we avoid the alert
  // when signed out on other pages.
  // But in case the current page has unsaved changes we need to
  // accept leaving them unsaved.
  acceptBeforeUnloadDialogs(page);

  await page.goto('/');
  await page.waitForURL('**/roadmap');
  await expect(page).toHaveTitle(/Chrome Status/);

  // If we are already logged out, return immediately.
  // This avoids unnecessary menu clicking and flaky checks.
  const loginButton = page.getByTestId('dev-mode-sign-in-button');
  if (await loginButton.isVisible()) {
    return;
  }

  // Variable to store the promise for the page reload
  let reloadPromise = null;

  if (await isMobile(page)) {
    const menuButton = page.getByTestId('menu');
    // Force the menu to open, retrying if necessary
    await menuButton.click();

    const signOutLink = page.getByTestId('sign-out-link');
    await expect(signOutLink).toBeVisible();

    // Setup the listener BEFORE the action
    reloadPromise = page.waitForEvent('load');
    await signOutLink.click();
  } else {
    // Desktop Logic
    const accountIndicator = page.getByTestId('account-indicator');
    await expect(accountIndicator).toBeVisible({timeout: 20000});
    await accountIndicator.click();

    const signOutLink = page.getByTestId('sign-out-link');
    await expect(signOutLink).toBeVisible();

    // Setup the listener BEFORE the action
    reloadPromise = page.waitForEvent('load');
    await signOutLink.click();
  }

  // Await the reload only if we triggered it.
  if (reloadPromise) {
    await reloadPromise;
  }

  // If the button is missing, it implies either:
  //    a) The 'getPermissions' call failed on the new page (Limbo state)
  //    b) The session wasn't cleared (Still logged in)
  // We reload the page to fix (a) and recursively try logout to fix (b).
  try {
    // Give it a short time (5s) to appear normally
    await expect(loginButton).toBeVisible({timeout: 5000});
  } catch {
    console.log('Login button not found after logout. Retrying...');

    // Reloading to avoid a "getPermissions failed" state.
    await page.reload();

    // Recursively calling logout handles the "Still logged in" state.
    // (The recursive call will hit step 1 and return immediately if successful)
    await logout(page);
  }
}

/**
 * From top-level page, after logging in, go to the New Feature page.
 * @param {import('@playwright/test').Page} page
 */
export async function gotoNewFeaturePage(page) {
  const mobile = await isMobile(page);
  const createFeatureButton = page.getByTestId('create-feature-button');
  const menuButton = page.getByTestId('menu');

  // Navigate to the new feature page.
  await expect(menuButton).toBeVisible();
  if (mobile) {
    await menuButton.click();
  }

  await expect(createFeatureButton).toBeVisible();
  await createFeatureButton.click();

  if (mobile) {
    // To hide menu (Close Drawer).
    await menuButton.click();
  }

  // Expect "Add a feature" header to be present.
  const addAFeatureHeader = page.getByTestId('add-a-feature');
  await expect(addAFeatureHeader).toBeVisible({timeout: 10000});
}

/**
 * Enters a blink component on the page.
 *
 * @param {import("playwright-core").Page} page - The page object representing the web page.
 * @return {Promise<void>} A promise that resolves once the blink component is entered.
 */
export async function enterBlinkComponent(page) {
  const blinkComponentsInputWrapper = page.getByTestId(
    'blink_components_wrapper'
  );
  await expect(blinkComponentsInputWrapper).toBeVisible();

  const blinkComponentsInput = blinkComponentsInputWrapper.locator('input');
  await blinkComponentsInput.fill('blink');

  // If you need to verify the value "stuck"
  await expect(blinkComponentsInput).toHaveValue('blink');
}

/**
 * Enters a web feature id on the page.
 *
 * @param {import("playwright-core").Page} page - The page object representing the web page.
 * @return {Promise<void>} A promise that resolves once the web feature id is entered.
 */
export async function enterWebFeatureId(page) {
  const webFeatureIdInputWrapper = page.getByTestId('web_feature_wrapper');
  await expect(webFeatureIdInputWrapper).toBeVisible();

  const webFeatureIdInput = webFeatureIdInputWrapper.locator('input');
  await webFeatureIdInput.fill('hwb');

  // Verify the value
  await expect(webFeatureIdInput).toHaveValue('hwb');

  // TODO(kyleju): assert that the link to webstatus.dev is present.
  // It is missing in the current test setup.
}

/**
 * Create a new feature, starting from top-level page, ending up on feature page.
 * @param {import('@playwright/test').Page} page
 */
export async function createNewFeature(page) {
  await gotoNewFeaturePage(page);
  // Enter feature name
  const featureNameInput = page.locator('input[name="name"]');
  await featureNameInput.fill('Test feature name');

  // Enter summary description
  const summaryInput = page.locator('textarea[name="summary"]');
  await summaryInput.fill('Test summary description');

  await enterBlinkComponent(page);
  await enterWebFeatureId(page);

  // Select feature type.
  const featureTypeRadioNew = page.locator(
    'input[name="feature_type"][value="0"]'
  );
  await featureTypeRadioNew.click();

  // Submit the form.
  const submitButton = page.locator('input[type="submit"]');
  await submitButton.click();

  // Wait until we are on the Feature page.
  await page.waitForURL('**/feature/*');
  const detail = page.locator('chromedash-feature-detail');

  // If the feature detail doesn't appear within 5 seconds, it's likely a 404.
  // Reload the page to try fetching the data again.
  try {
    await detail.waitFor({state: 'visible', timeout: 5000});
  } catch {
    console.log('Feature detail not found immediately. Reloading...');
    await page.reload();
  }

  await expect(detail).toBeVisible({timeout: 30000});
}

/**
 * Navigates to new features list page.
 * @param {import("playwright-core").Page} page
 */
export async function gotoNewFeatureList(page) {
  await page.goto('/features');
  const pagiation = page.locator('chromedash-feature-pagination');
  await expect(pagiation).toBeVisible();
}

/**
 * Check a screenshot with some default options that reduce
 * flakiness.
 */
export async function expectScreenshot(page, name, options, hoverElement) {
  // TODO(jrobbins): This is where we would add logic for testing dark mode.
  options = {...options, timeout: 30000};
  await page.mouse.move(0, 0);
  if (hoverElement) {
    // Always approach the element from (0, 0).
    await expect(hoverElement).toBeVisible();
    await hoverElement.hover();
  }
  await page.waitForTimeout(250 + 100); // Some shoelace animations take 250ms.
  await expect(page).toHaveScreenshot(`${name}.png`, options);
}

/**
 * Fakes the current date in the page.
 * @param {import('@playwright/test').Page} page
 * @param {string} [fakeNowDateString]
 */
export async function setupFakeNow(
  page,
  fakeNowDateString = 'Jan 1 2026 00:00:00'
) {
  // Get fakeNow from UTC to extract the timeZone offset used in the test
  const fakeNowFromUTC = new Date(fakeNowDateString);
  const offset = fakeNowFromUTC.getTimezoneOffset();
  const offsetSign = offset < 0 ? '-' : '+';
  const offsetHours = `${Math.abs(Math.floor(offset / 60))}`.padStart(2, '0');
  const offsetMinutes = `${Math.abs(offset % 60)}`.padStart(2, '0');
  const offsetText = `${offsetSign}${offsetHours}:${offsetMinutes}`;

  // Get fakeNow from the test timeZone
  const fakeNow = new Date(`${fakeNowDateString}Z${offsetText}`).valueOf();

  // Update the Date accordingly in your test pages
  await page.addInitScript(`{
    // Extend Date constructor to default to fakeNow
    Date = class extends Date {
      constructor(...args) {
        if (args.length === 0) {
          super(${fakeNow});
        } else {
          super(...args);
        }
      }
    }
    // Override Date.now() to start from fakeNow
    const __DateNowOffset = ${fakeNow} - Date.now();
    const __DateNow = Date.now;
    Date.now = () => __DateNow() + __DateNowOffset;
  }`);
}
