/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
 * 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/. */

/**
 * Bug 499990 - Locale-aware collation
 *
 * Tests our custom, locale-aware collating sequences.
 */

// The name of the file containing the strings we'll sort during this test.
// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and
// and intl/locale/tests/sort/us-ascii_sort.txt.
const DATA_BASENAME = "locale_collation.txt";

// The test data from DATA_BASENAME is read into this array.
var gStrings;

// A connection to our in-memory UTF-16-encoded database.
var gUtf16Conn;

// Helper Functions

/**
 * Since we create a UTF-16 database we have to clean it up, in addition to
 * the normal cleanup of Storage tests.
 */
function cleanupLocaleTests() {
  print("-- Cleaning up test_locale_collation.js suite.");
  gUtf16Conn.close();
  cleanup();
}

/**
 * Creates a test database similar to the default one created in
 * head_storage.js, except that this one uses UTF-16 encoding.
 *
 * @return A connection to the database.
 */
function createUtf16Database() {
  print("Creating the in-memory UTF-16-encoded database.");
  let conn = Services.storage.openSpecialDatabase("memory");
  conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'");

  print("Make sure the encoding was set correctly and is now UTF-16.");
  let stmt = conn.createStatement("PRAGMA encoding");
  Assert.ok(stmt.executeStep());
  let enc = stmt.getString(0);
  stmt.finalize();

  // The value returned will actually be UTF-16le or UTF-16be.
  Assert.ok(enc === "UTF-16le" || enc === "UTF-16be");

  return conn;
}

/**
 * Compares aActual to aExpected, ensuring that the numbers and orderings of
 * the two arrays' elements are the same.
 *
 * @param aActual
 *        An array of strings retrieved from the database.
 * @param aExpected
 *        An array of strings to which aActual should be equivalent.
 */
function ensureResultsAreCorrect(aActual, aExpected) {
  print("Actual results:   " + aActual);
  print("Expected results: " + aExpected);

  Assert.equal(aActual.length, aExpected.length);
  for (let i = 0; i < aActual.length; i++) {
    Assert.equal(aActual[i], aExpected[i]);
  }
}

/**
 * Synchronously SELECTs all rows from the test table of the given database
 * using the given collation.
 *
 * @param  aCollation
 *         The name of one of our custom locale collations.  The rows are
 *         ordered by this collation.
 * @param  aConn
 *         A connection to either the UTF-8 database or the UTF-16 database.
 * @return The resulting strings in an array.
 */
function getResults(aCollation, aConn) {
  let results = [];
  let stmt = aConn.createStatement(
    "SELECT t FROM test ORDER BY t COLLATE " + aCollation + " ASC"
  );
  while (stmt.executeStep()) {
    results.push(stmt.row.t);
  }
  stmt.finalize();
  return results;
}

/**
 * Inserts strings into our test table of the given database in the order given.
 *
 * @param aStrings
 *        An array of strings.
 * @param aConn
 *        A connection to either the UTF-8 database or the UTF-16 database.
 */
function initTableWithStrings(aStrings, aConn) {
  print("Initializing test table.");

  aConn.executeSimpleSQL("DROP TABLE IF EXISTS test");
  aConn.createTable("test", "t TEXT");
  let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)");
  aStrings.forEach(function (str) {
    stmt.params.t = str;
    stmt.execute();
    stmt.reset();
  });
  stmt.finalize();
}

/**
 * Returns a sorting function suitable for passing to Array.prototype.sort().
 * The returned function uses the application's locale to compare strings.
 *
 * @param  aCollation
 *         The name of one of our custom locale collations.  The sorting
 *         strength is computed from this value.
 * @return A function to use as a sorting callback.
 */
function localeCompare(aCollation) {
  let sensitivity;

  switch (aCollation) {
    case "locale":
      sensitivity = "base";
      break;
    case "locale_case_sensitive":
      sensitivity = "case";
      break;
    case "locale_accent_sensitive":
      sensitivity = "accent";
      break;
    case "locale_case_accent_sensitive":
      sensitivity = "variant";
      break;
    default:
      do_throw("Error in test: unknown collation '" + aCollation + "'");
      break;
  }
  const collation = new Intl.Collator("en", { sensitivity });
  return function (aStr1, aStr2) {
    return collation.compare(aStr1, aStr2);
  };
}

/**
 * Reads in the test data from the file DATA_BASENAME and returns it as an array
 * of strings.
 *
 * @return The test data as an array of strings.
 */
function readTestData() {
  print("Reading in test data.");

  let file = do_get_file(DATA_BASENAME);

  let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
    Ci.nsIFileInputStream
  );
  istream.init(file, -1, -1, 0);
  istream.QueryInterface(Ci.nsILineInputStream);

  let line = {};
  let lines = [];
  while (istream.readLine(line)) {
    lines.push(line.value);
  }
  istream.close();

  return lines;
}

/**
 * Gets the results from the given database using the given collation and
 * ensures that they match gStrings sorted by the same collation.
 *
 * @param aCollation
 *        The name of one of our custom locale collations.  The rows from the
 *        database and the expected results are ordered by this collation.
 * @param aConn
 *        A connection to either the UTF-8 database or the UTF-16 database.
 */
function runTest(aCollation, aConn) {
  ensureResultsAreCorrect(
    getResults(aCollation, aConn),
    gStrings.slice(0).sort(localeCompare(aCollation))
  );
}

/**
 * Gets the results from the UTF-8 database using the given collation and
 * ensures that they match gStrings sorted by the same collation.
 *
 * @param aCollation
 *        The name of one of our custom locale collations.  The rows from the
 *        database and the expected results are ordered by this collation.
 */
function runUtf8Test(aCollation) {
  runTest(aCollation, getOpenedDatabase());
}

/**
 * Gets the results from the UTF-16 database using the given collation and
 * ensures that they match gStrings sorted by the same collation.
 *
 * @param aCollation
 *        The name of one of our custom locale collations.  The rows from the
 *        database and the expected results are ordered by this collation.
 */
function runUtf16Test(aCollation) {
  runTest(aCollation, gUtf16Conn);
}

/**
 * Sets up the test suite.
 */
function setup() {
  print("-- Setting up the test_locale_collation.js suite.");

  gStrings = readTestData();

  initTableWithStrings(gStrings, getOpenedDatabase());

  gUtf16Conn = createUtf16Database();
  initTableWithStrings(gStrings, gUtf16Conn);
}

// Test Runs

var gTests = [
  {
    desc: "Case and accent sensitive UTF-8",
    run: () => runUtf8Test("locale_case_accent_sensitive"),
  },

  {
    desc: "Case sensitive, accent insensitive UTF-8",
    run: () => runUtf8Test("locale_case_sensitive"),
  },

  {
    desc: "Case insensitive, accent sensitive UTF-8",
    run: () => runUtf8Test("locale_accent_sensitive"),
  },

  {
    desc: "Case and accent insensitive UTF-8",
    run: () => runUtf8Test("locale"),
  },

  {
    desc: "Case and accent sensitive UTF-16",
    run: () => runUtf16Test("locale_case_accent_sensitive"),
  },

  {
    desc: "Case sensitive, accent insensitive UTF-16",
    run: () => runUtf16Test("locale_case_sensitive"),
  },

  {
    desc: "Case insensitive, accent sensitive UTF-16",
    run: () => runUtf16Test("locale_accent_sensitive"),
  },

  {
    desc: "Case and accent insensitive UTF-16",
    run: () => runUtf16Test("locale"),
  },
];

function run_test() {
  setup();
  gTests.forEach(function (test) {
    print("-- Running test: " + test.desc);
    test.run();
  });
  cleanupLocaleTests();
}
