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

"use strict";

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();

// LightweightThemeManager.fallbackThemeData setter calls are recorded here:
const gFallbackHistory = [];

// Simulates opening a browser window and retrieves the color.
async function getColorFromAppliedTheme() {
  const { LightweightThemeConsumer } = ChromeUtils.importESModule(
    "resource://gre/modules/LightweightThemeConsumer.sys.mjs"
  );

  // The LightweightThemeConsumer class expects a document (browser.xhtml), so
  // create a minimal one that looks like it. A blank document works because the
  // module does not have any other requirements for the document - the last
  // requirement (expecting some elements to exist) was removed in bug 1905726.
  const browser = Services.appShell.createWindowlessBrowser(true);
  const document = browser.document;

  let promise = new Promise(resolve => {
    // Will be invoked by LightweightThemeConsumer
    document.defaultView.addEventListener("windowlwthemeupdate", resolve, {
      once: true,
    });
  });

  new LightweightThemeConsumer(document);

  info("Waiting for windowlwthemeupdate from LightweightThemeConsumer");
  await promise;
  info("Got windowlwthemeupdate from LightweightThemeConsumer");
  // This is the color for "colors.frame".
  let accentcolor =
    document.documentElement.style.getPropertyValue("--lwt-accent-color");
  browser.close();
  return accentcolor;
}

function loadStaticThemeExtension(manifest) {
  const extensionId = manifest.browser_specific_settings.gecko.id;
  let extension = ExtensionTestUtils.loadExtension({
    manifest,
    useAddonManager: "permanent",
  });
  // When a theme is installed, it starts off in disabled mode, as seen in
  //  toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js .
  // To make sure that ExtensionTestUtils.loadExtension resolves, enable it
  // during installation.
  extension.onInstalling = function (addon) {
    if (addon.id === extensionId) {
      equal(addon.userDisabled, true, "Static theme is disabled at install");
      // We're modifying the AddonInternal's userDisabled flag instead of
      // calling addon.enable(), because using addon.enable() during
      // onInstalling results in the Extension's startup() to be called twice,
      // which is not realistic in the real world.
      addon.__AddonInternal__.userDisabled = false;
      equal(addon.userDisabled, false, "Static theme force enabled at install");
    }
  };
  return extension;
}

add_setup(function setup_intercept_fallbackThemeData() {
  const { LightweightThemeManager } = ChromeUtils.importESModule(
    "resource://gre/modules/LightweightThemeManager.sys.mjs"
  );
  const fallbackThemeDataPropertyDescriptor = Object.getOwnPropertyDescriptor(
    LightweightThemeManager,
    "fallbackThemeData"
  );
  Object.defineProperty(LightweightThemeManager, "fallbackThemeData", {
    enumerable: true,
    configurable: true,
    set: function (value) {
      gFallbackHistory.push(structuredClone(value));
      console.trace(`Set fallbackThemeData: ${value?.theme?.id}`);
      fallbackThemeDataPropertyDescriptor.set.call(this, value);
      if (value) {
        Assert.deepEqual(
          LightweightThemeManager.themeData,
          value,
          "Expected only valid fallback values to be set"
        );
      }
    },
    get: fallbackThemeDataPropertyDescriptor.get,
  });
});

add_setup(async () => {
  await ExtensionTestUtils.startAddonManager();
  // In reality, loading a built-in static theme would populate the fallback.
  // But we don't have a built-in static theme in this test.
  Assert.deepEqual(gFallbackHistory, [], "Fallback history is empty");
});

async function do_test_static_theme_startupData(expectDark) {
  const EXPECTED_COLOR = expectDark ? "rgb(65, 43, 21)" : "rgb(12, 34, 56)";

  let extension = loadStaticThemeExtension({
    browser_specific_settings: { gecko: { id: "@my-theme" } },
    theme: { colors: { frame: "rgb(12, 34, 56)" } },
    dark_theme: { colors: { frame: "rgb(65, 43, 21)" } },
  });
  await extension.startup();
  let startupDataOriginal = extension.extension.startupData;
  let startupDataCopy = structuredClone(extension.extension.startupData);

  equal(
    await getColorFromAppliedTheme(),
    EXPECTED_COLOR,
    "Theme applied to simulated browser window"
  );

  equal(
    extension.extension.startupData,
    startupDataOriginal,
    "startupData should be the same value (extension not unloaded)"
  );
  Assert.deepEqual(
    extension.extension.startupData,
    startupDataCopy,
    "startupData should not have been changed when a browser window was opened"
  );

  Assert.deepEqual(
    gFallbackHistory,
    [startupDataCopy.lwtData],
    "Fallback set when theme has loaded"
  );
  gFallbackHistory.length = 0;

  let readyPromise = AddonTestUtils.promiseWebExtensionStartup("@my-theme");
  info("Simulating browser restart");
  await AddonTestUtils.promiseShutdownManager();

  Assert.deepEqual(
    gFallbackHistory,
    [],
    "Should not have bothered with resetting the fallback upon app shutdown"
  );
  gFallbackHistory.length = 0;

  await AddonTestUtils.promiseStartupManager();
  info("Waiting for theme to have started again");
  await readyPromise;

  let startupData = extension.extension.startupData;
  // It cannot be the same object because it should have been loaded from disk.
  notEqual(startupData, startupDataOriginal, "Not trivially equal");

  Assert.deepEqual(
    startupDataCopy,
    startupData,
    "startupData should be identical when the theme loads again"
  );
  ok(startupData.lwtData, "startupData.lwtData should be set");
  // Expected to be set 2x: first as soon as the static theme extension has been
  // initialized by the AddonManager, secondly when the extension internals have
  // loaded and parsed the actual theme data.
  Assert.deepEqual(
    gFallbackHistory,
    [startupData.lwtData, startupData.lwtData],
    "Should have set startupData.lwtData as fallback at startup (2x)"
  );
  gFallbackHistory.length = 0;

  equal(
    await getColorFromAppliedTheme(),
    EXPECTED_COLOR,
    "Theme applied to simulated browser window after a restart"
  );

  await extension.unload();
  Assert.deepEqual(
    gFallbackHistory,
    [null],
    "Should have cleared fallback when static theme unloaded (theme uninstall)"
  );
  gFallbackHistory.length = 0;
}

add_task(
  { pref_set: [["ui.systemUsesDarkTheme", 0]] },
  async function test_static_theme_startupData_light_theme() {
    await do_test_static_theme_startupData(/* expectDark */ false);
  }
);

add_task(
  { pref_set: [["ui.systemUsesDarkTheme", 1]] },
  async function test_static_theme_startupData_dark_theme() {
    await do_test_static_theme_startupData(/* expectDark */ true);
  }
);

// Regression test for bug 1830144.
add_task(async function test_dynamic_theme_startupData() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version: 3,
      browser_specific_settings: { gecko: { id: "@my-dynamic-theme" } },
      permissions: ["theme"],
    },
    useAddonManager: "permanent",
    background() {
      browser.runtime.onInstalled.addListener(async () => {
        await browser.theme.update({ colors: { frame: "rgb(7, 8, 9)" } });
        browser.test.sendMessage("onInstalled");
      });
    },
  });
  await extension.startup();
  await extension.awaitMessage("onInstalled");

  equal(
    await getColorFromAppliedTheme(),
    "rgb(7, 8, 9)",
    "Dynamic theme applies to simulated browser window"
  );

  let startupData = extension.extension.startupData;

  // Notably, startupData.lwtData (among other properties) is not set. In a
  // previous test (test_static_theme_startupData) we have confirmed that this
  // property is present in startupData of static themes.
  Assert.deepEqual(
    Object.keys(startupData),
    ["persistentListeners"],
    "startupData should not have unexpected properties from ext-theme.js"
  );

  assertPersistentListeners(extension, "runtime", "onInstalled", {
    primed: false,
    persisted: true,
  });

  await extension.unload();
});

// Due to bug 1830144, startupData of an extension may contain stale data, which
// is not automatically dropped on updates (unlike StartupCache).
// Double-check that this does not break anything.
add_task(async function test_obsolete_theme_in_extension_startupData() {
  // We're going to load a fake theme to borrow its startupData.
  let theme = loadStaticThemeExtension({
    browser_specific_settings: { gecko: { id: "@theme-ext" } },
    theme: { colors: { frame: "rgb(1, 33, 7)" } },
  });
  await theme.startup();
  let startupDataFromStaticTheme = structuredClone(theme.extension.startupData);
  await theme.unload();

  // test_static_theme_startupData already tests the expected behavior for
  // static themes, so don't bother checking again and just reset the history.
  gFallbackHistory.length = 0;

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: "@theme-ext" } },
      permissions: ["theme"],
    },
    useAddonManager: "permanent",
  });
  await extension.startup();

  // The extension does not have anything of interest (such as persistent
  // listeners), so startupData should be empty.
  Assert.deepEqual(extension.extension.startupData, {}, "startupData is empty");
  extension.extension.startupData = startupDataFromStaticTheme;
  extension.extension.saveStartupData();

  let readyPromise = AddonTestUtils.promiseWebExtensionStartup("@theme-ext");
  await AddonTestUtils.promiseShutdownManager();
  await AddonTestUtils.promiseStartupManager();
  await readyPromise;
  await extension.unload();

  Assert.deepEqual(
    gFallbackHistory,
    [],
    "startupData.lwtData should be ignored for extensions that are not themes"
  );
});
