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

"use strict";

/**
 * @typedef {object} TestRenderTemplateResult
 * @property {string} markup
 *   The rendered template markup as a string.
 * @property {Document} backupDOM
 *   The rendered template as parsed by a DOMParser.
 */

/**
 * Renders a template and returns an object that contains both the raw markup
 * and the DOM of the markup as parsed by DOMParser.
 *
 * @param {boolean} isEncrypted
 *   True if the template should report that the backup is encrypted.
 * @param {object} metadata
 *   The metadata for the backup. See the BackupManifest schema for details.
 * @returns {TestRenderTemplateResult}
 */
async function testRenderTemplate(isEncrypted, metadata = FAKE_METADATA) {
  let bs = new BackupService();
  let markup = await bs.renderTemplate(
    BackupService.ARCHIVE_TEMPLATE,
    isEncrypted,
    metadata
  );
  let backupDOM = new DOMParser().parseFromString(markup, "text/html");
  return { backupDOM, markup };
}

add_setup(() => {
  // Setting this pref lets us use Cu.evalInSandbox to run the archive.js
  // script.
  Services.prefs.setBoolPref(
    "security.allow_parent_unrestricted_js_loads",
    true
  );
  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
  });
});

/**
 * Tests that header matches our expectations. The header of this file should
 * be:
 *
 * <!DOCTYPE html>
 * <!-- Version: 1 -->
 */
add_task(async function test_header() {
  let { markup } = await testRenderTemplate(false /* isEncrypted */);
  const EXPECTED_HEADER =
    /^<!DOCTYPE html>[\r\n]+<!-- Version: (\d+) -->[\r\n]+/;
  Assert.ok(
    markup.match(EXPECTED_HEADER),
    "Should have found the expected header."
  );
});

/**
 * Tests that the `encryption-state` DOM node says "Not encrypted" if the backup
 * is not encrypted, and "Encrypted" otherwise.
 */
add_task(async function test_encryption_state() {
  let { backupDOM } = await testRenderTemplate(false /* isEncrypted */);
  Assert.equal(
    backupDOM.querySelector("#encryption-state-value").textContent,
    "No"
  );

  ({ backupDOM } = await testRenderTemplate(true /* isEncrypted */));
  Assert.equal(
    backupDOM.querySelector("#encryption-state-value").textContent,
    "Yes"
  );
});

/**
 * Tests that metadata is properly inserted. The expected metadata inserted on
 * the page is the time and date of the backup, as well as the name of the
 * machine that the backup was created on.
 */
add_task(async function test_metadata() {
  let { backupDOM } = await testRenderTemplate(true /* isEncrypted */);
  let backupDate = new Date(FAKE_METADATA.date);
  let expectedDate = new Intl.DateTimeFormat("en-US", {
    dateStyle: "short",
  }).format(backupDate);
  let expectedTime = new Intl.DateTimeFormat("en-US", {
    timeStyle: "short",
  }).format(backupDate);
  Assert.equal(
    backupDOM.querySelector("#creation-date-value").textContent,
    `${expectedTime}, ${expectedDate}`
  );
  Assert.equal(
    backupDOM.querySelector("#creation-device-value").textContent,
    "A super cool machine"
  );
});

/**
 * Tests that metadata is properly escaped. This isn't exhaustive, since we're
 * using Fluent under the hood, which is tested pretty widely already.
 */
add_task(async function test_hostile_metadata() {
  let { backupDOM } = await testRenderTemplate(true /* isEncrypted */, {
    date: "<script>alert('test');</script>",
    appName: "<script>alert('test');</script>",
    appVersion: "<script>alert('test');</script>",
    buildID: "<script>alert('test');</script>",
    profileName: "<script>alert('test');</script>",
    machineName: "<script>alert('test');</script>",
    osName: "<script>alert('test');</script>",
    osVersion: "<script>alert('test');</script>",
    legacyClientID: "<script>alert('test');</script>",
    profileGroupID: "<script>alert('test');</script>",
    accountID: "<script>alert('test');</script>",
    accountEmail: "<script>alert('test');</script>",
  });

  let scriptTags = backupDOM.querySelectorAll("script");
  Assert.equal(
    scriptTags.length,
    1,
    "There should only be 1 script tag on the page."
  );
  let scriptContent = scriptTags[0].innerHTML;
  let evalSandbox = Cu.Sandbox(Cu.getGlobalForObject({}));

  evalSandbox.navigator = {
    userAgent: "",
  };
  evalSandbox.document = {
    getElementById: sinon.stub().callsFake((...args) => {
      return backupDOM.getElementById(...args);
    }),
    body: {
      toggleAttribute: sinon.stub(),
    },
    location: {
      pathname: "test_archive.html",
    },
  };
  evalSandbox.alert = sinon.stub();

  Cu.evalInSandbox(scriptContent, evalSandbox);

  Assert.ok(evalSandbox.alert.notCalled, "alert() was never called");
});

add_task(async function test_backup_file_path_from_javascript() {
  let { backupDOM } = await testRenderTemplate(false /* isEncrypted */);
  let scriptTags = backupDOM.querySelectorAll("script");
  Assert.equal(
    scriptTags.length,
    1,
    "There should only be 1 script tag on the page."
  );
  let scriptContent = scriptTags[0].innerHTML;
  let evalSandbox = Cu.Sandbox(Cu.getGlobalForObject({}));

  evalSandbox.navigator = {
    userAgent:
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0",
  };
  evalSandbox.document = {
    getElementById: sinon.stub().callsFake((...args) => {
      return backupDOM.getElementById(...args);
    }),
    body: {
      toggleAttribute: sinon.stub(),
    },
    location: {
      pathname: "test_archive.html",
    },
  };

  Cu.evalInSandbox(scriptContent, evalSandbox);

  Assert.equal(
    backupDOM.querySelector("#backup-file-path-value").textContent,
    "test_archive.html",
    "backup file path should have been sourced from document.location"
  );
});

/**
 * Tests that if the User Agent is a browser that includes "Firefox", that
 * toggleAttribute("is-moz-browser", true) is called on document.body.
 */
add_task(async function test_moz_browser_handling() {
  let { backupDOM } = await testRenderTemplate(false /* isEncrypted */);
  let scriptTags = backupDOM.querySelectorAll("script");
  Assert.equal(
    scriptTags.length,
    1,
    "There should only be 1 script tag on the page."
  );
  let scriptContent = scriptTags[0].innerHTML;
  let evalSandbox = Cu.Sandbox(Cu.getGlobalForObject({}));

  evalSandbox.navigator = {
    userAgent:
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:129.0) Gecko/20100101 Firefox/129.0",
  };
  evalSandbox.document = {
    getElementById: sinon.stub().callsFake((...args) => {
      return backupDOM.getElementById(...args);
    }),
    body: {
      toggleAttribute: sinon.stub(),
    },
    location: {
      pathname: "test_archive.html",
    },
  };

  Cu.evalInSandbox(scriptContent, evalSandbox);

  Assert.ok(
    evalSandbox.document.body.toggleAttribute.calledOnce,
    "document.body.toggleAttribute called"
  );
  Assert.ok(
    evalSandbox.document.body.toggleAttribute.calledWith(
      "is-moz-browser",
      true
    ),
    "document.body.toggleAttribute called setting is-moz-browser to true"
  );
});

/**
 * Tests that if the User Agent is a browser that does not include "Firefox",
 * that toggleAttribute("is-moz-browser", false) is called on document.body.
 */
add_task(async function test_non_moz_browser_handling() {
  let { backupDOM } = await testRenderTemplate(true /* isEncrypted */);
  let scriptTags = backupDOM.querySelectorAll("script");
  Assert.equal(
    scriptTags.length,
    1,
    "There should only be 1 script tag on the page."
  );
  let scriptContent = scriptTags[0].innerHTML;
  let evalSandbox = Cu.Sandbox(Cu.getGlobalForObject({}));

  evalSandbox.navigator = {
    userAgent:
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15",
  };
  evalSandbox.document = {
    getElementById: sinon.stub().callsFake((...args) => {
      return backupDOM.getElementById(...args);
    }),
    body: {
      toggleAttribute: sinon.stub(),
    },
    location: {
      pathname: "test_archive.html",
    },
  };

  Cu.evalInSandbox(scriptContent, evalSandbox);

  Assert.ok(
    evalSandbox.document.body.toggleAttribute.calledOnce,
    "document.body.toggleAttribute called"
  );
  Assert.ok(
    evalSandbox.document.body.toggleAttribute.calledWith(
      "is-moz-browser",
      false
    ),
    "document.body.toggleAttribute called setting is-moz-browser to false"
  );
});

/**
 * Tests that the license header does not exist in the generated rendering.
 */
add_task(async function test_no_license() {
  let { markup } = await testRenderTemplate(true /* isEncrypted */);

  // Instead of looking for the exact license header (which might be indented)
  // in such a way as to make string-searching brittle) we'll just look for
  // a key part of the license header, which is the reference to
  // https://mozilla.org/MPL.

  Assert.ok(
    !markup.includes("https://mozilla.org/MPL"),
    "The license headers were stripped."
  );
});

/**
 * Tests that the "Learn More" support link/SUMO link has the expected UTM
 * parameters.
 */
add_task(async function test_support_link_utm_parameters() {
  let { backupDOM } = await testRenderTemplate(false /* isEncrypted */);

  let supportLinkElement = backupDOM.getElementById("support-link");
  Assert.ok(supportLinkElement, "support link should be found");
  Assert.ok(
    supportLinkElement.href,
    "support link should have a non-empty href"
  );

  let supportLinkUrl = new URL(supportLinkElement.href);
  let { searchParams } = supportLinkUrl;
  Assert.equal(
    searchParams.get("utm_medium"),
    "firefox-desktop",
    "utm_medium should be firefox-desktop"
  );
  Assert.equal(
    searchParams.get("utm_source"),
    "html-backup",
    "utm_source should be html-backup"
  );
  Assert.equal(
    searchParams.get("utm_campaign"),
    "fx-backup-restore",
    "utm_campaign should be fx-backup-restore"
  );
});
