/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * These tests load a SERP that has multiple ways of refining a search term
 * within content, or moving it into another search engine. It is also common
 * for providers to remove tracking params.
 */

"use strict";

const { SearchSERPTelemetry, SearchSERPTelemetryUtils } =
  ChromeUtils.importESModule("resource:///modules/SearchSERPTelemetry.sys.mjs");

const TEST_PROVIDER_INFO = [
  {
    telemetryId: "example",
    searchPageRegexp:
      /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_searchbox_with_content.html/,
    queryParamName: "s",
    codeParamName: "abc",
    taggedCodes: ["ff"],
    adServerAttributes: ["mozAttr"],
    nonAdsLinkRegexps: [
      /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_searchbox_with_content_redirect.html/,
    ],
    extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
    shoppingTab: {
      selector: "nav a",
      regexp: "&page=shopping",
      inspectRegexpInSERP: true,
    },
    components: [
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
        included: {
          parent: {
            selector: "form",
          },
          children: [
            {
              selector: "input",
            },
          ],
          related: {
            selector: "div",
          },
        },
        topDown: true,
        nonAd: true,
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.REFINED_SEARCH_BUTTONS,
        included: {
          parent: {
            selector: ".refined-search-buttons",
          },
          children: [
            {
              selector: "a",
            },
          ],
        },
        topDown: true,
        nonAd: true,
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
        default: true,
      },
    ],
  },
];

function getSERPUrl(page, organic = false) {
  let url =
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.org"
    ) + page;
  return `${url}?s=test${organic ? "" : "&abc=ff"}`;
}

async function promiseImpressionReceived() {
  return TestUtils.waitForCondition(() => {
    let adImpressions = Glean.serp.adImpression.testGetValue() ?? [];
    return adImpressions.length;
  }, "Should have received an ad impression.");
}

async function waitForIdle() {
  for (let i = 0; i < 10; i++) {
    await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
  }
}

add_setup(async function () {
  SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
  await waitForIdle();
  // Enable local telemetry recording for the duration of the tests.
  let oldCanRecord = Services.telemetry.canRecordExtended;
  Services.telemetry.canRecordExtended = true;
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.search.log", true],
      ["browser.search.serpEventTelemetry.enabled", true],
    ],
  });

  registerCleanupFunction(async () => {
    SearchSERPTelemetry.overrideSearchTelemetryForTests();
    Services.telemetry.canRecordExtended = oldCanRecord;
    resetTelemetry();
  });
});

// "Tabs" are considered to be links the navigation of a SERP. Their hrefs
// may look similar to a search page, including related searches.
add_task(async function test_click_tab() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#images",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression?.testGetValue()?.length == 2;
  }, "Should have two impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Ensure that shopping links on a page with many non-ad link regular
// expressions doesn't get confused for a non-ads link.
add_task(async function test_click_shopping() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#shopping",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression?.testGetValue()?.length == 2;
  }, "Should have two impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.SHOPPING_TAB,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "unknown",
        is_shopping_page: "true",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

add_task(async function test_click_related_search_in_new_tab() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let targetUrl =
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.org"
    ) + "searchTelemetryAd_searchbox_with_content.html?s=test+one+two+three";

  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetUrl, true);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#related-new-tab",
    {},
    tab.linkedBrowser
  );
  let tab2 = await tabPromise;

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression?.testGetValue()?.length == 2;
  }, "Should have two impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "opened_in_new_tab",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
  BrowserTestUtils.removeTab(tab2);
});

// We consider regular expressions in nonAdsLinkRegexps and searchPageRegexp
// as valid non ads links when recording an engagement event.
add_task(async function test_click_redirect_search_in_newtab() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let targetUrl =
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.org"
    ) + "searchTelemetryAd_searchbox_with_content.html?s=test+one+two+three";

  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, targetUrl, true);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#related-redirect",
    {},
    tab.linkedBrowser
  );
  let tab2 = await tabPromise;

  await waitForPageWithAdImpressions();

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression.testGetValue()?.length == 2;
  }, "Should have two impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "opened_in_new_tab",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
  BrowserTestUtils.removeTab(tab2);
});

// Ensure if a user does a search that uses one of the in-content sources,
// we clear the cached source value.
add_task(async function test_content_source_reset() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  // Do a text search to trigger a defined target.
  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "form input",
    {},
    tab.linkedBrowser
  );
  EventUtils.synthesizeKey("KEY_Enter");
  await pageLoadPromise;

  // Click on a related search that will load within the same page and should
  // have an unknown target.
  await waitForPageWithAdImpressions();
  pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#related-in-page",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression.testGetValue()?.length == 3;
  }, "Should have three impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
        },
        {
          action: SearchSERPTelemetryUtils.ACTIONS.SUBMITTED,
          target: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "follow_on_from_refine_on_incontent_search",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// This test also deliberately includes an anchor with a reserved character in
// the href that gets parsed on page load. This is because when the URL is
// requested and observed in the network process, it is converted into a
// percent encoded string, so we want to ensure we're categorizing the
// component properly. This can happen with refinement buttons.
add_task(async function test_click_refinement_button() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let targetUrl =
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.org"
    ) + "searchTelemetryAd_searchbox_with_content.html?s=test%27s";

  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(
    gBrowser,
    targetUrl
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#refined-search-button",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;

  await TestUtils.waitForCondition(() => {
    return Glean.serp.impression.testGetValue()?.length == 2;
  }, "Should have two impressions.");

  assertImpressionEvents([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.REFINED_SEARCH_BUTTONS,
        },
      ],
    },
    {
      impression: {
        provider: "example",
        tagged: "false",
        partner_code: "",
        source: "follow_on_from_refine_on_SERP",
        is_shopping_page: "false",
        shopping_tab_displayed: "true",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});
