/* 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/. */

// Regression test for the alpnChanged=false path in ZeroRttHandle::Finish0RTT.
//
// When H1 0-RTT early data is rejected by the server (NSS reports
// earlyDataAccepted=false) but the ALPN is unchanged (H1→H1), the H1
// connection is still fully usable.  ZeroRttHandle::Finish0RTT must NOT
// close the HET; it must fall through to InvokeCallback(NS_OK) so HE
// declares the winner and the real transaction is adopted onto the live
// connection.
//
// Observable: the second request has resumed=true — the TLS session was
// resumed via PSK even though early data was rejected.  If the fix were
// absent (alpnChanged check missing), the PSK token would be evicted and
// the retry would open a fresh connection with resumed=false.
//
// ZeroRttAcceptServer's "0rtt-reject-h1.example.com" host omits the NSS
// anti-replay context so early data is always rejected without closing the
// connection.

"use strict";

const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);
const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);
var { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

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

const kHost = "0rtt-reject-h1.example.com";
const kPort = 8443;

let callbackServer;
let gServerStarted = false;

add_setup(
  {
    skip_if: () => AppConstants.MOZ_SYSTEM_NSS,
  },
  async () => {
    callbackServer = new HttpServer();
    callbackServer.registerPrefixHandler("/callback/", () => {});
    callbackServer.start(-1);

    Services.env.set(
      "MOZ_ZERORTT_ACCEPT_CALLBACK_PORT",
      callbackServer.identity.primaryPort
    );
    Services.env.set("MOZ_TLS_SERVER_0RTT", "1");
    const started = await asyncStartTLSTestServer(
      "ZeroRttAcceptServer",
      "../../../security/manager/ssl/tests/unit/test_faulty_server"
    );
    if (!started) {
      return;
    }
    gServerStarted = true;

    Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
    Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
    Services.prefs.setBoolPref("network.ssl_tokens_cache_enabled", true);
    Services.prefs.setBoolPref("network.http.http3.enable", false);
    // Single address to keep the HE race to one HCA.
    Services.prefs.setBoolPref("network.dns.disableIPv6", true);

    override.addIPOverride(kHost, "127.0.0.1");

    registerCleanupFunction(async () => {
      Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
      Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
      Services.prefs.clearUserPref("network.ssl_tokens_cache_enabled");
      Services.prefs.clearUserPref("network.http.http3.enable");
      Services.prefs.clearUserPref("network.dns.disableIPv6");
      override.clearOverrides();
      if (callbackServer) {
        await callbackServer.stop();
        callbackServer = null;
      }
    });
  }
);

function fetchResult(url) {
  return new Promise(resolve => {
    let chan = NetUtil.newChannel({
      uri: url,
      loadUsingSystemPrincipal: true,
    }).QueryInterface(Ci.nsIHttpChannel);
    chan.loadFlags =
      Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI | Ci.nsIRequest.LOAD_BYPASS_CACHE;
    chan.asyncOpen({
      onStartRequest() {},
      onDataAvailable(_req, stream, _offset, count) {
        read_stream(stream, count);
      },
      onStopRequest(req, status) {
        let resumed = false;
        try {
          resumed = req.securityInfo.resumed;
        } catch (_e) {}
        resolve({
          ok: Components.isSuccessCode(status),
          status: Components.isSuccessCode(status)
            ? req.QueryInterface(Ci.nsIHttpChannel).responseStatus
            : 0,
          resumed,
        });
      },
      QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
    });
  });
}

add_task(
  {
    skip_if: () =>
      AppConstants.MOZ_SYSTEM_NSS ||
      !gServerStarted ||
      mozinfo.os == "android" ||
      mozinfo.socketprocess_networking,
  },
  async function test_he_h1_0rtt_early_data_rejected_reuses_connection() {
    const url = `https://${kHost}:${kPort}/`;

    let nss = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
    await nss.asyncClearSSLExternalAndInternalSessionCache();

    // ── Warm-up: full H1 handshake, PSK ticket written to SSLTokensCache ──
    const wu = await fetchResult(url);
    Assert.ok(wu.ok, "warm-up must succeed");
    Assert.equal(wu.status, 200, "warm-up must return 200");
    Assert.equal(wu.resumed, false, "warm-up must be a fresh handshake");

    // Give NSS time to persist the session ticket, then clear connections
    // so the next request opens a new one (triggering 0-RTT).
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 500));
    Services.obs.notifyObservers(null, "net:cancel-all-connections");
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(r => setTimeout(r, 200));

    // ── Test: H1 0-RTT, early data rejected (alpnChanged=false) ──────────
    // The server omits SSL_SetAntiReplayContext so NSS rejects early data,
    // but the ALPN stays H1.  ZeroRttHandle::Finish0RTT(restart=1,
    // alpnChanged=0) must fall through to InvokeCallback(NS_OK): the H1
    // connection is still alive and the real transaction is retried on it.
    //
    // resumed=true confirms the PSK ticket was used (TLS session resumed)
    // and the connection was not discarded.  Without the alpnChanged guard,
    // the HET would be closed, the token evicted, and the retry would open
    // a fresh connection with resumed=false.
    const r = await fetchResult(url);
    Assert.ok(r.ok, "request must succeed after 0-RTT early-data rejection");
    Assert.equal(r.status, 200, "request must return 200");
    Assert.equal(
      r.resumed,
      true,
      "PSK must have been used — connection reused after early-data rejection"
    );

    Services.obs.notifyObservers(null, "net:cancel-all-connections");
  }
);
