/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const {StorageFront} = require("devtools/server/actors/storage");
let gTests;
let gExpected;
let index = 0;

const beforeReload = {
  cookies: ["test1.example.org", "sectest1.example.org"],
  localStorage: ["http://test1.example.org", "http://sectest1.example.org"],
  sessionStorage: ["http://test1.example.org", "http://sectest1.example.org"],
};

function finishTests(client) {
  // Forcing GC/CC to get rid of docshells and windows created by this test.
  forceCollections();
  client.close(() => {
    forceCollections();
    DebuggerServer.destroy();
    forceCollections();
    gTests = null;
    finish();
  });
}

function markOutMatched(toBeEmptied, data, deleted) {
  if (!Object.keys(toBeEmptied).length) {
    info("Object empty")
    return;
  }
  ok(Object.keys(data).length,
     "Atleast some storage types should be present in deleted");
  for (let storageType in toBeEmptied) {
    if (!data[storageType]) {
      continue;
    }
    info("Testing for " + storageType);
    for (let host in data[storageType]) {
      ok(toBeEmptied[storageType][host], "Host " + host + " found");

      for (let item of data[storageType][host]) {
        let index = toBeEmptied[storageType][host].indexOf(item);
        ok(index > -1, "Item found - " + item);
        if (index > -1) {
          toBeEmptied[storageType][host].splice(index, 1);
        }
      }
      if (!toBeEmptied[storageType][host].length) {
        delete toBeEmptied[storageType][host];
      }
    }
    if (!Object.keys(toBeEmptied[storageType]).length) {
      delete toBeEmptied[storageType];
    }
  }
}

function onStoresCleared(data) {
  if (data.sessionStorage || data.localStorage) {
    let hosts = data.sessionStorage || data.localStorage;
    info("Stores cleared required for session storage");
    is(hosts.length, 1, "number of hosts is 1");
    is(hosts[0], "http://test1.example.org",
       "host matches for " + Object.keys(data)[0]);
    gTests.next();
  }
  else {
    ok(false, "Stores cleared should only be for local and sesion storage");
  }

}

function onStoresUpdate({added, changed, deleted}) {
  info("inside stores update for index " + index);

  // Here, added, changed and deleted might be null even if they are required as
  // per gExpected. This is fine as they might come in the next stores-update
  // call or have already come in the previous one.
  if (added) {
    info("matching added object for index " + index);
    markOutMatched(gExpected.added, added);
  }
  if (changed) {
    info("matching changed object for index " + index);
    markOutMatched(gExpected.changed, changed);
  }
  if (deleted) {
    info("matching deleted object for index " + index);
    markOutMatched(gExpected.deleted, deleted);
  }

  if ((!gExpected.added || !Object.keys(gExpected.added).length) &&
      (!gExpected.changed || !Object.keys(gExpected.changed).length) &&
      (!gExpected.deleted || !Object.keys(gExpected.deleted).length)) {
    info("Everything expected has been received for index " + index);
    index++;
    gTests.next();
  }
  else {
    info("Still some updates pending for index " + index);
  }
}

function* UpdateTests(front, win, client) {
  front.on("stores-update", onStoresUpdate);

  // index 0
  gExpected = {
    added: {
      cookies: {
        "test1.example.org": ["c1", "c2"]
      },
      localStorage: {
        "http://test1.example.org": ["l1"]
      }
    }
  };
  win.addCookie("c1", "foobar1");
  win.addCookie("c2", "foobar2");
  win.localStorage.setItem("l1", "foobar1");
  yield undefined;

  // index 1
  gExpected = {
    changed: {
      cookies: {
        "test1.example.org": ["c1"]
      }
    },
    added: {
      localStorage: {
        "http://test1.example.org": ["l2"]
      }
    }
  };
  win.addCookie("c1", "new_foobar1");
  win.localStorage.setItem("l2", "foobar2");
  yield undefined;

  // index 2
  gExpected = {
    deleted: {
      cookies: {
        "test1.example.org": ["c2"]
      },
      localStorage: {
        "http://test1.example.org": ["l1"]
      }
    },
    added: {
      localStorage: {
        "http://test1.example.org": ["l3"]
      }
    }
  };
  win.removeCookie("c2");
  win.localStorage.removeItem("l1");
  win.localStorage.setItem("l3", "foobar3");
  yield undefined;

  // index 3
  gExpected = {
    added: {
      cookies: {
        "test1.example.org": ["c3"]
      },
      sessionStorage: {
        "http://test1.example.org": ["s1", "s2"]
      }
    },
    changed: {
      localStorage: {
        "http://test1.example.org": ["l3"]
      }
    },
    deleted: {
      cookies: {
        "test1.example.org": ["c1"]
      },
      localStorage: {
        "http://test1.example.org": ["l2"]
      }
    }
  };
  win.removeCookie("c1");
  win.addCookie("c3", "foobar3");
  win.localStorage.removeItem("l2");
  win.sessionStorage.setItem("s1", "foobar1");
  win.sessionStorage.setItem("s2", "foobar2");
  win.localStorage.setItem("l3", "new_foobar3");
  yield undefined;

  // index 4
  gExpected = {
    deleted: {
      sessionStorage: {
        "http://test1.example.org": ["s1"]
      }
    }
  };
  win.sessionStorage.removeItem("s1");
  yield undefined;

  // index 5
  gExpected = {
    deleted: {
      cookies: {
        "test1.example.org": ["c3"]
      }
    }
  };
  front.on("stores-cleared", onStoresCleared);
  win.clear();
  yield undefined;
  // Another 2 more yield undefined s so as to wait for the "stores-cleared" to
  // fire. One for Local Storage and other for Session Storage
  yield undefined;
  yield undefined;

  front.off("stores-cleared", onStoresCleared);
  front.off("stores-update", onStoresUpdate);
  finishTests(client);
}


function test() {
  addTab(MAIN_DOMAIN + "storage-updates.html").then(function(doc) {
    initDebuggerServer();

    let client = new DebuggerClient(DebuggerServer.connectPipe());
    connectDebuggerClient(client).then(form => {
      let front = StorageFront(client, form);
      gTests = UpdateTests(front, doc.defaultView.wrappedJSObject,
                           client);
      // Make an initial call to initialize the actor
      front.listStores().then(() => gTests.next());
    });
  })
}
