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

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

ChromeUtils.defineESModuleGetters(this, {
  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
});

// Used in multiple tests for loading a page in the sidebar
const TEST_CHAT_PROVIDER_URL = "http://mochi.test:8888/";

registerCleanupFunction(async () => {
  Services.prefs.clearUserPref("sidebar.old-sidebar.has-used");
});

/**
 * Check that chat sidebar renders
 */
add_task(async function test_sidebar_render() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["browser.ml.chat.provider", TEST_CHAT_PROVIDER_URL],
      ["browser.ml.chat.page", false],
    ],
  });

  await SidebarController.show("viewGenaiChatSidebar");

  const { document, getComputedStyle } =
    SidebarController.browser.contentWindow;

  const provider = document.getElementById("provider");

  Assert.ok(provider, "Rendered provider select");

  const summarizeBtnContainer = document.getElementById(
    "summarize-btn-container"
  );
  Assert.equal(
    getComputedStyle(summarizeBtnContainer).display,
    "none",
    "Button container set hidden"
  );

  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.chat.page", true]],
  });

  await TestUtils.waitForCondition(
    () => getComputedStyle(summarizeBtnContainer).display != "none",
    "Button container changed by css"
  );

  Assert.notEqual(
    getComputedStyle(summarizeBtnContainer).display,
    "none",
    "Button container set not hidden"
  );

  await SidebarController.hide();
});

/**
 * Check that chat sidebar renders providers
 */
add_task(async function test_sidebar_providers() {
  const countVisible = () =>
    [
      ...SidebarController.browser.contentWindow.document.getElementById(
        "provider"
      ).options,
    ].filter(option => !option.hidden && option.value).length;

  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.chat.provider", ""]],
  });
  await SidebarController.show("viewGenaiChatSidebar");

  const origCount = countVisible();
  Assert.equal(origCount, 5, "Rendered expected number of provider options");

  await SidebarController.hide();
  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.chat.hideLocalhost", false]],
  });
  await SidebarController.show("viewGenaiChatSidebar");

  Assert.equal(countVisible(), origCount + 1, "Added localhost option");

  await SidebarController.hide();
});

/**
 * Check that onboarding renders
 */
add_task(async function test_sidebar_onboarding() {
  Services.fog.testResetFOG();
  await SidebarController.show("viewGenaiChatSidebar");

  const win = SidebarController.browser.contentWindow;
  const { document, browserPromise } = win;
  const label = await TestUtils.waitForCondition(() =>
    document.querySelector("label:has(.localhost)")
  );
  Assert.ok(label, "Got a provider");
  let events =
    Glean.genaiChatbot.onboardingProviderChoiceDisplayed.testGetValue();
  Assert.equal(events.length, 1, "Displayed onboarding once");
  Assert.equal(events[0].extra.provider, "none", "Opened with no provider");
  Assert.equal(events[0].extra.step, "1", "First step");
  const browser = await browserPromise;
  Assert.ok(browser.hasAttribute("maychangeremoteness"), "Supports fission");
  Assert.equal(browser.currentURI.spec, "about:blank", "Nothing loaded yet");

  label.click();

  await TestUtils.waitForCondition(
    () => browser.currentURI.spec != "about:blank",
    "Should have previewed provider"
  );

  const link = await TestUtils.waitForCondition(() =>
    document.querySelector(".link-paragraph a")
  );
  const expectedURL = link.href;

  const sandbox = sinon.createSandbox();
  const stub = sandbox.stub(win, "openLink");

  link.click();

  Assert.ok(stub.calledOnce, "openLink should call once");
  Assert.equal(stub.firstCall.args[0], expectedURL);

  sandbox.restore();

  const pickButton = await TestUtils.waitForCondition(() =>
    document.querySelector(".chat_pick .primary:not([disabled])")
  );
  Assert.ok(pickButton, "Got button to activate provider");
  events = Glean.genaiChatbot.onboardingProviderSelection.testGetValue();
  Assert.equal(events.length, 1, "Selected one provider");
  Assert.equal(events[0].extra.provider, "localhost", "Picked localhost");
  Assert.equal(events[0].extra.step, "1", "First step");

  pickButton.click();

  Assert.equal(
    Services.prefs.getStringPref("browser.ml.chat.provider"),
    "http://localhost:8080",
    "Provider pref changed during onboarding"
  );

  const noOnboarding = await TestUtils.waitForCondition(
    () => !document.getElementById("multi-stage-message-root")
  );
  Assert.ok(noOnboarding, "Onboarding container went away");

  events = Glean.genaiChatbot.onboardingFinish.testGetValue();
  Assert.equal(events.length, 1, "Finished once");
  Assert.equal(
    events[0].extra.provider,
    "localhost",
    "Finished with localhost"
  );
  Assert.equal(events[0].extra.step, "1", "First step");

  Services.prefs.clearUserPref("browser.ml.chat.provider");
  await SidebarController.hide();
});

/**
 * Check that custom onboarding can be configured
 */
add_task(async function test_custom_onboarding() {
  await SpecialPowers.pushPrefEnv({
    set: [
      [
        "browser.ml.chat.onboarding.config",
        JSON.stringify({
          screens: [
            {
              content: {
                secondary_button: {
                  label: "custom",
                  action: { type: "chatbot:support" },
                },
              },
            },
          ],
        }),
      ],
    ],
  });
  await SidebarController.show("viewGenaiChatSidebar");
  const { document } = SidebarController.browser.contentWindow;
  const button = await TestUtils.waitForCondition(() =>
    document.querySelector(".secondary")
  );

  Assert.equal(button.textContent, "custom", "Custom button label");

  const loaded = BrowserTestUtils.firstBrowserLoaded(window, false);
  button.click();
  await loaded;
  const tab = gBrowser.selectedTab;

  Assert.equal(
    tab.linkedBrowser.currentURI.spec,
    "http://127.0.0.1:8888/support-dummy/ai-chatbot",
    "Custom support action opened tab"
  );

  BrowserTestUtils.removeTab(tab);
  await SidebarController.hide();
});

/**
 * Check that more options menu renders
 */
add_task(async function test_sidebar_menu() {
  Services.fog.testResetFOG();
  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.chat.provider", "http://mochi.test:8888"]],
  });

  await SidebarController.show("viewGenaiChatSidebar");

  const button =
    SidebarController.browser.contentWindow.document.getElementById(
      "header-more"
    );
  Assert.ok(button, "Rendered more menu button");

  button.click();
  const popup = await TestUtils.waitForCondition(() =>
    document.getElementById("chatbot-menupopup")
  );

  Assert.ok(popup, "Menu popup created");
  let items = popup.querySelectorAll("menuitem");
  Assert.equal(items.length, 4, "Items added to menu");
  Assert.ok(items[1].hasAttribute("checked"), "Shortcuts shown");
  Assert.ok(!items[2].hasAttribute("checked"), "Shortcuts not hidden");
  let events = Glean.genaiChatbot.sidebarMoreMenuDisplay.testGetValue();
  Assert.equal(events.length, 1, "Displayed menu once");
  Assert.equal(
    events[0].extra.provider,
    "custom",
    "Opened with custom provider"
  );

  // Disable shortcuts via menu
  items[2].click();
  const shown = BrowserTestUtils.waitForEvent(popup, "popupshown");
  Services.prefs.clearUserPref("browser.ml.chat.provider");
  button.click();
  await shown;

  items = popup.querySelectorAll("menuitem");
  Assert.ok(!items[1].hasAttribute("checked"), "Shortcuts not shown");
  Assert.ok(items[2].hasAttribute("checked"), "Shortcuts hidden");
  events = Glean.genaiChatbot.sidebarMoreMenuClick.testGetValue();
  Assert.equal(events.length, 1, "Clicked menu once");
  Assert.equal(
    events[0].extra.action,
    "hide_shortcuts",
    "Hide shortcuts clicked"
  );
  Assert.equal(events[0].extra.provider, "custom", "Still custom provider");
  Assert.equal(
    Glean.genaiChatbot.sidebarMoreMenuDisplay.testGetValue().length,
    2,
    "Opened second time"
  );

  Services.prefs.clearUserPref("browser.ml.chat.shortcuts");
  const hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
  popup.hidePopup();
  await hidden;
  await SidebarController.hide();
});

/**
 * Check that places doesn't get history entries from embedded provider browser
 */
add_task(async function test_sidebar_no_history() {
  // Earlier test opened sidebar with this test provider
  Assert.ok(
    !(await PlacesTestUtils.isPageInDB(TEST_CHAT_PROVIDER_URL)),
    "Earlier test with provider from test_sidebar_render is not in history"
  );
});

/**
 * Check that keyboard shortcut toggles and enables chatbot
 */
add_task(async function test_keyboard_shortcut() {
  const key = document.getElementById("viewGenaiChatSidebarKb");
  const enabled = "browser.ml.chat.enabled";
  await SpecialPowers.pushPrefEnv({ set: [[enabled, false]] });

  key.doCommand();

  Assert.ok(
    !Services.prefs.getBoolPref(enabled),
    "Keyboard shortcut doesn't flip the pref"
  );
  Assert.ok(
    !SidebarController.isOpen,
    "Keyboard shortcut doesn't Open chatbot if disabled"
  );

  await SpecialPowers.pushPrefEnv({ set: [[enabled, true]] });
  key.doCommand();

  Assert.ok(Services.prefs.getBoolPref(enabled), "Enabled with keyboard");
  Assert.ok(SidebarController.isOpen, "Opened chatbot with keyboard");

  key.doCommand();

  Assert.ok(!SidebarController.isOpen, "Closed chatbot with keyboard");

  Assert.ok(
    Services.prefs.getBoolPref(enabled),
    "Keyboard shortcut doesn't flip the pref"
  );

  const events = Glean.genaiChatbot.keyboardShortcut.testGetValue();
  Assert.equal(events.length, 3, "Got 3 keyboard events");
  Assert.equal(events[0].extra.enabled, "false", "Initially disabled");
  Assert.equal(events[0].extra.sidebar, "", "Initially closed");
  Assert.equal(events[1].extra.enabled, "true", "Already enabled");
  Assert.equal(events[1].extra.sidebar, "", "Still closed");
  Assert.equal(events[2].extra.enabled, "true", "Still enabled");
  Assert.equal(
    events[2].extra.sidebar,
    "viewGenaiChatSidebar",
    "Already opened"
  );
});

/**
 * Check Picture in Picture actors are not attached in sidebar chatbot
 */
add_task(async function test_pip_actor_not_chat_sidebar() {
  await BrowserTestUtils.withNewTab("about:blank", async browser => {
    const wgp = browser.browsingContext.currentWindowGlobal;
    const actor = wgp.getActor("PictureInPicture");
    Assert.ok(actor, "PiP actor is attached in the tab content");

    await SidebarController.show("viewGenaiChatSidebar");

    const chatbotBrowser = await TestUtils.waitForCondition(() => {
      const { document } = SidebarController.browser.contentWindow;
      const chatbotBrowserContainer =
        document.getElementById("browser-container");
      return chatbotBrowserContainer.querySelector("browser");
    }, "Chatbot <browser> is loaded in the sidebar");

    Assert.throws(
      () =>
        chatbotBrowser.browsingContext.currentWindowGlobal.getActor(
          "PictureInPicture"
        ),
      /doesn't match message manager group/i,
      "Getting PiP actor in sidebar chatbot should throw"
    );

    await SidebarController.hide();
  });
});

add_task(
  async function test_chatbot_microphone_access_if_persistent_perm_already_granted_in_tab() {
    await SpecialPowers.pushPrefEnv({
      set: [
        ["media.navigator.streams.fake", true],
        ["browser.ml.chat.provider", "https://example.org"],
      ],
    });

    await BrowserTestUtils.withNewTab("https://example.org", async browser => {
      const { principal, rawId } = await SpecialPowers.spawn(
        browser,
        [],
        async () => {
          const stream = await content.navigator.mediaDevices.getUserMedia({
            audio: true,
          });
          const track = stream.getAudioTracks()[0];
          const id = track.getSettings().deviceId || "default";
          stream.getTracks().forEach(t => t.stop());
          return {
            principal: content.document.nodePrincipal,
            rawId: id,
          };
        }
      );

      Assert.ok(rawId, "Got microphone rawId from the tab");

      const key = "microphone";
      SitePermissions.setForPrincipal(
        principal,
        key,
        SitePermissions.ALLOW,
        SitePermissions.SCOPE_PERSISTENT,
        browser
      );

      await SidebarController.show("viewGenaiChatSidebar");

      const { document } = SidebarController.browser.contentWindow;
      const chatbotBrowserContainer =
        document.getElementById("browser-container");
      const chatbotBrowser = chatbotBrowserContainer.querySelector("browser");

      await BrowserTestUtils.browserLoaded(chatbotBrowser, false, url => {
        return new URL(url).origin === "https://example.org";
      });

      const chatbotPrincipal = await SpecialPowers.spawn(
        chatbotBrowser,
        [],
        async () => content.document.nodePrincipal
      );

      let shown = false;
      let onShown = () => {
        shown = true;
      };
      PopupNotifications.panel.addEventListener("popupshown", onShown);

      await SpecialPowers.spawn(chatbotBrowser, [rawId], async id => {
        const { WebRTCChild } = SpecialPowers.ChromeUtils.importESModule(
          "resource:///actors/WebRTCChild.sys.mjs"
        );

        const mic = [
          {
            type: "audioinput",
            rawName: "fake mic",
            rawId: id,
            id,
            QueryInterface: ChromeUtils.generateQI([Ci.nsIMediaDevice]),
            mediaSource: "microphone",
          },
        ];

        const req = {
          type: "getUserMedia",
          windowID: content.windowGlobalChild.outerWindowId,
          isSecure: true,
          isHandlingUserInput: true,

          audioInputDevices: mic,
          videoInputDevices: [],
          audioOutputDevices: [],

          deviceIndex: 0,
          devices: mic,

          getConstraints: () => ({ audio: true }),
          getAudioInputOptions: () => ({ deviceId: id }),
          getVideoInputOptions: () => ({}),
          getAudioOutputOptions: () => ({}),
        };

        WebRTCChild.observe(req, "getUserMedia:request");
      });

      const sidebarMicPerm = SitePermissions.getForPrincipal(
        chatbotPrincipal,
        key,
        chatbotBrowser
      );

      is(
        sidebarMicPerm.state,
        SitePermissions.ALLOW,
        "Sidebar chatbot has granted mic allow"
      );
      is(
        sidebarMicPerm.scope,
        SitePermissions.SCOPE_PERSISTENT,
        "sidebar chatbot mic access is persistent"
      );
      Assert.ok(
        !shown,
        "PopupNotification didn't fire because the mic access has been granted"
      );

      PopupNotifications.panel.removeEventListener("popupshown", onShown);
      SitePermissions.removeFromPrincipal(principal, key, browser);
      SitePermissions.removeFromPrincipal(
        chatbotPrincipal,
        key,
        chatbotBrowser
      );
    });

    await SidebarController.hide();
    await SpecialPowers.popPrefEnv();
    Services.fog.testResetFOG();
  }
);
