/* 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";

var { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

let trrServer;
let h3Port;
let host;

add_setup(async function setup() {
  trr_test_setup();

  h3Port = Services.env.get("MOZHTTP3_PORT_ECH");
  Assert.notEqual(h3Port, null);
  Assert.notEqual(h3Port, "");

  host = `https://alt1.example.com:${h3Port}/`;

  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRFIRST);
});

registerCleanupFunction(async () => {
  trr_clear_prefs();
  if (trrServer) {
    await trrServer.stop();
  }
});

function makeChan(url) {
  let chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
  }).QueryInterface(Ci.nsIHttpChannel);
  return chan;
}

function channelOpenPromise(chan, flags) {
  return new Promise(resolve => {
    function finish(req, buffer) {
      resolve([req, buffer]);
    }
    chan.asyncOpen(new ChannelListener(finish, null, flags));
  });
}

function ActivityObserver() {
  this.activites = [];
}

ActivityObserver.prototype = {
  activites: [],
  observeActivity(
    aHttpChannel,
    aActivityType,
    aActivitySubtype,
    aTimestamp,
    aExtraSizeData,
    aExtraStringData
  ) {
    try {
      aHttpChannel.QueryInterface(Ci.nsINullChannel);
      aHttpChannel.QueryInterface(Ci.nsIChannel);
      if (aHttpChannel.URI.spec === host) {
        dump(
          "*** HTTP Activity 0x" +
            aActivityType.toString(16) +
            " 0x" +
            aActivitySubtype.toString(16) +
            " " +
            aExtraStringData +
            "\n"
        );
        this.activites.push({ host, subType: aActivitySubtype });
      }
    } catch (e) {}
  },
};

function checkHttpActivities(activites, expected) {
  let foundTransClosed = false;
  for (let activity of activites) {
    switch (activity.subType) {
      case Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
        foundTransClosed = true;
        break;
      default:
        break;
    }
  }

  Assert.equal(
    foundTransClosed,
    expected,
    "The activity of speculative transaction should match"
  );
}
// Test steps:
// 1. Create a TRR server that serves the HTTPS record for the H3 server.
// 2. Create a channel and connect to the H3 server.
// 3. Use nsIHttpActivityObserver to observe if we receive the HTTP activity
//    that is sent from the speculative transaction.
async function doTestAltSVCWithHTTPSRR(foundActivity) {
  trrServer = new TRRServer();
  await trrServer.start();

  let observerService = Cc[
    "@mozilla.org/network/http-activity-distributor;1"
  ].getService(Ci.nsIHttpActivityDistributor);

  Services.prefs.setIntPref("network.trr.mode", Ci.nsIDNSService.MODE_TRRONLY);
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://foo.example.com:${trrServer.port()}/dns-query`
  );

  let portPrefixedName = `_${h3Port}._https.alt1.example.com`;
  let vals = [
    { key: "alpn", value: "h3" },
    { key: "port", value: h3Port },
  ];

  await trrServer.registerDoHAnswers(portPrefixedName, "HTTPS", {
    answers: [
      {
        name: portPrefixedName,
        ttl: 55,
        type: "HTTPS",
        flush: false,
        data: {
          priority: 1,
          name: ".",
          values: vals,
        },
      },
    ],
  });

  await trrServer.registerDoHAnswers("alt1.example.com", "A", {
    answers: [
      {
        name: "alt1.example.com",
        ttl: 55,
        type: "A",
        flush: false,
        data: "127.0.0.1",
      },
    ],
  });

  let chan = makeChan(`${host}alt_svc_header`);
  let h3AltSvc = ":" + h3Port;
  chan.setRequestHeader("x-altsvc", h3AltSvc, false);
  let observer = new ActivityObserver();

  let responseObserver = {
    QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
    observe(aSubject, aTopic) {
      let channel = aSubject.QueryInterface(Ci.nsIChannel);
      if (
        aTopic == "http-on-examine-response" &&
        channel.URI.spec === `${host}alt_svc_header`
      ) {
        Services.obs.removeObserver(
          responseObserver,
          "http-on-examine-response"
        );

        observerService.addObserver(observer);
        channel.suspend();
        // We need to close all connections here, otherwise we are not allowed
        // to create a specul connection to validate the Alt-svc header.
        Services.obs.notifyObservers(null, "net:cancel-all-connections");
        // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
        setTimeout(function () {
          channel.resume();
        }, 3000);
      }
    },
  };
  Services.obs.addObserver(responseObserver, "http-on-examine-response");

  await channelOpenPromise(chan);

  // Some dekay here to collect HTTP activites.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 3000));

  checkHttpActivities(observer.activites, foundActivity);

  await trrServer.stop();
  observerService.removeObserver(observer);
}

add_task(async function testAltSVCWithHTTPSRR() {
  Services.prefs.setBoolPref(
    "network.http.skip_alt_svc_validation_on_https_rr",
    false
  );

  await doTestAltSVCWithHTTPSRR(true);

  // Clear the alt-svc mapping.
  Services.obs.notifyObservers(null, "last-pb-context-exited");
  Services.obs.notifyObservers(null, "net:cancel-all-connections");
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 3000));

  Services.prefs.setBoolPref(
    "network.http.skip_alt_svc_validation_on_https_rr",
    true
  );

  await doTestAltSVCWithHTTPSRR(false);
});
