var counter = 0;
var interacted;
var timestamps = [];
const MAX_CLICKS = 50;
// Entries for one hard navigation + 50 soft navigations.
const MAX_PAINT_ENTRIES = 51;
const URL = 'foobar.html';
const readValue = (value, defaultValue) => {
  return value !== undefined ? value : defaultValue;
};
const testSoftNavigation = options => {
  const addContent = options.addContent;
  const link = options.link;
  const pushState =
      readValue(options.pushState, url => {history.pushState({}, '', url)});
  const clicks = readValue(options.clicks, 1);
  const extraValidations = readValue(options.extraValidations, () => {});
  const testName = options.testName;
  const pushUrl = readValue(options.pushUrl, true);
  const eventType = readValue(options.eventType, 'click');
  const interactionFunc = options.interactionFunc;
  const eventPrepWork = options.eventPrepWork;
  promise_test(async t => {
    await waitInitialLCP();
    const preClickLcp = await getLcpEntries();
    setEvent(t, link, pushState, addContent, pushUrl, eventType, eventPrepWork);
    let first_navigation_id;
    for (let i = 0; i < clicks; ++i) {
      const firstClick = (i === 0);
      let paint_entries_promise = waitOnPaintEntriesPromise(firstClick);
      interacted = false;
      const soft_nav_promise = waitOnSoftNav();
      interact(link, interactionFunc);

      const navigation_id = await soft_nav_promise;
      if (!first_navigation_id) {
        first_navigation_id = navigation_id;
      }
      // Ensure paint timing entries are fired before moving on to the next
      // click.
      await paint_entries_promise;
    }
    assert_equals(
        document.softNavigations, clicks,
        'Soft Navigations detected are the same as the number of clicks');
    await validateSoftNavigationEntry(clicks, extraValidations, pushUrl);

    await runEntryValidations(
        preClickLcp, first_navigation_id, clicks + 1, options.validate);
  }, testName);
};

const testNavigationApi = (testName, navigateEventHandler, link) => {
  promise_test(async t => {
    navigation.addEventListener('navigate', navigateEventHandler);
    const navigated = new Promise(resolve => {
      navigation.addEventListener('navigatesuccess', resolve);
      navigation.addEventListener('navigateerror', resolve);
    });
    await waitInitialLCP();
    const preClickLcp = await getLcpEntries();
    let paint_entries_promise = waitOnPaintEntriesPromise();
    const soft_nav_promise = waitOnSoftNav();
    interact(link);
    const first_navigation_id = await soft_nav_promise;
    await navigated;
    await paint_entries_promise;
    assert_equals(document.softNavigations, 1, 'Soft Navigation detected');
    await validateSoftNavigationEntry(1, () => {}, 'foobar.html');

    await runEntryValidations(preClickLcp, first_navigation_id);
  }, testName);
};

const testSoftNavigationNotDetected = options => {
  promise_test(async t => {
    const preClickLcp = await getLcpEntries();
    options.eventTarget.addEventListener(
        options.eventName, options.eventHandler);
    interact(options.link);
    await new Promise((resolve, reject) => {
      new PerformanceObserver(() => {
        reject('Soft navigation should not be triggered');
      }).observe({type: 'soft-navigation', buffered: true});
      t.step_timeout(resolve, 1000);
    });
    if (document.softNavigations) {
      assert_equals(
          document.softNavigations, 0, 'Soft Navigation not detected');
    }
    const postClickLcp = await getLcpEntries();
    assert_equals(
        preClickLcp.length, postClickLcp.length, 'No LCP entries accumulated');
  }, options.testName);
};

const runEntryValidations = async (
    preClickLcp, first_navigation_id, entries_expected_number = 2,
    validate = null) => {
  await validatePaintEntries(
      'first-contentful-paint', entries_expected_number, first_navigation_id);
  await validatePaintEntries(
      'first-paint', entries_expected_number, first_navigation_id);
  const postClickLcp = await getLcpEntries();
  const postClickLcpWithoutSoftNavs = await getLcpEntriesWithoutSoftNavs();
  assert_greater_than(
      postClickLcp.length, preClickLcp.length,
      'Soft navigation should have triggered at least an LCP entry');

  if (validate) {
    await validate();
  }
  assert_equals(
      postClickLcpWithoutSoftNavs.length, preClickLcp.length,
      'Soft navigation should not have triggered an LCP entry when the ' +
          'observer did not opt in');
  assert_not_equals(
      postClickLcp[postClickLcp.length - 1].size,
      preClickLcp[preClickLcp.length - 1].size,
      'Soft navigation LCP element should not have identical size to the hard ' +
          'navigation LCP element');
  assert_equals(
      postClickLcp[preClickLcp.length].navigationId, first_navigation_id,
      'Soft navigation LCP should have the same navigation ' +
          'ID as the last soft nav entry');
};

const interact =
    (link, interactionFunc = undefined) => {
      if (test_driver) {
        if (interactionFunc) {
          interactionFunc();
        } else {
          test_driver.click(link);
        }
        timestamps[counter] = {'syncPostInteraction': performance.now()};
      }
    }

const setEvent =
    (t, button, pushState, addContent, pushUrl, eventType, prepWork) => {
      const eventObject =
          (eventType == 'click' || eventType.startsWith('key')) ? button :
                                                                  window;
      eventObject.addEventListener(eventType, async e => {
        let prepWorkFailed = false;
        if (prepWork && !prepWork(t)) {
          prepWorkFailed = true;
        }
        // This is the end of the event's sync processing.
        if (!timestamps[counter]['eventEnd']) {
          timestamps[counter]['eventEnd'] = performance.now();
        }
        if (prepWorkFailed) {
          return;
        }
        // Jump through a task, to ensure task tracking is working properly.
        await new Promise(r => t.step_timeout(r, 0));

        const url = URL + '?' + counter;
        if (pushState) {
          // Change the URL
          if (pushUrl) {
            pushState(url);
          } else {
            pushState();
          }
        }

        // Wait 10 ms to make sure the timestamps are correct.
        await new Promise(r => t.step_timeout(r, 10));

        await addContent(url);

        interacted = true;
        ++counter;
      });
    };

const validateSoftNavigationEntry =
    async (clicks, extraValidations, pushUrl) => {
  const [entries, options] = await new Promise(resolve => {
    new PerformanceObserver((list, obs, options) => {
      resolve([list.getEntries(), options]);
    }).observe({type: 'soft-navigation', buffered: true});
  });
  const expectedClicks = Math.min(clicks, MAX_CLICKS);

  assert_equals(
      entries.length, expectedClicks, 'Performance observer got an entry');
  for (let i = 0; i < entries.length; ++i) {
    const entry = entries[i];
    assert_true(
        entry.name.includes(pushUrl ? URL : document.location.href),
        'The soft navigation name is properly set');
    const entryTimestamp = entry.startTime;
    assert_less_than_equal(
        timestamps[i]['syncPostInteraction'], entryTimestamp,
        'Entry timestamp is lower than the post interaction one');
    assert_greater_than_equal(
        entryTimestamp, timestamps[i]['eventEnd'],
        'Event start timestamp matches');
    assert_not_equals(
        entry.navigationId,
        performance.getEntriesByType('navigation')[0].navigationId,
        'The navigation ID was re-generated and different from the initial one.');
    if (i > 0) {
      assert_not_equals(
          entry.navigationId, entries[i - 1].navigationId,
          'The navigation ID was re-generated between clicks');
    }
  }
  assert_equals(
      performance.getEntriesByType('soft-navigation').length, expectedClicks,
      'Performance timeline got an entry');
  await extraValidations(entries, options);
};

const validatePaintEntries =
    async (type, entries_number, first_navigation_id) => {
  if (!performance.softNavPaintMetricsSupported) {
    return;
  }
  const expected_entries_number = Math.min(entries_number, MAX_PAINT_ENTRIES);
  const entries = await new Promise(resolve => {
    const entries = [];
    new PerformanceObserver(list => {
      entries.push(...list.getEntriesByName(type));
      if (entries.length >= expected_entries_number) {
        resolve(entries);
      }
    }).observe({
      type: 'paint',
      buffered: true,
      includeSoftNavigationObservations: true
    });
  });
  const entries_without_softnavs = await new Promise(resolve => {
    new PerformanceObserver(list => {
      resolve(list.getEntriesByName(type));
    }).observe({type: 'paint', buffered: true});
  });
  assert_equals(
      entries.length, expected_entries_number,
      `There are ${entries_number} entries for ${type}`);
  assert_equals(
      entries_without_softnavs.length, 1,
      `There is one non-softnav entry for ${type}`);
  if (entries_number > 1) {
    assert_not_equals(
        entries[0].startTime, entries[1].startTime,
        'Entries have different timestamps for ' + type);
  }
  if (expected_entries_number > entries_without_softnavs.length) {
    assert_equals(
        entries[entries_without_softnavs.length].navigationId,
        first_navigation_id,
        'First paint entry should have the same navigation ID as the last soft ' +
            'navigation entry');
  }
};

const waitInitialLCP =
    () => {
      return new Promise(resolve => {
        new PerformanceObserver(resolve).observe(
            {type: 'largest-contentful-paint', buffered: true});
      });
    }

const waitOnSoftNav = () => {
  return new Promise(resolve => {
    new PerformanceObserver(list => {
      const entries = list.getEntries();
      assert_equals(entries.length, 1, 'Only one soft navigation entry');
      resolve(entries[0].navigationId);
    }).observe({type: 'soft-navigation'});
  });
};

const getLcpEntries = async () => {
  const entries = await new Promise(resolve => {
    new PerformanceObserver(list => {
      resolve(list.getEntries());
    }).observe({
      type: 'largest-contentful-paint',
      buffered: true,
      includeSoftNavigationObservations: true
    });
  });
  return entries;
};

const getLcpEntriesWithoutSoftNavs = async () => {
  const entries = await new Promise(resolve => {
    new PerformanceObserver(list => {
      resolve(list.getEntries());
    }).observe({type: 'largest-contentful-paint', buffered: true});
  });
  return entries;
};

const addImage = async (element, url = 'blue.png', id = 'imagelcp') => {
  const img = new Image();
  img.src = '/images/' + url + '?' + Math.random();
  img.id = id;
  img.setAttribute('elementtiming', id);
  await img.decode();
  element.appendChild(img);
};
const addImageToMain = async (url = 'blue.png', id = 'imagelcp') => {
  await addImage(document.getElementById('main'), url, id);
};

const addTextParagraphToMain = (text, element_timing = '') => {
  const main = document.getElementById('main');
  const p = document.createElement('p');
  const textNode = document.createTextNode(text);
  p.appendChild(textNode);
  if (element_timing) {
    p.setAttribute('elementtiming', element_timing);
  }
  p.style = 'font-size: 3em';
  main.appendChild(p);
  return p;
};
const addTextToDivOnMain = () => {
  const main = document.getElementById('main');
  const prevDiv = document.getElementsByTagName('div')[0];
  if (prevDiv) {
    main.removeChild(prevDiv);
  }
  const div = document.createElement('div');
  const text = document.createTextNode('Lorem Ipsum');
  div.appendChild(text);
  div.style = 'font-size: 3em';
  main.appendChild(div);
};

const waitOnPaintEntriesPromise = (expectLCP = true) => {
  return new Promise((resolve, reject) => {
    if (performance.softNavPaintMetricsSupported) {
      const paint_entries = [];
      new PerformanceObserver(list => {
        paint_entries.push(...list.getEntries());
        if (paint_entries.length == 2) {
          resolve();
        } else if (paint_entries.length > 2) {
          reject();
        }
      }).observe({type: 'paint', includeSoftNavigationObservations: true});
    } else if (expectLCP) {
      new PerformanceObserver(list => {
        resolve();
      }).observe({
        type: 'largest-contentful-paint',
        includeSoftNavigationObservations: true
      });
    } else {
      step_timeout(() => {
        requestAnimationFrame(() => {
          requestAnimationFrame(resolve);
        });
      }, 100);
    }
  });
};
