/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

class CookieValidatedObserver {
  #promise;

  static waitForCookieValidation() {
    return new Promise(resolve => {
      new CookieValidatedObserver(resolve);
    });
  }

  constructor(promise) {
    this.#promise = promise;

    this.obs = Services.obs;
    this.obs.addObserver(this, "cookies-validated");
  }

  observe(subject, topic) {
    if (topic == "cookies-validated") {
      if (this.obs) {
        this.obs.removeObserver(this, "cookies-validated");
      }

      this.#promise();
    }
  }
}

add_task(async function test_invalid_cookie_fix() {
  let promise = CookieValidatedObserver.waitForCookieValidation();

  // Set up a profile.
  let profile = do_get_profile();

  // Start the cookieservice, to force creation of a database.
  Services.cookies.sessionCookies;

  // Each cookie-db opening will trigger a validation.
  await promise;

  // Close the profile.
  await promise_close_profile();

  // Remove the cookie file in order to create another database file.
  do_get_cookie_file(profile).remove(false);

  // Create a schema 16 database.
  let schema16db = new CookieDatabaseConnection(
    do_get_cookie_file(profile),
    17
  );

  const nowInMSec = Date.now();
  const farFarInThePastInMSec = nowInMSec - 60 * 60 * 24 * 1000 * 1000;
  const farFarInTheFutureInMSec = nowInMSec + 60 * 60 * 24 * 1000 * 1000;
  const nearFutureInMSec = nowInMSec + 60 * 60 * 24 * 1000;
  const nowInUSec = nowInMSec * 1000;
  const farFarInThePastInUSec = farFarInThePastInMSec * 1000;

  // CookieValidation.result => eRejectedNoneRequiresSecure
  schema16db.insertCookie(
    new Cookie(
      "test1",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_NONE,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eOK
  schema16db.insertCookie(
    new Cookie(
      "test2",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_LAX,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eOK
  schema16db.insertCookie(
    new Cookie(
      "test3",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_STRICT,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eOK
  schema16db.insertCookie(
    new Cookie(
      "test4",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eRejectedNoneRequiresSecure
  schema16db.insertCookie(
    new Cookie(
      "test5",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      true,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_NONE,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eRejectedAttributeExpiryOversize
  schema16db.insertCookie(
    new Cookie(
      "test6",
      "Some data",
      "foo.com",
      "/",
      farFarInTheFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eRejectedEmptyNameAndValue
  schema16db.insertCookie(
    new Cookie(
      "",
      "",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eRejectedInvalidCharName
  schema16db.insertCookie(
    new Cookie(
      " test8",
      "",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eRejectedInvalidCharValue
  schema16db.insertCookie(
    new Cookie(
      "test9",
      " test9",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      nowInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  // CookieValidation.result => eOK
  schema16db.insertCookie(
    new Cookie(
      "testA",
      "Some data",
      "foo.com",
      "/",
      nearFutureInMSec,
      nowInUSec,
      farFarInThePastInUSec,
      false,
      false,
      false,
      false,
      {},
      Ci.nsICookie.SAMESITE_UNSET,
      Ci.nsICookie.SCHEME_UNSET,
      nowInUSec
    )
  );

  schema16db.close();
  schema16db = null;

  // Check if we have the right entries
  {
    const dbConnection = Services.storage.openDatabase(
      do_get_cookie_file(profile)
    );
    const stmt = dbConnection.createStatement(
      "SELECT name, sameSite, isSecure, creationTime, expiry FROM moz_cookies ORDER BY name"
    );

    const results = [];
    while (stmt.executeStep()) {
      results.push({
        name: stmt.getString(0),
        sameSite: stmt.getInt32(1),
        isSecure: stmt.getInt32(2),
        creationTime: stmt.getInt64(3),
        expiry: stmt.getInt64(4),
      });
    }

    Assert.deepEqual(results, [
      {
        name: "",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: " test8",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test1",
        sameSite: Ci.nsICookie.SAMESITE_NONE,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test2",
        sameSite: Ci.nsICookie.SAMESITE_LAX,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test3",
        sameSite: Ci.nsICookie.SAMESITE_STRICT,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test4",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test5",
        sameSite: Ci.nsICookie.SAMESITE_NONE,
        isSecure: 1,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test6",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: farFarInTheFutureInMSec,
      },
      {
        name: "test9",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "testA",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: farFarInThePastInUSec,
        expiry: nearFutureInMSec,
      },
    ]);

    stmt.finalize();
    dbConnection.close();
  }

  promise = CookieValidatedObserver.waitForCookieValidation();

  // Reset the lastEpoch so validation runs again on the next profile load.
  Services.prefs.clearUserPref("network.cookie.validation.lastEpoch");

  // Reload profile.
  await promise_load_profile();

  await promise;

  // Assert inserted cookies are in the db and correctly handled by services.
  Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 7);

  // Close the profile.
  await promise_close_profile();

  // Check if the sameSite issues were fixed
  {
    const dbConnection = Services.storage.openDatabase(
      do_get_cookie_file(profile)
    );
    const stmt = dbConnection.createStatement(
      "SELECT name, sameSite, isSecure, creationTime, expiry FROM moz_cookies ORDER BY name"
    );

    const results = [];
    while (stmt.executeStep()) {
      results.push({
        name: stmt.getString(0),
        sameSite: stmt.getInt32(1),
        isSecure: stmt.getInt32(2),
        creationTime: stmt.getInt64(3),
        expiry: stmt.getInt64(4),
      });
    }

    Assert.deepEqual(results, [
      {
        name: "test1",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test2",
        sameSite: Ci.nsICookie.SAMESITE_LAX,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test3",
        sameSite: Ci.nsICookie.SAMESITE_STRICT,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test4",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test5",
        sameSite: Ci.nsICookie.SAMESITE_NONE,
        isSecure: 1,
        creationTime: nowInUSec,
        expiry: nearFutureInMSec,
      },
      {
        name: "test6",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: nowInUSec,
        expiry: results.find(a => a.name === "test6").expiry,
      },
      {
        name: "testA",
        sameSite: Ci.nsICookie.SAMESITE_UNSET,
        isSecure: 0,
        creationTime: farFarInThePastInUSec,
        expiry: nearFutureInMSec,
      },
    ]);

    for (const r of results) {
      Assert.less(r.expiry, farFarInTheFutureInMSec);
    }

    stmt.finalize();
    dbConnection.close();
  }

  // Cleanup
  await promise_load_profile();
  Services.cookies.removeAll();
  do_close_profile();
});
