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

// Test for MerinoClient sessions.

"use strict";

const { MerinoClient } = ChromeUtils.importESModule(
  "resource:///modules/MerinoClient.sys.mjs"
);

const { SEARCH_PARAMS } = MerinoClient;

let gClient;

add_task(async function init() {
  gClient = new MerinoClient();
  await MerinoTestUtils.server.start();
});

// In a single session, all requests should use the same session ID and the
// sequence number should be incremented.
add_task(async function singleSession() {
  for (let i = 0; i < 3; i++) {
    let query = "search" + i;
    await gClient.fetch({ query });

    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// Different sessions should use different session IDs and the sequence number
// should be reset.
add_task(async function manySessions() {
  for (let i = 0; i < 3; i++) {
    let query = "search" + i;
    await gClient.fetch({ query });

    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
        },
      },
    ]);

    gClient.resetSession();
  }
});

// Tests two consecutive fetches:
//
// 1. Start a fetch
// 2. Wait for the mock Merino server to receive the request
// 3. Start a second fetch before the client receives the response
//
// The first fetch will be canceled by the second but the sequence number in the
// second fetch should still be incremented.
add_task(async function twoFetches_wait() {
  for (let i = 0; i < 3; i++) {
    // Send the first response after a delay to make sure the client will not
    // receive it before we start the second fetch.
    MerinoTestUtils.server.response.delay = UrlbarPrefs.get("merino.timeoutMs");

    // Start the first fetch but don't wait for it to finish.
    let requestPromise = MerinoTestUtils.server.waitForNextRequest();
    let query1 = "search" + i;
    gClient.fetch({ query: query1 });

    // Wait until the first request is received before starting the second
    // fetch, which will cancel the first. The response doesn't need to be
    // delayed, so remove it to make the test run faster.
    await requestPromise;
    delete MerinoTestUtils.server.response.delay;
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    // The sequence number should have been incremented for each fetch.
    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query1,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
        },
      },
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// Tests two consecutive fetches:
//
// 1. Start a fetch
// 2. Immediately start a second fetch
//
// The first fetch will be canceled by the second but the sequence number in the
// second fetch should still be incremented.
add_task(async function twoFetches_immediate() {
  for (let i = 0; i < 3; i++) {
    // Send the first response after a delay to make sure the client will not
    // receive it before we start the second fetch.
    MerinoTestUtils.server.response.delay =
      100 * UrlbarPrefs.get("merino.timeoutMs");

    // Start the first fetch but don't wait for it to finish.
    let query1 = "search" + i;
    gClient.fetch({ query: query1 });

    // Immediately do a second fetch that cancels the first. The response
    // doesn't need to be delayed, so remove it to make the test run faster.
    delete MerinoTestUtils.server.response.delay;
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    // The sequence number should have been incremented for each fetch, but the
    // first won't have reached the server since it was immediately canceled.
    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// When a network error occurs, the sequence number should still be incremented.
add_task(async function networkError() {
  for (let i = 0; i < 3; i++) {
    // Do a fetch that fails with a network error.
    let query1 = "search" + i;
    await MerinoTestUtils.server.withNetworkError(async () => {
      await gClient.fetch({ query: query1 });
    });

    Assert.equal(
      gClient.lastFetchStatus,
      "network_error",
      "The request failed with a network error"
    );

    // Do another fetch that successfully finishes.
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    Assert.equal(
      gClient.lastFetchStatus,
      "success",
      "The request completed successfully"
    );

    // Only the second request should have been received but the sequence number
    // should have been incremented for each.
    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// When the server returns a response with an HTTP error, the sequence number
// should be incremented.
add_task(async function httpError() {
  for (let i = 0; i < 3; i++) {
    // Do a fetch that fails with an HTTP error.
    MerinoTestUtils.server.response.status = 500;
    let query1 = "search" + i;
    await gClient.fetch({ query: query1 });

    Assert.equal(
      gClient.lastFetchStatus,
      "http_error",
      "The last request failed with a network error"
    );

    // Do another fetch that successfully finishes.
    MerinoTestUtils.server.response.status = 200;
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    Assert.equal(
      gClient.lastFetchStatus,
      "success",
      "The last request completed successfully"
    );

    // Both requests should have been received and the sequence number should
    // have been incremented for each.
    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query1,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
        },
      },
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);

    MerinoTestUtils.server.reset();
  }

  gClient.resetSession();
});

// When the client times out waiting for a response but later receives it and no
// other fetch happens in the meantime, the sequence number should be
// incremented.
add_task(async function clientTimeout_wait() {
  for (let i = 0; i < 3; i++) {
    // Do a fetch that causes the client to time out.
    MerinoTestUtils.server.response.delay =
      2 * UrlbarPrefs.get("merino.timeoutMs");
    let responsePromise = gClient.waitForNextResponse();
    let query1 = "search" + i;
    await gClient.fetch({ query: query1 });

    Assert.equal(
      gClient.lastFetchStatus,
      "timeout",
      "The last request failed with a client timeout"
    );

    // Wait for the client to receive the response.
    await responsePromise;

    // Do another fetch that successfully finishes.
    delete MerinoTestUtils.server.response.delay;
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    Assert.equal(
      gClient.lastFetchStatus,
      "success",
      "The last request completed successfully"
    );

    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query1,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
        },
      },
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// When the client times out waiting for a response and a second fetch starts
// before the response is received, the first fetch should be canceled but the
// sequence number should still be incremented.
add_task(async function clientTimeout_canceled() {
  for (let i = 0; i < 3; i++) {
    // Do a fetch that causes the client to time out.
    MerinoTestUtils.server.response.delay =
      2 * UrlbarPrefs.get("merino.timeoutMs");
    let query1 = "search" + i;
    await gClient.fetch({ query: query1 });

    Assert.equal(
      gClient.lastFetchStatus,
      "timeout",
      "The last request failed with a client timeout"
    );

    // Do another fetch that successfully finishes.
    delete MerinoTestUtils.server.response.delay;
    let query2 = query1 + "again";
    await gClient.fetch({ query: query2 });

    Assert.equal(
      gClient.lastFetchStatus,
      "success",
      "The last request completed successfully"
    );

    MerinoTestUtils.server.checkAndClearRequests([
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query1,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i,
        },
      },
      {
        params: {
          [SEARCH_PARAMS.QUERY]: query2,
          [SEARCH_PARAMS.SEQUENCE_NUMBER]: 2 * i + 1,
        },
      },
    ]);
  }

  gClient.resetSession();
});

// When the session times out, the next fetch should use a new session ID and
// the sequence number should be reset.
add_task(async function sessionTimeout() {
  // Set the session timeout to something reasonable to test.
  let originalTimeoutMs = gClient.sessionTimeoutMs;
  gClient.sessionTimeoutMs = 500;

  // Do a fetch.
  let query1 = "search";
  await gClient.fetch({ query: query1 });

  // Wait for the session to time out.
  await gClient.waitForNextSessionReset();

  Assert.strictEqual(
    gClient.sessionID,
    null,
    "sessionID is null after session timeout"
  );
  Assert.strictEqual(
    gClient.sequenceNumber,
    0,
    "sequenceNumber is zero after session timeout"
  );
  Assert.strictEqual(
    gClient._test_sessionTimer,
    null,
    "sessionTimer is null after session timeout"
  );

  // Do another fetch.
  let query2 = query1 + "again";
  await gClient.fetch({ query: query2 });

  // The second request's sequence number should be zero due to the session
  // timeout.
  MerinoTestUtils.server.checkAndClearRequests([
    {
      params: {
        [SEARCH_PARAMS.QUERY]: query1,
        [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
      },
    },
    {
      params: {
        [SEARCH_PARAMS.QUERY]: query2,
        [SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
      },
    },
  ]);

  Assert.ok(
    gClient.sessionID,
    "sessionID is non-null after first request in a new session"
  );
  Assert.equal(
    gClient.sequenceNumber,
    1,
    "sequenceNumber is one after first request in a new session"
  );
  Assert.ok(
    gClient._test_sessionTimer,
    "sessionTimer is non-null after first request in a new session"
  );

  gClient.sessionTimeoutMs = originalTimeoutMs;
  gClient.resetSession();
});
