"use strict";

// Used to access the DOM node in this test
const DOM_PARENT_ID = "container";
const DOM_PARENT = document.getElementById(DOM_PARENT_ID);
const APP_IFRAME_ID = "appFrame";
const APP_IFRAME_ID2 = "appFrame2";

// Settings for testing the permission
const SESSION_PERSIST_MINUTES = 10;
const PERMISSION_TYPE = "geolocation";
const permManager = SpecialPowers.Cc["@mozilla.org/permissionmanager;1"]
                    .getService(SpecialPowers.Ci.nsIPermissionManager);

// Used to access App's information like appId
const gAppsService = SpecialPowers.Cc["@mozilla.org/AppsService;1"]
                    .getService(SpecialPowers.Ci.nsIAppsService);

// Target-app for this testing
const APP_URL = "http://example.org";
const APP_MANIFEST = "http://example.org/manifest.webapp";

// Used to embed a remote app that open the test-target-app above
const embedAppHostedManifestURL = window.location.origin +
      '/tests/dom/ipc/tests/file_app.sjs?testToken=test_permission_embed.html&template=file_app.template.webapp';
var embedApp;

// Used to add listener for ipc:content-shutdown that
// will be triggered after ContentParent::DeallocateTabId
var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('test_permission_framescript.js'));

// Delay reporting a result until after finish() got called
SimpleTest.waitForExplicitFinish();

// Run tests in order
function runTests() {
  if (!tests.length) {
    ok(true, "pass all tests!");
    SimpleTest.finish();
    return;
  }

  var test = tests.shift();
  test();
}

function test1() {
  allocateAppFrame(APP_IFRAME_ID, DOM_PARENT, APP_URL, APP_MANIFEST);

  var appId = gAppsService.getAppLocalIdByManifestURL(APP_MANIFEST);

  if (!SpecialPowers.hasPermission( PERMISSION_TYPE,
                                    { url: APP_URL,
                                      originAttributes: { appId: appId }})) {
    errorHandler('[test 1] App should have permission: ' + PERMISSION_TYPE);
  }

  removeAppFrame(APP_IFRAME_ID);

  // We expect there is no permission for the test-target-app
  // after removing the in-process iframe embedding this app
  runNextIfAppHasPermission(1, false, APP_URL, APP_MANIFEST);
}

function test2() {
  var afterShutdown = function () {
    // We expect there is no permission for the test-target-app
    // after removing the remote iframe embedding this app
    runNextIfAppHasPermission(2, false, APP_URL, APP_MANIFEST);
  };

  // Test permission after the child-process containing
  // test-target-app is killed
  afterContentShutdown(1, afterShutdown);

  allocateAppFrame(APP_IFRAME_ID, DOM_PARENT, APP_URL, APP_MANIFEST, true);

  var appId = gAppsService.getAppLocalIdByManifestURL(APP_MANIFEST);

  if (!SpecialPowers.hasPermission( PERMISSION_TYPE,
                                    { url: APP_URL,
                                      originAttributes: { appId: appId }})) {
    errorHandler('[test 2] App should have permission: ' + PERMISSION_TYPE);
  }

  removeAppFrame(APP_IFRAME_ID);
}

function test3() {
  var afterGrandchildShutdown = function () {
    // We expect there is no permission for the test-target-app
    // after removing the remote iframe embedding this app
    runNextIfAppHasPermission(3, false, APP_URL, APP_MANIFEST);
  };

  // Test permission after the grandchild-process
  // containing test-target-app is killed
  afterContentShutdown(1, afterGrandchildShutdown);

  allocateAppFrame(APP_IFRAME_ID,
                   DOM_PARENT,
                   embedApp.manifest.launch_path,
                   embedApp.manifestURL,
                   true);

  var appId = gAppsService.getAppLocalIdByManifestURL(APP_MANIFEST);

  if (!SpecialPowers.hasPermission(PERMISSION_TYPE,
                                   { url: APP_URL,
                                     originAttributes: { appId: appId }})) {
    errorHandler('[test 3] App should have permission: ' + PERMISSION_TYPE);
  }
}

function test4() {
  var afterGrandchildShutdown = function () {
    // We expect there is still a permission for the test-target-app
    // after killing test-target-app on 3rd-level process
    runNextIfAppHasPermission(4, true, APP_URL, APP_MANIFEST);
  };

  // Test permission after the grandchild-process
  // containing test-target-app is killed
  afterContentShutdown(1, afterGrandchildShutdown);

  // Open a child process(2nd level) and a grandchild process(3rd level)
  // that contains a test-target-app
  allocateAppFrame(APP_IFRAME_ID,
                   DOM_PARENT,
                   embedApp.manifest.launch_path,
                   embedApp.manifestURL,
                   true);

  // Open another child process that contains
  // another test-target-app(2nd level)
  allocateAppFrame(APP_IFRAME_ID2,
                   DOM_PARENT,
                   APP_URL,
                   APP_MANIFEST,
                   true);

  var appId = gAppsService.getAppLocalIdByManifestURL(APP_MANIFEST);

  if (!SpecialPowers.hasPermission(PERMISSION_TYPE,
                                   { url: APP_URL,
                                     originAttributes: { appId: appId }})) {
    errorHandler('[test 4] App should have permission: ' + PERMISSION_TYPE);
  }
}

function test5() {
  var afterShutdown = function () {
    // We expect there is no permission for the test-target-app
    // after crashing its parent-process
    runNextIfAppHasPermission(5, false, APP_URL, APP_MANIFEST);
  };

  // Test permission after the parent-process of test-target-app is crashed.
  afterContentShutdown(2, afterShutdown);

  allocateAppFrame(APP_IFRAME_ID,
                   DOM_PARENT,
                   embedApp.manifest.launch_path + '#' + encodeURIComponent('crash'),
                   embedApp.manifestURL,
                   true);

  var appId = gAppsService.getAppLocalIdByManifestURL(APP_MANIFEST);

  if (!SpecialPowers.hasPermission( PERMISSION_TYPE,
                                    { url: APP_URL,
                                      originAttributes: { appId: appId }})) {
    errorHandler('[test 5] App should have permission: ' + PERMISSION_TYPE);
  }

  // Crash the child-process on 2nd level after
  // the grandchild process on 3rd is allocated
  var handler = {'crash': function() {
      gScript.sendAsyncMessage("crashreporter-status", {});

      getCrashReporterStatus(function(enable) {
        if (enable) {
          SimpleTest.expectChildProcessCrash();
        }
        crashChildProcess(APP_IFRAME_ID);
      });
    }
  };

  receiveMessageFromChildFrame(APP_IFRAME_ID, handler);
}

// Setup the prefrences and permissions.
// This function should be called before any tests
function setupPrefsAndPermissions() {
  SpecialPowers.pushPrefEnv({"set": [
    ["dom.mozBrowserFramesEnabled", true],
    ["dom.ipc.tabs.nested.enabled", true],
    ["dom.ipc.tabs.disabled", false]]},
    function() {
      var autoManageApp = function () {
        SpecialPowers.setAllAppsLaunchable(true);
        // No confirmation needed when an app is installed and uninstalled.
        SpecialPowers.autoConfirmAppInstall(() => {
          // This callback should trigger the first test
          SpecialPowers.autoConfirmAppUninstall(runTests);
        });
      };

      setupOpenAppPermission(document, autoManageApp);
    }
  );
}

function setupOpenAppPermission(ctx, callback) {
  SpecialPowers.pushPermissions([
    { "type": "browser", "allow": true, "context": ctx}, // for mozbrowser
    { "type": "embed-apps", "allow": true, "context": ctx }, // for mozapp
    { "type": "webapps-manage", "allow": true, "context": ctx }], // for (un)installing apps
    function checkPermissions() {
      if (SpecialPowers.hasPermission("browser", ctx) &&
          SpecialPowers.hasPermission("embed-apps", ctx) &&
          SpecialPowers.hasPermission("webapps-manage", ctx)) {
        callback();
      } else {
        errorHandler(">> At least one of required permissions to open app is disallowed!\n");
      }
    });
}

function installApp() {
  // Install App from manifest
  var request = navigator.mozApps.install(embedAppHostedManifestURL);
  request.onerror = cbError;
  request.onsuccess = function() {
    // Get installed app
    embedApp = request.result; // Assign to global variable
    runTests();
  }
}

function uninstallApp() {
  var request = navigator.mozApps.mgmt.uninstall(embedApp);
  request.onerror = cbError;
  request.onsuccess = function() {
    info("uninstall app susseccfully!");
    runTests();
  }
}

function removeAppFrame(id) {
  var ifr  = document.getElementById(id);
  ifr.remove();
}

function allocateAppFrame(id, parentNode, appSRC, manifestURL, useRemote = false) {
  var ifr = document.createElement('iframe');
  ifr.setAttribute('id', id);
  ifr.setAttribute('remote', useRemote? "true" : "false");
  ifr.setAttribute('mozbrowser', 'true');
  ifr.setAttribute('mozapp', manifestURL);
  ifr.setAttribute('src', appSRC);
  parentNode.appendChild(ifr);
}

function receiveMessageFromChildFrame(id, handlers) {
  var ifr = document.getElementById(id);
  ifr.addEventListener('mozbrowsershowmodalprompt', function (recvMsg) {
    var msg = recvMsg.detail.message;
    handlers[msg]();
  });
}

function addPermissionToApp(appURL, manifestURL) {
  var appId = gAppsService.getAppLocalIdByManifestURL(manifestURL);

  // Get the time now. This is used for permission manager's expire_time
  var now = Number(Date.now());

  // Add app's permission asynchronously
  SpecialPowers.pushPermissions([
      { "type":PERMISSION_TYPE,
        "allow": 1,
        "expireType":permManager.EXPIRE_SESSION,
        "expireTime":now + SESSION_PERSIST_MINUTES*60*1000,
        "context": { url: appURL,
                     originAttributes: { appId: appId } }
      }
    ], function() {
      runTests();
    });
}

function runNextIfAppHasPermission(round, expect, appURL, manifestURL) {
  var appId = gAppsService.getAppLocalIdByManifestURL(manifestURL);

  var hasPerm = SpecialPowers.hasPermission(PERMISSION_TYPE,
                                            { url: appURL,
                                              originAttributes: { appId: appId }});
  var result = (expect==hasPerm);
  if (result) {
    runTests();
  } else {
    errorHandler( '[test ' + round + '] App should ' + ((expect)? '':'NOT ') +
                  'have permission: ' + PERMISSION_TYPE);
  }
}

function afterContentShutdown(times, callback) {
  // handle the message from test_permission_framescript.js
  var num = 0;
  gScript.addMessageListener('content-shutdown', function onShutdown(data) {
    num += 1;
    if (num >= times) {
      gScript.removeMessageListener('content-shutdown', onShutdown);
      callback();
    }
  });
}

function getCrashReporterStatus(callback) {
  gScript.addMessageListener('crashreporter-enable', function getStatus(data) {
    gScript.removeMessageListener('crashreporter-enable', getStatus);
    callback(data);
  });
}

// Inject a frame script that crashes the content process
function crashChildProcess(frameId) {
  var child = document.getElementById(frameId);
  var mm = SpecialPowers.getBrowserFrameMessageManager(child);
  var childFrameScriptStr =
    'function ContentScriptScope() {' +
      'var Cu = Components.utils;' +
      'Cu.import("resource://gre/modules/ctypes.jsm");' +
      'var crash = function() {' +
        'var zero = new ctypes.intptr_t(8);' +
        'var badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));' +
        'badptr.contents;' +
      '};' +
      'privateNoteIntentionalCrash();' +
      'crash();' +
    '}';
  mm.loadFrameScript('data:,new ' + childFrameScriptStr, false);
}

function errorHandler(errMsg) {
  ok(false, errMsg);
  SimpleTest.finish();
}

function cbError(e) {
  errorHandler("Error callback invoked: " + this.error.name);
}
