var counter = 0;
var clicked;
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");
      promise_test(async t => {
        const preClickLcp = await getLcpEntries();
        setEvent(t, link, pushState, addContent, pushUrl, eventType);
        for (let i = 0; i < clicks; ++i) {
          let paint_entries_promise = waitOnPaintEntriesPromise();
          clicked = false;
          click(link);

          await new Promise(resolve => {
            (new PerformanceObserver(() => resolve())).observe({
              type: 'soft-navigation'
            });
          });
          // 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, clicks + 1);
      }, testName);
    };

const testNavigationApi = (testName, navigateEventHandler, link) => {
  promise_test(async t => {
    const preClickLcp = await getLcpEntries();
    navigation.addEventListener('navigate', navigateEventHandler);
    const navigated = new Promise(resolve => {
      navigation.addEventListener('navigatesuccess', resolve);
      navigation.addEventListener('navigateerror', resolve);
    });
    click(link);
    await new Promise(resolve => {
      (new PerformanceObserver(() => resolve())).observe({
        type: 'soft-navigation'
      });
    });
    await navigated;
    assert_equals(document.softNavigations, 1, 'Soft Navigation detected');
    await validateSoftNavigationEntry(1, () => {}, 'foobar.html');

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

const testSoftNavigationNotDetected = options => {
    promise_test(async t => {
      const preClickLcp = await getLcpEntries();
      options.eventTarget.addEventListener(options.eventName, options.eventHandler);
      click(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);
      });
      assert_equals(
          document.softNavigations, 0, 'Soft Navigation not detected');
    }, options.testName);
  };

const runEntryValidations = async (preClickLcp, entries_expected_number = 2) => {
  await validatePaintEntries('first-contentful-paint', entries_expected_number);
  await validatePaintEntries('first-paint', entries_expected_number);
  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');
  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');
};

const click = link => {
  if (test_driver) {
    test_driver.click(link);
    timestamps[counter] = {"syncPostClick": performance.now()};
  }
}

const setEvent = (t, button, pushState, addContent, pushUrl, eventType) => {
  const eventObject = (eventType == "click") ? button : window;
  eventObject.addEventListener(eventType, async e => {
    timestamps[counter]["eventStart"] = performance.now();
    // 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);
    ++counter;

    clicked = true;
  });
};

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]["syncPostClick"], entryTimestamp);
    assert_greater_than_equal(
        timestamps[i]['eventStart'], entryTimestamp,
        '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) => {
  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);
  }
};

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) => {
  const img = new Image();
  img.src = '/images/blue.png' + "?" + Math.random();
  await img.decode();
  element.appendChild(img);
};
const addImageToMain = async () => {
  await addImage(document.getElementById('main'));
};

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 = () => {
  return new Promise((resolve, reject) => {
    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});
  });
};
