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

/**
 * These tests load SERPs and click on links that can either be ads or non ads
 * and verifies that the engagement events and the target associated with them
 * are correct.
 */

"use strict";

const TEST_PROVIDER_INFO = [
  {
    telemetryId: "example",
    searchPageRegexp:
      /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetryAd_/,
    queryParamNames: ["s"],
    codeParamName: "abc",
    taggedCodes: ["ff"],
    adServerAttributes: ["mozAttr"],
    nonAdsLinkRegexps: [
      /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetryAd_nonAdsLink_redirect.html/,
    ],
    extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
    ignoreLinkRegexps: [/^https:\/\/example\.org\/consent\?data=/],
    components: [
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
        included: {
          parent: {
            selector: ".moz-carousel",
          },
          children: [
            {
              selector: ".moz-carousel-card",
              countChildren: true,
            },
          ],
          related: {
            selector: "button",
          },
        },
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
        included: {
          parent: {
            selector: ".moz_ad",
          },
          children: [
            {
              selector: ".multi-col",
              type: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
            },
          ],
          related: {
            selector: "button",
          },
        },
        excluded: {
          parent: {
            selector: ".rhs",
          },
        },
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
        included: {
          parent: {
            selector: "form",
          },
          children: [
            {
              // This isn't contained in any of the HTML examples but the
              // presence of the entry ensures that if it is not found during
              // a topDown examination, the next element in the array is
              // inspected and found.
              selector: "textarea",
            },
            {
              selector: "input",
            },
          ],
          related: {
            selector: "div",
          },
        },
        topDown: true,
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
        default: true,
      },
      {
        type: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
        topDown: true,
        included: {
          parent: {
            selector: "#banner",
          },
          children: [
            {
              selector: "#cookie_accept",
              eventListeners: [
                {
                  eventType: "click",
                  action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_ACCEPT,
                },
              ],
            },
            {
              selector: "#cookie_reject",
              eventListeners: [
                {
                  eventType: "click",
                  action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_REJECT,
                },
              ],
            },
            {
              selector: "#cookie_more_options",
              eventListeners: [
                {
                  eventType: "click",
                  action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_MORE_OPTIONS,
                },
              ],
            },
          ],
        },
      },
    ],
  },
];

// This is used to check if not providing an nonAdsLinkRegexp can still
// reliably categorize non_ads_links.
const TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP = [
  {
    ...TEST_PROVIDER_INFO[0],
    nonAdsLinkRegexps: [],
  },
];

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;

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

// This test ensures clicking a non-first link in a component registers the
// proper component. This is because the first link of a component does the
// heavy lifting in finding the parent and best categorization of the
// component. Subsequent anchors that have the same parent get grouped into it.
// Additionally, this test deliberately has ads with different paths so that
// there are no collisions in hrefs.
add_task(async function test_click_second_ad_in_component() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_text.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

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

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
          ads_loaded: "5",
          ads_visible: "5",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// If a provider appends query parameters to a link after the page has been
// parsed, we should still be able to record the click.
add_task(async function test_click_ads_link_modified() {
  resetTelemetry();

  let url = getSERPUrl("searchTelemetryAd_components_text.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
    let target = content.document.getElementById("deep_ad_sitelink");
    let href = target.getAttribute("href");
    target.setAttribute("href", href + "?foo=bar");
    content.document.getElementById("deep_ad_sitelink").click();
  });
  await browserLoadedPromise;

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
          ads_loaded: "5",
          ads_visible: "5",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Search box is a special case which has to be tracked in the child process.
add_task(async function test_click_and_submit_incontent_searchbox() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  // Click on the searchbox.
  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "form input",
    {},
    tab.linkedBrowser
  );
  EventUtils.synthesizeKey("KEY_Enter");
  await pageLoadPromise;
  await waitForPageWithAdImpressions();

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      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",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Click an auto-suggested term. The element that is clicked is related
// to the searchbox but not in search-telemetry-v2 because it can be too
// difficult to determine ahead of time since the elements are generated
// dynamically. So instead it should listen to an element higher in the DOM.
add_task(async function test_click_autosuggest() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_searchbox.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  // Click an autosuggested term.
  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#suggest",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;
  await waitForPageWithAdImpressions();

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          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",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Carousel related buttons expand content.
add_task(async function test_click_carousel_expand() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_carousel.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  // Click a button that is expected to expand.
  await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
    content.document.querySelector("button").click();
  });

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.EXPANDED,
          target: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
          ads_loaded: "4",
          ads_visible: "3",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// This test clicks a link that has apostrophes in the both the path and list
// of query parameters, and uses search telemetry with no nonAdsRegexps defined,
// which will force us to cache every non ads link in a map and pass it back to
// the parent.
// If this test fails, it means we're doing the conversion wrong, because when
// we observe the clicked URL in the parent process, it should look exactly the
// same as how it was saved in the hrefToComponent map.
add_task(async function test_click_link_with_special_characters_in_path() {
  SearchSERPTelemetry.overrideSearchTelemetryForTests(
    TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP
  );
  await waitForIdle();

  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_text.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  let pageLoadPromise = BrowserTestUtils.waitForLocationChange(
    gBrowser,
    "https://example.com/path'?hello_world&foo=bar%27s"
  );
  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#non_ads_link_with_special_characters_in_path",
    {},
    tab.linkedBrowser
  );
  await pageLoadPromise;

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
          target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
          ads_loaded: "5",
          ads_visible: "5",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);

  // Reset state for other tests.
  SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
  await waitForIdle();
});

// Test that clicking the accept button on the cookie banner is correctly
// tracked as an engagement event.
add_task(async function test_click_cookie_banner_accept() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_cookie_banner.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#cookie_accept",
    {},
    tab.linkedBrowser
  );

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_ACCEPT,
          target: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Test that clicking the reject button on the cookie banner is accurately
// recorded as an engagement event.
add_task(async function test_click_cookie_banner_reject() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_cookie_banner.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#cookie_reject",
    {},
    tab.linkedBrowser
  );

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_REJECT,
          target: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});

// Test that clicking the more options button on the cookie banner is accurately
// recorded as an engagement event.
add_task(async function test_click_cookie_banner_more_options() {
  resetTelemetry();
  let url = getSERPUrl("searchTelemetryAd_components_cookie_banner.html");
  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
  await waitForPageWithAdImpressions();

  await BrowserTestUtils.synthesizeMouseAtCenter(
    "#cookie_more_options",
    {},
    tab.linkedBrowser
  );

  assertSERPTelemetry([
    {
      impression: {
        provider: "example",
        tagged: "true",
        partner_code: "ff",
        source: "unknown",
        is_shopping_page: "false",
        is_private: "false",
        shopping_tab_displayed: "false",
        is_signed_in: "false",
      },
      engagements: [
        {
          action: SearchSERPTelemetryUtils.ACTIONS.CLICKED_MORE_OPTIONS,
          target: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
        },
      ],
      adImpressions: [
        {
          component: SearchSERPTelemetryUtils.COMPONENTS.COOKIE_BANNER,
          ads_loaded: "1",
          ads_visible: "1",
          ads_hidden: "0",
        },
      ],
    },
  ]);

  BrowserTestUtils.removeTab(tab);
});
