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

MARIONETTE_TIMEOUT = 40000;

SpecialPowers.addPermission("sms", true, document);
SpecialPowers.setBoolPref("dom.sms.enabled", true);

let manager = window.navigator.mozMobileMessage;
ok(manager instanceof MozMobileMessageManager,
   "manager is instance of " + manager.constructor);

let pendingEmulatorCmdCount = 0;
function sendSmsToEmulator(from, text, callback) {
  ++pendingEmulatorCmdCount;

  let cmd = "sms send " + from + " " + text;
  runEmulatorCmd(cmd, function (result) {
    --pendingEmulatorCmdCount;

    callback(result[0] == "OK");
  });
}

let tasks = {
  // List of test fuctions. Each of them should call |tasks.next()| when
  // completed or |tasks.finish()| to jump to the last one.
  _tasks: [],
  _nextTaskIndex: 0,

  push: function push(func) {
    this._tasks.push(func);
  },

  next: function next() {
    let index = this._nextTaskIndex++;
    let task = this._tasks[index];
    try {
      task.apply(null, Array.slice(arguments));
    } catch (ex) {
      ok(false, "test task[" + index + "] throws: " + ex);
      // Run last task as clean up if possible.
      if (index != this._tasks.length - 1) {
        this.finish();
      }
    }
  },

  finish: function finish() {
    this._tasks[this._tasks.length - 1]();
  },

  run: function run() {
    this.next();
  }
};

function getAllMessages(callback, filter, reverse) {
  if (!filter) {
    filter = new MozSmsFilter;
  }
  let messages = [];
  let request = manager.getMessages(filter, reverse || false);
  request.onsuccess = function(event) {
    if (!request.done) {
      messages.push(request.result);
      request.continue();
      return;
    }

    window.setTimeout(callback.bind(null, messages), 0);
  }
}

function deleteAllMessages() {
  getAllMessages(function deleteAll(messages) {
    let message = messages.shift();
    if (!message) {
      ok(true, "all messages deleted");
      tasks.next();
      return;
    }

    let request = manager.delete(message.id);
    request.onsuccess = deleteAll.bind(null, messages);
    request.onerror = function (event) {
      ok(false, "failed to delete all messages");
      tasks.finish();
    }
  });
}

function sendMessage(to, body) {
  manager.onsent = function () {
    manager.onsent = null;
    tasks.next();
  };
  let request = manager.send(to, body);
  request.onerror = tasks.finish.bind(tasks);
}

function receiveMessage(from, body) {
  manager.onreceived = function () {
    manager.onreceived = null;
    tasks.next();
  };
  sendSmsToEmulator(from, body, function (success) {
    if (!success) {
      tasks.finish();
    }
  });
}

function getAllThreads(callback) {
  let threads = [];

  let cursor = manager.getThreads();
  ok(cursor instanceof DOMCursor,
     "cursor is instanceof " + cursor.constructor);

  cursor.onsuccess = function (event) {
    if (!cursor.done) {
      threads.push(cursor.result);
      cursor.continue();
      return;
    }

    window.setTimeout(callback.bind(null, threads), 0);
  };
}

function checkThread(bodies, lastBody, unreadCount, participants,
                     thread, callback) {
  log("Validating MozMobileMessageThread attributes " +
      JSON.stringify([bodies, lastBody, unreadCount, participants]));

  ok(thread, "current thread should be valid.");

  ok(thread.id, "thread id", "thread.id");
  log("Got thread " + thread.id);

  if (lastBody != null) {
    is(thread.body, lastBody, "thread.body");
  }

  is(thread.unreadCount, unreadCount, "thread.unreadCount");

  ok(Array.isArray(thread.participants), "thread.participants is array");
  is(thread.participants.length, participants.length,
     "thread.participants.length");
  for (let i = 0; i < participants.length; i++) {
    is(thread.participants[i], participants[i],
       "thread.participants[" + i + "]");
  }

  // Check whether the thread does contain all the messages it supposed to have.
  let filter = new MozSmsFilter;
  filter.threadId = thread.id;
  getAllMessages(function (messages) {
    is(messages.length, bodies.length, "messages.length and bodies.length");

    for (let message of messages) {
      let index = bodies.indexOf(message.body);
      ok(index >= 0, "message.body '" + message.body +
         "' should be found in bodies array.");
      bodies.splice(index, 1);
    }

    is(bodies.length, 0, "bodies array length");

    window.setTimeout(callback, 0);
  }, filter, false);
}

tasks.push(deleteAllMessages);

tasks.push(getAllThreads.bind(null, function (threads) {
  is(threads.length, 0, "Empty thread list at beginning.");
  tasks.next();
}));

// Populate MobileMessageDB with messages.
let checkFuncs = [];

// [Thread 1]
// One message only, body = "thread 1";
// All sent message, unreadCount = 0;
// One participant only, participants = ["5555211001"].
tasks.push(sendMessage.bind(null, "5555211001", "thread 1"));
checkFuncs.push(checkThread.bind(null, ["thread 1"],
                                 "thread 1", 0, ["5555211001"]));

// [Thread 2]
// Two messages, body = "thread 2-2";
// All sent message, unreadCount = 0;
// One participant with two aliased addresses, participants = ["5555211002"].
tasks.push(sendMessage.bind(null, "5555211002",   "thread 2-1"));
tasks.push(sendMessage.bind(null, "+15555211002", "thread 2-2"));
checkFuncs.push(checkThread.bind(null, ["thread 2-1", "thread 2-2"],
                                 "thread 2-2", 0, ["5555211002"]));

// [Thread 3]
// Two messages, body = "thread 3-2";
// All sent message, unreadCount = 0;
// One participant with two aliased addresses, participants = ["+15555211003"].
tasks.push(sendMessage.bind(null, "+15555211003", "thread 3-1"));
tasks.push(sendMessage.bind(null, "5555211003",   "thread 3-2"));
checkFuncs.push(checkThread.bind(null, ["thread 3-1", "thread 3-2"],
                                 "thread 3-2", 0, ["+15555211003"]));

// [Thread 4]
// One message only, body = "thread 4";
// All received message, unreadCount = 1;
// One participant only, participants = ["5555211004"].
tasks.push(receiveMessage.bind(null, "5555211004", "thread 4"));
checkFuncs.push(checkThread.bind(null, ["thread 4"],
                                 "thread 4", 1, ["5555211004"]));

// [Thread 5]
//
// Thread body should be set to text body of the last message in this
// thread. However due to BUG 840051, we're having SMSC time as message
// timestamp and SMSC time resolution is 1 second. So it's likely that the two
// messages have the same timestamp and we just can't tell which is the later
// one.
//
// All received message, unreadCount = 2;
// One participant with two aliased addresses, participants = ["5555211005"].
tasks.push(receiveMessage.bind(null, "5555211005",   "thread 5-1"));
tasks.push(receiveMessage.bind(null, "+15555211005", "thread 5-2"));
checkFuncs.push(checkThread.bind(null, ["thread 5-1", "thread 5-2"],
                                 null, 2, ["5555211005"]));

// [Thread 6]
//
// Thread body should be set to text body of the last message in this
// thread. However due to BUG 840051, we're having SMSC time as message
// timestamp and SMSC time resolution is 1 second. So it's likely that the two
// messages have the same timestamp and we just can't tell which is the later
// one.
//
// All received message, unreadCount = 2;
// One participant with two aliased addresses, participants = ["+15555211006"].
tasks.push(receiveMessage.bind(null, "+15555211006", "thread 6-1"));
tasks.push(receiveMessage.bind(null, "5555211006",   "thread 6-2"));
checkFuncs.push(checkThread.bind(null, ["thread 6-1", "thread 6-2"],
                                 null, 2, ["+15555211006"]));

// [Thread 7]
//
// Thread body should be set to text body of the last message in this
// thread. However due to BUG 840051, there might be time difference between
// SMSC and device time. So the result of comparing the timestamps of sent and
// received message may not follow the order of requests and may result in
// UNEXPECTED-FAIL in following tests.
//
// Two received message, unreadCount = 2;
// One participant with two aliased addresses, participants = ["5555211007"].
tasks.push(sendMessage.bind(null,    "5555211007",   "thread 7-1"));
tasks.push(sendMessage.bind(null,    "+15555211007", "thread 7-2"));
tasks.push(receiveMessage.bind(null, "5555211007",   "thread 7-3"));
tasks.push(receiveMessage.bind(null, "+15555211007", "thread 7-4"));
checkFuncs.push(checkThread.bind(null, ["thread 7-1", "thread 7-2",
                                        "thread 7-3", "thread 7-4"],
                                 null, 2, ["5555211007"]));

// [Thread 8]
//
// Thread body should be set to text body of the last message in this
// thread. However due to BUG 840051, there might be time difference between
// SMSC and device time. So the result of comparing the timestamps of sent and
// received message may not follow the order of requests and may result in
// UNEXPECTED-FAIL in following tests.
//
// Two received message, unreadCount = 2;
// One participant with two aliased addresses, participants = ["5555211008"].
tasks.push(receiveMessage.bind(null, "5555211008",   "thread 8-1"));
tasks.push(receiveMessage.bind(null, "+15555211008", "thread 8-2"));
tasks.push(sendMessage.bind(null,    "5555211008",   "thread 8-3"));
tasks.push(sendMessage.bind(null,    "+15555211008", "thread 8-4"));
checkFuncs.push(checkThread.bind(null, ["thread 8-1", "thread 8-2",
                                        "thread 8-3", "thread 8-4"],
                                 null, 2, ["5555211008"]));

// [Thread 9]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["+15555211009"].
tasks.push(sendMessage.bind(null, "+15555211009",  "thread 9-1"));
tasks.push(sendMessage.bind(null, "01115555211009", "thread 9-2"));
tasks.push(sendMessage.bind(null, "5555211009",    "thread 9-3"));
checkFuncs.push(checkThread.bind(null, ["thread 9-1", "thread 9-2",
                                        "thread 9-3"],
                                 "thread 9-3", 0, ["+15555211009"]));

// [Thread 10]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["+15555211010"].
tasks.push(sendMessage.bind(null, "+15555211010",  "thread 10-1"));
tasks.push(sendMessage.bind(null, "5555211010",    "thread 10-2"));
tasks.push(sendMessage.bind(null, "01115555211010", "thread 10-3"));
checkFuncs.push(checkThread.bind(null, ["thread 10-1", "thread 10-2",
                                        "thread 10-3"],
                                 "thread 10-3", 0, ["+15555211010"]));

// [Thread 11]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["01115555211011"].
tasks.push(sendMessage.bind(null, "01115555211011", "thread 11-1"));
tasks.push(sendMessage.bind(null, "5555211011",    "thread 11-2"));
tasks.push(sendMessage.bind(null, "+15555211011",  "thread 11-3"));
checkFuncs.push(checkThread.bind(null, ["thread 11-1", "thread 11-2",
                                        "thread 11-3"],
                                 "thread 11-3", 0, ["01115555211011"]));

// [Thread 12]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["01115555211012"].
tasks.push(sendMessage.bind(null, "01115555211012", "thread 12-1"));
tasks.push(sendMessage.bind(null, "+15555211012",  "thread 12-2"));
tasks.push(sendMessage.bind(null, "5555211012",    "thread 12-3"));
checkFuncs.push(checkThread.bind(null, ["thread 12-1", "thread 12-2",
                                        "thread 12-3"],
                                 "thread 12-3", 0, ["01115555211012"]));

// [Thread 13]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["5555211013"].
tasks.push(sendMessage.bind(null, "5555211013",    "thread 13-1"));
tasks.push(sendMessage.bind(null, "+15555211013",  "thread 13-2"));
tasks.push(sendMessage.bind(null, "01115555211013", "thread 13-3"));
checkFuncs.push(checkThread.bind(null, ["thread 13-1", "thread 13-2",
                                        "thread 13-3"],
                                 "thread 13-3", 0, ["5555211013"]));

// [Thread 14]
// Three sent message, unreadCount = 0;
// One participant with three aliased addresses, participants = ["5555211014"].
tasks.push(sendMessage.bind(null, "5555211014",     "thread 14-1"));
tasks.push(sendMessage.bind(null, "01115555211014", "thread 14-2"));
tasks.push(sendMessage.bind(null, "+15555211014",   "thread 14-3"));
checkFuncs.push(checkThread.bind(null, ["thread 14-1", "thread 14-2",
                                        "thread 14-3"],
                                 "thread 14-3", 0, ["5555211014"]));

//[Thread 15]
//Three sent message, unreadCount = 0;
//One participant but might be merged to 555211015, participants = ["5555211015"].
tasks.push(sendMessage.bind(null, "5555211015", "thread 15-1"));
checkFuncs.push(checkThread.bind(null, ["thread 15-1"],
                                 "thread 15-1", 0, ["5555211015"]));

//[Thread 16]
//Three sent message, unreadCount = 0;
//One participant but might be merged to 5555211015, participants = ["555211015"].
tasks.push(sendMessage.bind(null, "555211015", "thread 16-1"));
checkFuncs.push(checkThread.bind(null, ["thread 16-1"],
                                 "thread 16-1", 0, ["555211015"]));

//[Thread 17]
//Three sent message, unreadCount = 0;
//One participant with two aliased addresses, participants = ["555211017"].
tasks.push(sendMessage.bind(null, "+5511555211017", "thread 17-1"));
tasks.push(sendMessage.bind(null, "555211017",      "thread 17-2"));
checkFuncs.push(checkThread.bind(null, ["thread 17-1", "thread 17-2"],
                                 "thread 17-2", 0, ["+5511555211017"]));

//[Thread 18]
//Three sent message, unreadCount = 0;
//One participant with two aliased addresses, participants = ["555211018"].
tasks.push(sendMessage.bind(null, "555211018",      "thread 18-1"));
tasks.push(sendMessage.bind(null, "+5511555211018", "thread 18-2"));
checkFuncs.push(checkThread.bind(null, ["thread 18-1", "thread 18-2"],
                                 "thread 18-2", 0, ["555211018"]));

// Check threads.
tasks.push(getAllThreads.bind(null, function (threads) {
  is(threads.length, checkFuncs.length, "number of threads got");

  // Reverse threads as we iterate over them in reverse order
  threads.reverse();

  (function callback() {
    if (!threads.length) {
      tasks.next();
      return;
    }

    checkFuncs.shift()(threads.shift(), callback);
  })();
}));

tasks.push(deleteAllMessages);

tasks.push(getAllThreads.bind(null, function (threads) {
  is(threads.length, 0, "Empty thread list at the end.");
  tasks.next();
}));

// WARNING: All tasks should be pushed before this!!!
tasks.push(function cleanUp() {
  if (pendingEmulatorCmdCount) {
    window.setTimeout(cleanUp, 100);
    return;
  }

  SpecialPowers.removePermission("sms", document);
  SpecialPowers.clearUserPref("dom.sms.enabled");
  finish();
});

tasks.run();
