/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { ContextualIdentityListener } = ChromeUtils.importESModule(
  "chrome://remote/content/shared/listeners/ContextualIdentityListener.sys.mjs"
);

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

// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
const { ProxyConfiguration } = ChromeUtils.importESModule(
  "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs"
);

const { ProxyPerUserContextManager } = ChromeUtils.importESModule(
  "chrome://remote/content/webdriver-bidi/ProxyPerUserContextManager.sys.mjs"
);

const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(
  Ci.nsINativeDNSResolverOverride
);

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["network.proxy.allow_hijacking_localhost", true]],
  });
  gOverride.addIPOverride("test1.example.com", "127.0.0.1");

  registerCleanupFunction(async () => {
    gOverride.clearOverrides();
  });
});

add_task(async function test_manual_http_proxy_per_user_context() {
  const [serverURL, proxyURL] = createHTTPProxy();

  const proxyManager = new ProxyPerUserContextManager();

  const userContext = ContextualIdentityService.create("test_name");
  const { userContextId } = userContext;

  const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
    userContextId,
  });
  const browserInUserContext = tabInUserContext.linkedBrowser;

  info("Set up a manual proxy for user context");
  const proxyConfiguration = ProxyConfiguration.fromJSON({
    proxyType: "manual",
    httpProxy: proxyURL,
  });
  proxyManager.addConfiguration(userContextId, proxyConfiguration);

  info("Verify that navigation request in user context is proxied");
  await isPageProxied(browserInUserContext, serverURL);

  const tabInDefaultUserContext = BrowserTestUtils.addTab(gBrowser, serverURL);
  const browserInDefaultUserContext = tabInDefaultUserContext.linkedBrowser;

  info("Verify that navigation request in default user context is not proxied");
  await isPageNotProxied(browserInDefaultUserContext, serverURL);

  info("Destroy proxy manager");
  proxyManager.destroy();

  info("Verify that navigation request in user context is not proxied");
  await isPageNotProxied(browserInUserContext, serverURL);

  BrowserTestUtils.removeTab(tabInUserContext);
  ContextualIdentityService.remove(userContextId);
  BrowserTestUtils.removeTab(tabInDefaultUserContext);
});

add_task(async function test_manual_http_proxy_per_user_context_with_noProxy() {
  const [serverURL, proxyURL] = createHTTPProxy();

  const proxyManager = new ProxyPerUserContextManager();

  const userContext = ContextualIdentityService.create("test_name");
  const { userContextId } = userContext;

  const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
    userContextId,
  });
  const browserInUserContext = tabInUserContext.linkedBrowser;

  info("Set up a manual proxy for user context with `noProxy` field");
  const proxyConfiguration = ProxyConfiguration.fromJSON({
    proxyType: "manual",
    httpProxy: proxyURL,
    noProxy: ["test1.example.com"],
  });
  proxyManager.addConfiguration(userContextId, proxyConfiguration);

  info("Verify that navigation request is not proxied");
  await isPageNotProxied(browserInUserContext, serverURL);

  proxyManager.destroy();

  BrowserTestUtils.removeTab(tabInUserContext);
  ContextualIdentityService.remove(userContextId);
});

add_task(
  async function test_manual_http_proxy_per_user_context_with_noProxy_with_url_pattern() {
    const [serverURL, proxyURL, secondServerURL] = createHTTPProxy();

    const proxyManager = new ProxyPerUserContextManager();

    const userContext = ContextualIdentityService.create("test_name");
    const { userContextId } = userContext;

    const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
      userContextId,
    });
    const browserInUserContext = tabInUserContext.linkedBrowser;

    info(
      "Set up a manual proxy for user context with `noProxy` field for an URL pattern"
    );
    const proxyConfiguration = ProxyConfiguration.fromJSON({
      proxyType: "manual",
      httpProxy: proxyURL,
      noProxy: [".example.com"],
    });
    proxyManager.addConfiguration(userContextId, proxyConfiguration);

    info("Verify that navigation request is not proxied");
    await isPageNotProxied(browserInUserContext, serverURL);

    info("Verify that navigation to url not matching the pattern is proxied");
    await isPageProxied(browserInUserContext, secondServerURL);

    proxyManager.destroy();

    BrowserTestUtils.removeTab(tabInUserContext);
    ContextualIdentityService.remove(userContextId);
  }
);

add_task(async function test_override_manual_http_proxy_per_user_context() {
  const [serverURL, proxyURL] = createHTTPProxy();

  const proxyManager = new ProxyPerUserContextManager();

  const userContext = ContextualIdentityService.create("test_name");
  const { userContextId } = userContext;

  const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
    userContextId,
  });
  const browserInUserContext = tabInUserContext.linkedBrowser;

  info("Set up a manual proxy for user context");
  const proxyConfiguration = ProxyConfiguration.fromJSON({
    proxyType: "manual",
    httpProxy: proxyURL,
  });
  proxyManager.addConfiguration(userContextId, proxyConfiguration);

  info("Verify that navigation request is proxied");
  await isPageProxied(browserInUserContext, serverURL);

  info("Set a new proxy configuration for user context with direct proxy");
  const newProxyConfiguration = ProxyConfiguration.fromJSON({
    proxyType: "direct",
  });
  proxyManager.addConfiguration(userContextId, newProxyConfiguration);

  info("Verify that navigation request is not proxied");
  await isPageNotProxied(browserInUserContext, serverURL);

  proxyManager.destroy();

  BrowserTestUtils.removeTab(tabInUserContext);
  ContextualIdentityService.remove(userContextId);
});

add_task(
  async function test_override_global_manual_proxy_with_direct_proxy_per_user_context() {
    const [serverURL, proxyURL] = createHTTPProxy();

    info("Set up a global manual proxy");
    const globalProxy = ProxyConfiguration.fromJSON({
      proxyType: "manual",
      httpProxy: proxyURL,
    });

    globalProxy.init();

    const tabInDefaultUserContext = BrowserTestUtils.addTab(
      gBrowser,
      serverURL
    );
    const browserInDefaultUserContext = tabInDefaultUserContext.linkedBrowser;

    info("Verify that navigation request is proxied");
    await isPageProxied(browserInDefaultUserContext, serverURL);

    const proxyManager = new ProxyPerUserContextManager();

    const userContext = ContextualIdentityService.create("test_name");
    const { userContextId } = userContext;

    const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
      userContextId,
    });
    const browserInUserContext = tabInUserContext.linkedBrowser;

    info("Set up a direct proxy for user context");
    const proxyConfiguration = ProxyConfiguration.fromJSON({
      proxyType: "direct",
    });
    proxyManager.addConfiguration(userContextId, proxyConfiguration);

    info("Verify that navigation request is not proxied");
    await isPageNotProxied(browserInUserContext, serverURL);

    proxyManager.destroy();

    globalProxy.destroy();

    BrowserTestUtils.removeTab(tabInUserContext);
    ContextualIdentityService.remove(userContextId);
    BrowserTestUtils.removeTab(tabInDefaultUserContext);
  }
);

add_task(
  async function test_override_global_system_proxy_with_direct_proxy_per_user_context() {
    const [serverURL] = createHTTPProxy();

    info("Set up a global system proxy");
    const globalProxy = ProxyConfiguration.fromJSON({
      proxyType: "system",
    });

    globalProxy.init();

    const proxyManager = new ProxyPerUserContextManager();

    const userContext = ContextualIdentityService.create("test_name");
    const { userContextId } = userContext;

    const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
      userContextId,
    });
    const browserInUserContext = tabInUserContext.linkedBrowser;

    info("Set up a direct proxy for user context");
    const proxyConfiguration = ProxyConfiguration.fromJSON({
      proxyType: "direct",
    });

    proxyManager.addConfiguration(userContextId, proxyConfiguration);

    info("Verify that navigation request is not proxied");
    await isPageNotProxied(browserInUserContext, serverURL);

    proxyManager.destroy();

    globalProxy.destroy();

    BrowserTestUtils.removeTab(tabInUserContext);
    ContextualIdentityService.remove(userContextId);
  }
);

add_task(
  async function test_delete_configuration_for_manual_http_proxy_per_user_context() {
    const [serverURL, proxyURL] = createHTTPProxy();

    const proxyManager = new ProxyPerUserContextManager();

    const userContext = ContextualIdentityService.create("test_name");
    const { userContextId } = userContext;

    const tabInUserContext = BrowserTestUtils.addTab(gBrowser, serverURL, {
      userContextId,
    });
    const browserInUserContext = tabInUserContext.linkedBrowser;

    info("Set up a manual proxy for user context");
    const proxyConfiguration = ProxyConfiguration.fromJSON({
      proxyType: "manual",
      httpProxy: proxyURL,
    });
    proxyManager.addConfiguration(userContextId, proxyConfiguration);

    info("Verify that navigation request is proxied");
    await isPageProxied(browserInUserContext, serverURL);

    info("Delete the proxy configuration");
    proxyManager.deleteConfiguration(userContextId);

    info("Verify that navigation request is not proxied");
    await isPageNotProxied(browserInUserContext, serverURL);

    info("Try to delete the configuration again");
    proxyManager.deleteConfiguration(userContextId);

    proxyManager.destroy();

    BrowserTestUtils.removeTab(tabInUserContext);
    ContextualIdentityService.remove(userContextId);
  }
);

function createHTTPProxy() {
  const server = new HttpServer();
  server.start(-1);
  server.identity.add("http", "test1.example.com", server.identity.primaryPort);
  server.identity.add("http", "example.com", server.identity.primaryPort);
  server.registerPathHandler("/", (request, response) => {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "text/plain", true);
    response.write("Not proxied");
  });

  const proxyServer = new HttpServer();
  proxyServer.start(-1);
  proxyServer.identity.add(
    "http",
    "test1.example.com",
    server.identity.primaryPort
  );
  proxyServer.identity.add("http", "example.com", server.identity.primaryPort);
  proxyServer.registerPathHandler("/", (request, response) => {
    response.setStatusLine(request.httpVersion, 200, "OK");
    response.setHeader("Content-Type", "text/plain", true);
    response.write("Proxied");
  });

  registerCleanupFunction(async () => {
    await server.stop();
    await proxyServer.stop();
  });

  return [
    // eslint-disable-next-line sdl/no-insecure-url
    `http://test1.example.com:${server.identity.primaryPort}`,
    `localhost:${proxyServer.identity.primaryPort}`,
    // eslint-disable-next-line sdl/no-insecure-url
    `http://example.com:${server.identity.primaryPort}`,
  ];
}

async function isPageProxied(browser, url) {
  await loadPage(browser, url);
  return SpecialPowers.spawn(browser, [], async () => {
    Assert.strictEqual(
      content.document.body.textContent,
      "Proxied",
      "The page was proxied"
    );
  });
}

async function isPageNotProxied(browser, url) {
  await loadPage(browser, url);
  return SpecialPowers.spawn(browser, [], async () => {
    Assert.strictEqual(
      content.document.body.textContent,
      "Not proxied",
      "The page was not proxied"
    );
  });
}

async function loadPage(browser, url) {
  const loadedPromise = BrowserTestUtils.browserLoaded(browser);
  BrowserTestUtils.startLoadingURIString(browser, url);
  return loadedPromise;
}
