"use strict";

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

let httpserver = null;
let lnaServer = null;

ChromeUtils.defineLazyGetter(this, "cpURI", function () {
  return (
    "http://localhost:" + httpserver.identity.primaryPort + "/captive.html"
  );
});

ChromeUtils.defineLazyGetter(this, "LNA_URL", function () {
  return "http://localhost:" + lnaServer.identity.primaryPort + "/test";
});

const SUCCESS_STRING =
  '<meta http-equiv="refresh" content="0;url=https://support.mozilla.org/kb/captive-portal"/>';
let cpResponse = SUCCESS_STRING;

function captivePortalHandler(metadata, response) {
  response.setHeader("Content-Type", "text/html");
  response.bodyOutputStream.write(cpResponse, cpResponse.length);
}

function lnaHandler(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  let body = "success";
  response.bodyOutputStream.write(body, body.length);
}

const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled";
const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode";
const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL";
const PREF_CAPTIVE_CANONICAL_CONTENT = "captivedetect.canonicalContent";
const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval";
const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval";
const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost";

const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
  Ci.nsICaptivePortalService
);

function makeChannel(url, triggeringPrincipalURI = null) {
  let uri = NetUtil.newURI(url);
  var principal = Services.scriptSecurityManager.createContentPrincipal(
    uri,
    {}
  );

  var triggeringPrincipal;
  if (triggeringPrincipalURI) {
    let triggeringURI = NetUtil.newURI(triggeringPrincipalURI);
    triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
      triggeringURI,
      {}
    );
  } else {
    let triggeringURI = NetUtil.newURI("https://public.example.com");
    triggeringPrincipal = Services.scriptSecurityManager.createContentPrincipal(
      triggeringURI,
      {}
    );
  }

  return NetUtil.newChannel({
    uri: url,
    loadingPrincipal: principal,
    triggeringPrincipal,
    securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
  }).QueryInterface(Ci.nsIHttpChannel);
}

add_setup(async function () {
  // Setup captive portal detection server
  httpserver = new HttpServer();
  httpserver.registerPathHandler("/captive.html", captivePortalHandler);
  httpserver.start(-1);

  // Setup LNA target server
  lnaServer = new HttpServer();
  lnaServer.registerPathHandler("/test", lnaHandler);
  lnaServer.start(-1);

  // Configure captive portal service
  Services.prefs.setCharPref(PREF_CAPTIVE_CANONICAL_CONTENT, SUCCESS_STRING);
  Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI);
  Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50);
  Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100);
  Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true);
  Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true);

  // Configure LNA blocking
  Services.prefs.setBoolPref("network.lna.blocking", true);
  Services.prefs.setBoolPref("network.loopback-network.prompt.testing", true);
  Services.prefs.setBoolPref("network.localnetwork.prompt.testing", true);

  registerCleanupFunction(async () => {
    Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED);
    Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE);
    Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT);
    Services.prefs.clearUserPref(PREF_CAPTIVE_CANONICAL_CONTENT);
    Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME);
    Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME);
    Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST);
    Services.prefs.clearUserPref("network.lna.blocking");
    Services.prefs.clearUserPref("network.loopback-network.prompt.testing");
    Services.prefs.clearUserPref("network.localnetwork.prompt.testing");
    Services.prefs.clearUserPref(
      "network.loopback-network.prompt.testing.allow"
    );
    Services.prefs.clearUserPref("network.localnetwork.prompt.testing.allow");
    Services.prefs.clearUserPref("network.lna.address_space.private.override");

    await new Promise(resolve => {
      httpserver.stop(resolve);
    });
    await new Promise(resolve => {
      lnaServer.stop(resolve);
    });
  });
});

function observerPromise(topic) {
  return new Promise(resolve => {
    let observer = {
      QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
      observe(aSubject, aTopic, aData) {
        if (aTopic == topic) {
          Services.obs.removeObserver(observer, topic);
          resolve(aData);
        }
      },
    };
    Services.obs.addObserver(observer, topic);
  });
}

add_task(async function test_localnetwork_blocked_without_captive_portal() {
  // Override address space to treat this localhost:port as Private (local network)
  Services.prefs.setCharPref(
    "network.lna.address_space.private.override",
    "127.0.0.1:" + lnaServer.identity.primaryPort
  );

  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
  Services.prefs.setBoolPref(
    "network.localnetwork.prompt.testing.allow",
    false
  );

  let chan = makeChannel(LNA_URL);
  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;

  await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
  });

  Assert.equal(
    chan.status,
    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    "Request should be blocked when captive portal is not active"
  );
  Services.prefs.clearUserPref("network.lna.address_space.private.override");
});

add_task(async function test_localnetwork_allowed_with_captive_portal() {
  // Override address space to treat this localhost:port as Private (local network)
  Services.prefs.setCharPref(
    "network.lna.address_space.private.override",
    "127.0.0.1:" + lnaServer.identity.primaryPort
  );
  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);

  // Start captive portal service and wait for it to detect "no captive portal"
  let notification = observerPromise("network:captive-portal-connectivity");
  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
  await notification;
  Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);

  // Trigger captive portal detection (locked state)
  cpResponse = "captive portal page";
  notification = observerPromise("captive-portal-login");
  cps.recheckCaptivePortal();
  await notification;
  Assert.equal(
    cps.state,
    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    "Captive portal should be in LOCKED_PORTAL state"
  );

  // Set prompt to deny - but it should still succeed because captive portal is active
  Services.prefs.setBoolPref(
    "network.localnetwork.prompt.testing.allow",
    false
  );

  let chan = makeChannel(LNA_URL);
  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;

  await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, 0));
  });

  Assert.equal(
    chan.status,
    Cr.NS_OK,
    "Request should succeed when captive portal is active (locked)"
  );

  // Cleanup: unlock the captive portal
  cpResponse = SUCCESS_STRING;
  notification = observerPromise("captive-portal-login-success");
  cps.recheckCaptivePortal();
  await notification;
  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);

  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
  Services.prefs.clearUserPref("network.lna.address_space.private.override");
});

add_task(async function test_localhost_blocked_during_captive_portal() {
  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);

  // Start captive portal service and wait for it to detect "no captive portal"
  let notification = observerPromise("network:captive-portal-connectivity");
  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true);
  await notification;
  Assert.equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE);

  // Trigger captive portal detection (locked state)
  cpResponse = "captive portal page";
  notification = observerPromise("captive-portal-login");
  cps.recheckCaptivePortal();
  await notification;
  Assert.equal(
    cps.state,
    Ci.nsICaptivePortalService.LOCKED_PORTAL,
    "Captive portal should be in LOCKED_PORTAL state"
  );

  // Set prompt to deny localhost access
  Services.prefs.setBoolPref(
    "network.loopback-network.prompt.testing.allow",
    false
  );

  // Create a separate localhost server (without private override)
  // This will be treated as Local address space, not Private
  let localhostServer = new HttpServer();
  localhostServer.registerPathHandler("/test", lnaHandler);
  localhostServer.start(-1);

  let localhostURL =
    "http://localhost:" + localhostServer.identity.primaryPort + "/test";

  let chan = makeChannel(localhostURL);
  chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;

  await new Promise(resolve => {
    chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
  });

  Assert.equal(
    chan.status,
    Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
    "Localhost access should be blocked even when captive portal is active"
  );

  // Cleanup
  await new Promise(resolve => {
    localhostServer.stop(resolve);
  });

  // Unlock the captive portal
  cpResponse = SUCCESS_STRING;
  notification = observerPromise("captive-portal-login-success");
  cps.recheckCaptivePortal();
  await notification;
  Assert.equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL);

  Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
});

add_task(
  async function test_localnetwork_blocked_after_captive_portal_unlocked() {
    Services.prefs.setCharPref(
      "network.lna.address_space.private.override",
      "127.0.0.1:" + lnaServer.identity.primaryPort
    );

    Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false);
    Services.prefs.setBoolPref(
      "network.localnetwork.prompt.testing.allow",
      false
    );

    Assert.equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN);

    let chan = makeChannel(LNA_URL);
    chan.loadInfo.parentIpAddressSpace = Ci.nsILoadInfo.Public;

    await new Promise(resolve => {
      chan.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE));
    });

    Assert.equal(
      chan.status,
      Cr.NS_ERROR_LOCAL_NETWORK_ACCESS_DENIED,
      "Request should be blocked again when captive portal is no longer active"
    );
    Services.prefs.clearUserPref("network.lna.address_space.private.override");
  }
);
