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

"use strict";

const { SiteDataTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/SiteDataTestUtils.sys.mjs"
);

const DOMAIN_A = "example.com";
const DOMAIN_B = "example.org";
const DOMAIN_C = "example.net";

const ORIGIN_A = "https://" + DOMAIN_A;
const ORIGIN_A_SUB = `https://test1.${DOMAIN_A}`;
const ORIGIN_B = "https://" + DOMAIN_B;
const ORIGIN_C = "https://" + DOMAIN_C;

const TEST_COOKIE_HEADER_URL =
  getRootDirectory(gTestPath).replace(
    "chrome://mochitests/content",
    "https://example.com"
  ) + "testCookieHeader.sjs";

/**
 * Tests that the test domains have no cookies set.
 */
function assertNoCookies() {
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_A),
    "Should not set any cookies for ORIGIN_A"
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B"
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_C),
    "Should not set any cookies for ORIGIN_C"
  );
}

/**
 * Loads a list of urls consecutively from the same tab.
 * @param {string[]} urls - List of urls to load.
 */
async function visitTestSites(urls = [ORIGIN_A, ORIGIN_B, ORIGIN_C]) {
  let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");

  for (let url of urls) {
    await BrowserTestUtils.loadURIString(tab.linkedBrowser, url);
    await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
  }

  BrowserTestUtils.removeTab(tab);
}

add_setup(cookieInjectorTestSetup);

/**
 * Tests that no cookies are set if the cookie injection component is disabled
 * by pref, but the cookie banner service is enabled.
 */
add_task(async function test_cookie_injector_disabled() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", false],
    ],
  });

  insertTestCookieRules();

  await visitTestSites();
  assertNoCookies();

  await SiteDataTestUtils.clear();
});

/**
 * Tests that no cookies are set if the cookie injection component is enabled
 * by pref, but the cookie banner service is disabled or in detect-only mode.
 */
add_task(async function test_cookie_banner_service_disabled() {
  // Enable in PBM so the service is always initialized.
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode.privateBrowsing",
        Ci.nsICookieBannerService.MODE_REJECT,
      ],
    ],
  });

  for (let [serviceMode, detectOnly] of [
    [Ci.nsICookieBannerService.MODE_DISABLED, false],
    [Ci.nsICookieBannerService.MODE_DISABLED, true],
    [Ci.nsICookieBannerService.MODE_REJECT, true],
    [Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT, true],
  ]) {
    info(`Testing with serviceMode=${serviceMode}; detectOnly=${detectOnly}`);
    await SpecialPowers.pushPrefEnv({
      set: [
        ["cookiebanners.service.mode", serviceMode],
        ["cookiebanners.cookieInjector.enabled", true],
        ["cookiebanners.service.detectOnly", detectOnly],
      ],
    });

    await visitTestSites();
    assertNoCookies();

    await SiteDataTestUtils.clear();
    await SpecialPowers.popPrefEnv();
  }
});

/**
 * Tests that we don't inject cookies if there are no matching rules.
 */
add_task(async function test_no_rules() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  info("Clearing existing rules");
  Services.cookieBanners.resetRules(false);

  await visitTestSites();
  assertNoCookies();

  await SiteDataTestUtils.clear();
});

/**
 * Tests that inject the correct cookies for matching rules and MODE_REJECT.
 */
add_task(async function test_mode_reject() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  await visitTestSites();

  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_A, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for ORIGIN_A"
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B"
  );
  // RULE_A also includes DOMAIN_C.
  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_C, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for ORIGIN_C"
  );

  await SiteDataTestUtils.clear();
});

/**
 * Tests that inject the correct cookies for matching rules and
 * MODE_REJECT_OR_ACCEPT.
 */
add_task(async function test_mode_reject_or_accept() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode",
        Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  await visitTestSites();

  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_A, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for ORIGIN_A"
  );
  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_B, [
      {
        key: `cookieConsent_${DOMAIN_B}_1`,
        value: "optIn1",
      },
    ]),
    "Should set opt-in cookies for ORIGIN_B"
  );
  // Rule a also includes DOMAIN_C
  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_C, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for ORIGIN_C"
  );

  await SiteDataTestUtils.clear();
});

/**
 * Test that embedded third-parties do not trigger cookie injection.
 */
add_task(async function test_embedded_third_party() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode",
        Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  info("Loading example.com with an iframe for example.org.");
  await BrowserTestUtils.withNewTab("https://example.com", async browser => {
    await SpecialPowers.spawn(browser, [], async () => {
      let iframe = content.document.createElement("iframe");
      iframe.src = "https://example.org";
      content.document.body.appendChild(iframe);
      await ContentTaskUtils.waitForEvent(iframe, "load");
    });
  });

  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_A, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for top-level ORIGIN_A"
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for embedded ORIGIN_B"
  );

  await SiteDataTestUtils.clear();
});

/**
 * Test that the injected cookies are present in the cookie header for the
 * initial top level document request.
 */
add_task(async function test_cookie_header_and_document() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });
  insertTestCookieRules();

  await BrowserTestUtils.withNewTab(TEST_COOKIE_HEADER_URL, async browser => {
    await SpecialPowers.spawn(browser, [], async () => {
      const EXPECTED_COOKIE_STR =
        "cookieConsent_example.com_1=optOut1; cookieConsent_example.com_2=optOut2";
      is(
        content.document.body.innerText,
        EXPECTED_COOKIE_STR,
        "Sent the correct cookie header."
      );
      is(
        content.document.cookie,
        EXPECTED_COOKIE_STR,
        "document.cookie has the correct cookie string."
      );
    });
  });

  await SiteDataTestUtils.clear();
});

/**
 * Test that cookies get properly injected for private browsing mode.
 */
add_task(async function test_pbm() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode.privateBrowsing",
        Ci.nsICookieBannerService.MODE_REJECT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });
  insertTestCookieRules();

  let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  let tab = BrowserTestUtils.addTab(pbmWindow.gBrowser, "about:blank");
  await BrowserTestUtils.loadURIString(tab.linkedBrowser, ORIGIN_A);
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  ok(
    SiteDataTestUtils.hasCookies(
      tab.linkedBrowser.contentPrincipal.origin,
      [
        {
          key: `cookieConsent_${DOMAIN_A}_1`,
          value: "optOut1",
        },
        {
          key: `cookieConsent_${DOMAIN_A}_2`,
          value: "optOut2",
        },
      ],
      true
    ),
    "Should set opt-out cookies for top-level ORIGIN_A in private browsing."
  );

  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_A),
    "Should not set any cookies for ORIGIN_A without PBM origin attribute."
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B"
  );

  await BrowserTestUtils.closeWindow(pbmWindow);
  await SiteDataTestUtils.clear();
});

/**
 * Test that cookies get properly injected for container tabs.
 */
add_task(async function test_container_tab() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode",
        Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });
  insertTestCookieRules();

  info("Loading ORIGIN_B in a container tab.");
  let tab = BrowserTestUtils.addTab(gBrowser, ORIGIN_B, {
    userContextId: 1,
  });
  await BrowserTestUtils.loadURIString(tab.linkedBrowser, ORIGIN_B);
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_A),
    "Should not set any cookies for ORIGIN_A"
  );
  ok(
    SiteDataTestUtils.hasCookies(tab.linkedBrowser.contentPrincipal.origin, [
      {
        key: `cookieConsent_${DOMAIN_B}_1`,
        value: "optIn1",
      },
    ]),
    "Should set opt-out cookies for top-level ORIGIN_B in user context 1."
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B in default user context"
  );

  BrowserTestUtils.removeTab(tab);
  await SiteDataTestUtils.clear();
});

/**
 * Test that if there is already a cookie with the given key, we don't overwrite
 * it. If the rule sets the unsetValue field, this cookie may be overwritten if
 * the value matches.
 */
add_task(async function test_no_overwrite() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode",
        Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  info("Pre-setting a cookie that should not be overwritten.");
  SiteDataTestUtils.addToCookies({
    origin: ORIGIN_A,
    host: `.${DOMAIN_A}`,
    path: "/",
    name: `cookieConsent_${DOMAIN_A}_1`,
    value: "KEEPME",
  });

  info("Pre-setting a cookie that should be overwritten, based on its value");
  SiteDataTestUtils.addToCookies({
    origin: ORIGIN_B,
    host: `.${DOMAIN_B}`,
    path: "/",
    name: `cookieConsent_${DOMAIN_B}_1`,
    value: "UNSET",
  });

  await visitTestSites();

  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_A, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "KEEPME",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should retain pre-set opt-in cookies for ORIGIN_A, but write new secondary cookie from rules."
  );
  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_B, [
      {
        key: `cookieConsent_${DOMAIN_B}_1`,
        value: "optIn1",
      },
    ]),
    "Should have overwritten cookie for ORIGIN_B, based on its value and the unsetValue rule property."
  );

  await SiteDataTestUtils.clear();
});

/**
 * Tests that cookies are injected for the base domain when visiting a
 * subdomain.
 */
add_task(async function test_subdomain() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  await visitTestSites([ORIGIN_A_SUB, ORIGIN_B]);

  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_A, [
      {
        key: `cookieConsent_${DOMAIN_A}_1`,
        value: "optOut1",
      },
      {
        key: `cookieConsent_${DOMAIN_A}_2`,
        value: "optOut2",
      },
    ]),
    "Should set opt-out cookies for ORIGIN_A when visiting subdomain."
  );
  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B"
  );

  await SiteDataTestUtils.clear();
});

add_task(async function test_site_preference() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["cookiebanners.service.mode", Ci.nsICookieBannerService.MODE_REJECT],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  await visitTestSites();

  ok(
    !SiteDataTestUtils.hasCookies(ORIGIN_B),
    "Should not set any cookies for ORIGIN_B"
  );

  info("Set the site preference of example.org to MODE_REJECT_OR_ACCEPT.");
  let uri = Services.io.newURI(ORIGIN_B);
  Services.cookieBanners.setDomainPref(
    uri,
    Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
    false
  );

  await visitTestSites();
  ok(
    SiteDataTestUtils.hasCookies(ORIGIN_B, [
      {
        key: `cookieConsent_${DOMAIN_B}_1`,
        value: "optIn1",
      },
    ]),
    "Should set opt-in cookies for ORIGIN_B"
  );

  Services.cookieBanners.removeAllDomainPrefs(false);
  await SiteDataTestUtils.clear();
});

add_task(async function test_site_preference_pbm() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "cookiebanners.service.mode.privateBrowsing",
        Ci.nsICookieBannerService.MODE_REJECT,
      ],
      ["cookiebanners.cookieInjector.enabled", true],
    ],
  });

  insertTestCookieRules();

  let pbmWindow = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  let tab = BrowserTestUtils.addTab(pbmWindow.gBrowser, "about:blank");
  await BrowserTestUtils.loadURIString(tab.linkedBrowser, ORIGIN_B);
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  ok(
    !SiteDataTestUtils.hasCookies(
      tab.linkedBrowser.contentPrincipal.origin,
      null,
      true
    ),
    "Should not set any cookies for ORIGIN_B in the private window"
  );

  info(
    "Set the site preference of example.org to MODE_REJECT_OR_ACCEPT. in the private window."
  );
  let uri = Services.io.newURI(ORIGIN_B);
  Services.cookieBanners.setDomainPref(
    uri,
    Ci.nsICookieBannerService.MODE_REJECT_OR_ACCEPT,
    true
  );

  await BrowserTestUtils.loadURIString(tab.linkedBrowser, ORIGIN_B);
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  ok(
    SiteDataTestUtils.hasCookies(
      tab.linkedBrowser.contentPrincipal.origin,
      [
        {
          key: `cookieConsent_${DOMAIN_B}_1`,
          value: "optIn1",
        },
      ],
      true
    ),
    "Should set opt-in cookies for ORIGIN_B"
  );

  Services.cookieBanners.removeAllDomainPrefs(true);
  BrowserTestUtils.removeTab(tab);
  await BrowserTestUtils.closeWindow(pbmWindow);
  await SiteDataTestUtils.clear();
});