/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2024, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * Open MCT includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/

/*
Collection of Time List tests set to run with browser clock manipulate made possible with the
page.clock() API.
*/

import fs from 'fs';

import {
  createDomainObjectWithDefaults,
  createPlanFromJSON,
  navigateToObjectWithRealTime
} from '../../../appActions.js';
import {
  createTimelistWithPlanAndSetActivityInProgress,
  getEarliestStartTime,
  getFirstActivity
} from '../../../helper/planningUtils';
import { expect, test } from '../../../pluginFixtures.js';

const examplePlanSmall3 = JSON.parse(
  fs.readFileSync(
    new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
  )
);

const examplePlanSmall1 = JSON.parse(
  fs.readFileSync(
    new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
  )
);

const TIME_TO_FROM_COLUMN = 2;
const HEADER_ROW = 0;
const NUM_COLUMNS = 5;
const FULL_CIRCLE_PATH =
  'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';

/**
 * The regular expression used to parse the countdown string.
 * Some examples of valid Countdown strings:
 * ```
 * '35D 02:03:04'
 * '-1D 01:02:03'
 * '01:02:03'
 * '-05:06:07'
 * ```
 */
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;

/**
 * @typedef {Object} CountdownOrUpObject
 * @property {string} sign - The sign of the countdown ('-' if the countdown is negative, '+' otherwise).
 * @property {string} days - The number of days in the countdown (undefined if there are no days).
 * @property {string} hours - The number of hours in the countdown.
 * @property {string} minutes - The number of minutes in the countdown.
 * @property {string} seconds - The number of seconds in the countdown.
 * @property {string} toString - The countdown string.
 */

/**
 * Object representing the indices of the capture groups in a countdown regex match.
 *
 * @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
 * @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
 * @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
 * @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
 * @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
 * @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
 */
const COUNTDOWN = Object.freeze({
  SIGN: 1,
  DAYS: 2,
  HOURS: 3,
  MINUTES: 4,
  SECONDS: 5
});

const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);

test.describe('Time List with controlled clock @clock', () => {
  test.beforeEach(async ({ page }) => {
    await page.clock.install({ time: getEarliestStartTime(examplePlanSmall3) });
    await page.clock.resume();
    await page.goto('./', { waitUntil: 'domcontentloaded' });
    // Create Time List
    const timelist = await createDomainObjectWithDefaults(page, {
      type: 'Time List'
    });

    // Create a Plan with events that count down and up.
    // Add it as a child to the Time List.
    await createPlanFromJSON(page, {
      json: examplePlanSmall3,
      parent: timelist.uuid
    });

    // Navigate to the Time List in real-time mode
    await navigateToObjectWithRealTime(page, timelist.url, 900000, 1800000);

    //Expand the viewport to show the entire time list
    await page.getByLabel('Collapse Inspect Pane').click();
    await page.getByLabel('Collapse Browse Pane').click();
  });
  test('Time List shows current events and counts down correctly in real-time mode', async ({
    page
  }) => {
    const countUpCells = [
      getTimeListCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
      getTimeListCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
    ];
    const countdownCells = [
      getTimeListCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
      getTimeListCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
    ];

    // Verify that the countdown cells are counting down
    for (let i = 0; i < countdownCells.length; i++) {
      await test.step(`Countdown cell ${i + 1} counts down`, async () => {
        const countdownCell = countdownCells[i];
        // Get the initial countdown timestamp object
        const beforeCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
        // should not have a '-' sign
        await expect(countdownCell).not.toHaveText('-');
        // Wait until it changes
        await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
        // Get the new countdown timestamp object
        const afterCountdown = await getAndAssertCountdownOrUpObject(page, i + 3);
        // Verify that the new countdown timestamp object is less than the old one
        expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
      });
    }

    // Verify that the count-up cells are counting up
    for (let i = 0; i < countUpCells.length; i++) {
      await test.step(`Count-up cell ${i + 1} counts up`, async () => {
        const countUpCell = countUpCells[i];
        // Get the initial count-up timestamp object
        const beforeCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
        // should not have a '+' sign
        await expect(countUpCell).not.toHaveText('+');
        // Wait until it changes
        await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
        // Get the new count-up timestamp object
        const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
        // Verify that the new count-up timestamp object is greater than the old one
        expect(Number(afterCountUp.seconds)).toBeGreaterThan(Number(beforeCountUp.seconds));
      });
    }
  });
});

test.describe('Activity progress when activity is in the future @clock', () => {
  test.beforeEach(async ({ page }) => {
    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start - 1 });
    await page.clock.resume();
    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
  });

  test('progress pie is empty', async ({ page }) => {
    const anActivity = page.getByRole('row').nth(0);
    // Progress pie shows no progress when now is less than the start time
    await expect(anActivity.getByLabel('Activity in progress').locator('path')).not.toHaveAttribute(
      'd'
    );
  });
});

test.describe('Activity progress when now is between start and end of the activity @clock', () => {
  test.beforeEach(async ({ page }) => {
    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start + 50000 });
    await page.clock.resume();
    await page.goto('./', { waitUntil: 'domcontentloaded' });
    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
  });

  test('progress pie is partially filled', async ({ page }) => {
    const anActivity = page.getByRole('row').nth(0);
    const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
    // Progress pie shows progress when now is greater than the start time
    await expect(pathElement).toHaveAttribute('d');
  });
});

test.describe('Activity progress when now is after end of the activity @clock', () => {
  test.beforeEach(async ({ page }) => {
    await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 });
    await page.clock.resume();
    await page.goto('./', { waitUntil: 'domcontentloaded' });
    await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
  });

  test('progress pie is full', async ({ page }) => {
    const anActivity = page.getByRole('row').nth(0);
    // Progress pie is completely full and doesn't update if now is greater than the end time
    await expect(anActivity.getByLabel('Activity in progress').locator('path')).toHaveAttribute(
      'd',
      FULL_CIRCLE_PATH
    );
  });
});

/**
 * Get the cell at the given row and column indices.
 * @param {import('@playwright/test').Page} page
 * @param {number} rowIndex
 * @param {number} columnIndex
 * @returns {import('@playwright/test').Locator} cell
 */
function getTimeListCellByIndex(page, rowIndex, columnIndex) {
  return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
}

/**
 * Return the innerText of the cell at the given row and column indices.
 * @param {import('@playwright/test').Page} page
 * @param {number} rowIndex
 * @param {number} columnIndex
 * @returns {Promise<string>} text
 */
async function getTimeListCellTextByIndex(page, rowIndex, columnIndex) {
  const text = await getTimeListCellByIndex(page, rowIndex, columnIndex).innerText();
  return text;
}

/**
 * Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
 * regex, and return an object representing the countdown.
 * @param {import('@playwright/test').Page} page
 * @param {number} rowIndex the row index
 * @returns {Promise<CountdownOrUpObject>} The countdown (or countup) object
 */
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
  const timeToFrom = await getTimeListCellTextByIndex(
    page,
    HEADER_ROW + rowIndex,
    TIME_TO_FROM_COLUMN
  );

  expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
  const match = timeToFrom.match(COUNTDOWN_REGEXP);

  return {
    sign: match[COUNTDOWN.SIGN],
    days: match[COUNTDOWN.DAYS],
    hours: match[COUNTDOWN.HOURS],
    minutes: match[COUNTDOWN.MINUTES],
    seconds: match[COUNTDOWN.SECONDS],
    toString: () => timeToFrom
  };
}
