/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
/* global Services, do_register_cleanup, do_test_pending */

"use strict";

const Cm = Components.manager;

ChromeUtils.import("resource://gre/modules/Promise.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

const INFO_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-info;1";
const PROVIDER_CONTRACT_ID = "@mozilla.org/presentation-device/multicastdns-provider;1";
const SD_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-sd;1";
const UUID_CONTRACT_ID = "@mozilla.org/uuid-generator;1";
const SERVER_CONTRACT_ID = "@mozilla.org/presentation/control-service;1";

const PREF_DISCOVERY = "dom.presentation.discovery.enabled";
const PREF_DISCOVERABLE = "dom.presentation.discoverable";
const PREF_DEVICENAME= "dom.presentation.device.name";

const LATEST_VERSION = 1;
const SERVICE_TYPE = "_presentation-ctrl._tcp";
const versionAttr = Cc["@mozilla.org/hash-property-bag;1"]
                      .createInstance(Ci.nsIWritablePropertyBag2);
versionAttr.setPropertyAsUint32("version", LATEST_VERSION);

var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);

function sleep(aMs) {
  return new Promise(resolve => {

    let timer = Cc["@mozilla.org/timer;1"]
                  .createInstance(Ci.nsITimer);

    timer.initWithCallback({
      notify: function () {
        resolve();
      },
    }, aMs, timer.TYPE_ONE_SHOT);

  });
}

function MockFactory(aClass) {
  this._cls = aClass;
}
MockFactory.prototype = {
  createInstance: function(aOuter, aIID) {
    if (aOuter) {
      throw Cr.NS_ERROR_NO_AGGREGATION;
    }
    switch(typeof(this._cls)) {
      case "function":
        return new this._cls().QueryInterface(aIID);
      case "object":
        return this._cls.QueryInterface(aIID);
      default:
        return null;
    }
  },
  lockFactory: function(aLock) {
    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
  },
  QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory])
};

function ContractHook(aContractID, aClass) {
  this._contractID = aContractID;
  this.classID = Cc[UUID_CONTRACT_ID].getService(Ci.nsIUUIDGenerator).generateUUID();
  this._newFactory = new MockFactory(aClass);

  if (!this.hookedMap.has(this._contractID)) {
    this.hookedMap.set(this._contractID, []);
  }

  this.init();
}

ContractHook.prototype = {
  hookedMap: new Map(), // remember only the most original factory.

  init: function() {
    this.reset();

    let oldContract = this.unregister();
    this.hookedMap.get(this._contractID).push(oldContract);
    registrar.registerFactory(this.classID,
                              "",
                              this._contractID,
                              this._newFactory);

    registerCleanupFunction(() => { this.cleanup.apply(this); });
  },

  reset: function() {},

  cleanup: function() {
    this.reset();

    this.unregister();
    let prevContract = this.hookedMap.get(this._contractID).pop();

    if (prevContract.factory) {
      registrar.registerFactory(prevContract.classID,
                                "",
                                this._contractID,
                                prevContract.factory);
    }
  },

  unregister: function() {
    var classID, factory;

    try {
      classID = registrar.contractIDToCID(this._contractID);
      factory = Cm.getClassObject(Cc[this._contractID], Ci.nsIFactory);
    } catch (ex) {
      classID = "";
      factory = null;
    }

    if (factory) {
      registrar.unregisterFactory(classID, factory);
    }

    return { classID: classID, factory: factory };
  }
};

function MockDNSServiceInfo() {}
MockDNSServiceInfo.prototype = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceInfo]),

  set host(aHost) {
    this._host = aHost;
  },

  get host() {
    return this._host;
  },

  set address(aAddress) {
    this._address = aAddress;
  },

  get address() {
    return this._address;
  },

  set port(aPort) {
    this._port = aPort;
  },

  get port() {
    return this._port;
  },

  set serviceName(aServiceName) {
    this._serviceName = aServiceName;
  },

  get serviceName() {
    return this._serviceName;
  },

  set serviceType(aServiceType) {
    this._serviceType = aServiceType;
  },

  get serviceType() {
    return this._serviceType;
  },

  set domainName(aDomainName) {
    this._domainName = aDomainName;
  },

  get domainName() {
    return this._domainName;
  },

  set attributes(aAttributes) {
    this._attributes = aAttributes;
  },

  get attributes() {
    return this._attributes;
  }
};

function TestPresentationDeviceListener() {
  this.devices = {};
}
TestPresentationDeviceListener.prototype = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                          Ci.nsISupportsWeakReference]),

  addDevice: function(device) { this.devices[device.id] = device; },
  removeDevice: function(device) { delete this.devices[device.id]; },
  updateDevice: function(device) { this.devices[device.id] = device; },
  onSessionRequest: function(device, url, presentationId, controlChannel) {},

  count: function() {
    var size = 0, key;
    for (key in this.devices) {
        if (this.devices.hasOwnProperty(key)) {
          ++size;
        }
    }
    return size;
  }
};

function createDevice(host, port, serviceName, serviceType, domainName, attributes) {
  let device = new MockDNSServiceInfo();
  device.host = host || "";
  device.port = port || 0;
  device.address = host || "";
  device.serviceName = serviceName || "";
  device.serviceType = serviceType || "";
  device.domainName = domainName || "";
  device.attributes = attributes || versionAttr;
  return device;
}

function registerService() {
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let deferred = Promise.defer();

  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {},
    registerService: function(serviceInfo, listener) {
      deferred.resolve();
      this.serviceRegistered++;
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: () => {
          this.serviceUnregistered++;
        }
      };
    },
    resolveService: function(serviceInfo, listener) {},
    serviceRegistered: 0,
    serviceUnregistered: 0
  };
  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);

  Assert.equal(mockObj.serviceRegistered, 0);
  Assert.equal(mockObj.serviceUnregistered, 0);

  // Register
  provider.listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
  };

  deferred.promise.then(function() {
    Assert.equal(mockObj.serviceRegistered, 1);
    Assert.equal(mockObj.serviceUnregistered, 0);

    // Unregister
    provider.listener = null;
    Assert.equal(mockObj.serviceRegistered, 1);
    Assert.equal(mockObj.serviceUnregistered, 1);

    run_next_test();
  });
}

function noRegisterService() {
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);

  let deferred = Promise.defer();

  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {},
    registerService: function(serviceInfo, listener) {
      deferred.resolve();
      Assert.ok(false, "should not register service if not discoverable");
    },
    resolveService: function(serviceInfo, listener) {},
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);

  // Try register
  provider.listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
  };

  let race = Promise.race([
    deferred.promise,
    sleep(1000),
  ]);

  race.then(() => {
    provider.listener = null;

    run_next_test();
  });
}

function registerServiceDynamically() {
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);

  let deferred = Promise.defer();

  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {},
    registerService: function(serviceInfo, listener) {
      deferred.resolve();
      this.serviceRegistered++;
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: () => {
          this.serviceUnregistered++;
        }
      };
    },
    resolveService: function(serviceInfo, listener) {},
    serviceRegistered: 0,
    serviceUnregistered: 0
  };
  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);

  Assert.equal(mockObj.serviceRegistered, 0);
  Assert.equal(mockObj.serviceRegistered, 0);

  // Try Register
  provider.listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
  };

  Assert.equal(mockObj.serviceRegistered, 0);
  Assert.equal(mockObj.serviceUnregistered, 0);

  // Enable registration
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  deferred.promise.then(function() {
    Assert.equal(mockObj.serviceRegistered, 1);
    Assert.equal(mockObj.serviceUnregistered, 0);

    // Disable registration
    Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);
    Assert.equal(mockObj.serviceRegistered, 1);
    Assert.equal(mockObj.serviceUnregistered, 1);

    // Try unregister
    provider.listener = null;
    Assert.equal(mockObj.serviceRegistered, 1);
    Assert.equal(mockObj.serviceUnregistered, 1);

    run_next_test();
  });
}

function addDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);
  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = new TestPresentationDeviceListener();
  Assert.equal(listener.count(), 0);

  // Start discovery
  provider.listener = listener;
  Assert.equal(listener.count(), 1);

  // Force discovery again
  provider.forceDiscovery();
  Assert.equal(listener.count(), 1);

  provider.listener = null;
  Assert.equal(listener.count(), 1);

  run_next_test();
}

function filterDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);
  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {
      let tests = [
        { requestedUrl: "app://fling-player.gaiamobile.org/index.html", supported: true },
        { requestedUrl: "app://notification-receiver.gaiamobile.org/index.html", supported: true },
        { requestedUrl: "http://example.com", supported: true },
        { requestedUrl: "https://example.com", supported: true },
        { requestedUrl: "ftp://example.com", supported: false },
        { requestedUrl: "app://unknown-app-id", supported: false },
        { requestedUrl: "unknowSchem://example.com", supported: false },
      ];

      for (let test of tests) {
        Assert.equal(device.isRequestedUrlSupported(test.requestedUrl), test.supported);
      }

      provider.listener = null;
      provider = null;
      run_next_test();
    },
    updateDevice: function() {},
    removeDevice: function() {},
    onSessionRequest: function() {},
  };

  provider.listener = listener;
}

function handleSessionRequest() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, false);

  const testUrl = "http://example.com";
  const testPresentationId = "test-presentation-id";
  const testDeviceName = "test-device-name";

  Services.prefs.setCharPref(PREF_DEVICENAME, testDeviceName);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);
  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    connect: function(deviceInfo) {
      this.request = {
        deviceInfo: deviceInfo,
      };
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlChannel]),
      };
    },
    id: "",
    version: LATEST_VERSION,
    isCompatibleServer: function(version) {
      return this.version === version;
    }
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {
      this.device = device;
    },
  };

  provider.listener = listener;

  let controlChannel = listener.device.establishControlChannel();

  Assert.equal(mockServerObj.request.deviceInfo.id, mockDevice.host);
  Assert.equal(mockServerObj.request.deviceInfo.address, mockDevice.host);
  Assert.equal(mockServerObj.request.deviceInfo.port, mockDevice.port);
  Assert.equal(mockServerObj.id, testDeviceName);

  provider.listener = null;

  run_next_test();
}

function handleOnSessionRequest() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);
  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    startServer: function() {},
    sessionRequest: function() {},
    close: function() {},
    id: '',
    version: LATEST_VERSION,
    port: 0,
    listener: null,
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
    onSessionRequest: function(device, url, presentationId, controlChannel) {
      Assert.ok(true, "receive onSessionRequest event");
      this.request = {
        deviceId: device.id,
        url: url,
        presentationId: presentationId,
      };
    }
  };

  provider.listener = listener;

  const deviceInfo = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsITCPDeviceInfo]),
    id: mockDevice.host,
    address: mockDevice.host,
    port: 54321,
  };

  const testUrl = "http://example.com";
  const testPresentationId = "test-presentation-id";
  const testControlChannel = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlChannel]),
  };
  provider.QueryInterface(Ci.nsIPresentationControlServerListener)
          .onSessionRequest(deviceInfo, testUrl, testPresentationId, testControlChannel);

  Assert.equal(listener.request.deviceId, deviceInfo.id);
  Assert.equal(listener.request.url, testUrl);
  Assert.equal(listener.request.presentationId, testPresentationId);

  provider.listener = null;

  run_next_test();
}

function handleOnSessionRequestFromUnknownDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {},
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {}
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    startServer: function() {},
    sessionRequest: function() {},
    close: function() {},
    id: '',
    version: LATEST_VERSION,
    port: 0,
    listener: null,
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {
      Assert.ok(false, "shouldn't create any new device");
    },
    removeDevice: function(device) {
      Assert.ok(false, "shouldn't remote any device");
    },
    updateDevice: function(device) {
      Assert.ok(false, "shouldn't update any device");
    },
    onSessionRequest: function(device, url, presentationId, controlChannel) {
      Assert.ok(true, "receive onSessionRequest event");
      this.request = {
        deviceId: device.id,
        url: url,
        presentationId: presentationId,
      };
    }
  };

  provider.listener = listener;

  const deviceInfo = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsITCPDeviceInfo]),
    id: "unknown-device.local",
    address: "unknown-device.local",
    port: 12345,
  };

  const testUrl = "http://example.com";
  const testPresentationId = "test-presentation-id";
  const testControlChannel = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlChannel]),
  };
  provider.QueryInterface(Ci.nsIPresentationControlServerListener)
          .onSessionRequest(deviceInfo, testUrl, testPresentationId, testControlChannel);

  Assert.equal(listener.request.deviceId, deviceInfo.id);
  Assert.equal(listener.request.url, testUrl);
  Assert.equal(listener.request.presentationId, testPresentationId);

  provider.listener = null;

  run_next_test();
}

function noAddDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);

  let mockDevice = createDevice("device.local", 12345, "service.name", SERVICE_TYPE);
  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      Assert.ok(false, "shouldn't perform any device discovery");
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
    }
  };
  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);

  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
  };
  provider.listener = listener;
  provider.forceDiscovery();
  provider.listener = null;

  run_next_test();
}

function ignoreIncompatibleDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);

  let deferred = Promise.defer();

  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {
      deferred.resolve();
      listener.onServiceRegistered(createDevice("",
                                                54321,
                                                mockDevice.serviceName,
                                                mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    startServer: function() {
      Services.tm.dispatchToMainThread(() => {
        this.listener.onServerReady(this.port, this.certFingerprint);
      });
    },
    sessionRequest: function() {},
    close: function() {},
    id: '',
    version: LATEST_VERSION,
    isCompatibleServer: function(version) {
      return false;
    },
    port: 54321,
    certFingerprint: 'mock-cert-fingerprint',
    listener: null,
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = new TestPresentationDeviceListener();

  // Register service
  provider.listener = listener;

  deferred.promise.then(function() {
    Assert.equal(mockServerObj.id, mockDevice.host);

    // Start discovery
    Services.prefs.setBoolPref(PREF_DISCOVERY, true);
    Assert.equal(listener.count(), 0);

    provider.listener = null;
    provider = null;

    run_next_test();
  });
}

function ignoreSelfDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);

  let deferred = Promise.defer();
  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {
      deferred.resolve();
      listener.onServiceRegistered(createDevice("",
                                                0,
                                                mockDevice.serviceName,
                                                mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    startServer: function() {
      Services.tm.dispatchToMainThread(() => {
        this.listener.onServerReady(this.port, this.certFingerprint);
      });
    },
    sessionRequest: function() {},
    close: function() {},
    id: '',
    version: LATEST_VERSION,
    isCompatibleServer: function(version) {
      return this.version === version;
    },
    port: 54321,
    certFingerprint: 'mock-cert-fingerprint',
    listener: null,
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = new TestPresentationDeviceListener();

  // Register service
  provider.listener = listener;
  deferred.promise.then(() => {
    Assert.equal(mockServerObj.id, mockDevice.host);

    // Start discovery
    Services.prefs.setBoolPref(PREF_DISCOVERY, true);
    Assert.equal(listener.count(), 0);

    provider.listener = null;
    provider = null;

    run_next_test();
  });
}

function addDeviceDynamically() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);
  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    }
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = new TestPresentationDeviceListener();
  provider.listener = listener;
  Assert.equal(listener.count(), 0);

  // Enable discovery
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
  Assert.equal(listener.count(), 1);

  // Try discovery again
  provider.forceDiscovery();
  Assert.equal(listener.count(), 1);

  // Try discovery once more
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);
  provider.forceDiscovery();
  Assert.equal(listener.count(), 1);

  provider.listener = null;

  run_next_test();
}

function updateDevice() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
  let mockDevice2 = createDevice("A.local", 23456, "N2", SERVICE_TYPE);

  let mockObj = {
    discovered: false,

    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);

      if (!this.discovered) {
        listener.onServiceFound(mockDevice1);
      } else {
        listener.onServiceFound(mockDevice2);
      }
      this.discovered = true;

      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {
          listener.onDiscoveryStopped(serviceType);
        }
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
      if (serviceInfo.serviceName == "N1") {
        listener.onServiceResolved(mockDevice1);
      } else if (serviceInfo.serviceName == "N2") {
        listener.onServiceResolved(mockDevice2);
      } else {
        Assert.ok(false);
      }
    }
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),

    addDevice: function(device) {
      Assert.ok(!this.isDeviceAdded);
      Assert.equal(device.id, mockDevice1.host);
      Assert.equal(device.name, mockDevice1.serviceName);
      this.isDeviceAdded = true;
    },
    removeDevice: function(device) { Assert.ok(false); },
    updateDevice: function(device) {
      Assert.ok(!this.isDeviceUpdated);
      Assert.equal(device.id, mockDevice2.host);
      Assert.equal(device.name, mockDevice2.serviceName);
      this.isDeviceUpdated = true;
    },

    isDeviceAdded: false,
    isDeviceUpdated: false
  };
  Assert.equal(listener.isDeviceAdded, false);
  Assert.equal(listener.isDeviceUpdated, false);

  // Start discovery
  provider.listener = listener; // discover: N1

  Assert.equal(listener.isDeviceAdded, true);
  Assert.equal(listener.isDeviceUpdated, false);

  // temporarily disable to stop discovery and re-enable
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  provider.forceDiscovery(); // discover: N2

  Assert.equal(listener.isDeviceAdded, true);
  Assert.equal(listener.isDeviceUpdated, true);

  provider.listener = null;

  run_next_test();
}

function diffDiscovery() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  let mockDevice1 = createDevice("A.local", 12345, "N1", SERVICE_TYPE);
  let mockDevice2 = createDevice("B.local", 23456, "N2", SERVICE_TYPE);
  let mockDevice3 = createDevice("C.local", 45678, "N3", SERVICE_TYPE);

  let mockObj = {
    discovered: false,

    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);

      if (!this.discovered) {
        listener.onServiceFound(mockDevice1);
        listener.onServiceFound(mockDevice2);
      } else {
        listener.onServiceFound(mockDevice1);
        listener.onServiceFound(mockDevice3);
      }
      this.discovered = true;

      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {
          listener.onDiscoveryStopped(serviceType);
        }
      };
    },
    registerService: function(serviceInfo, listener) {},
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceType, SERVICE_TYPE);
      if (serviceInfo.serviceName == "N1") {
        listener.onServiceResolved(mockDevice1);
      } else if (serviceInfo.serviceName == "N2") {
        listener.onServiceResolved(mockDevice2);
      } else if (serviceInfo.serviceName == "N3") {
        listener.onServiceResolved(mockDevice3);
      } else {
        Assert.ok(false);
      }
    }
  };

  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = new TestPresentationDeviceListener();
  Assert.equal(listener.count(), 0);

  // Start discovery
  provider.listener = listener; // discover: N1, N2
  Assert.equal(listener.count(), 2);
  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
  Assert.equal(listener.devices['B.local'].name, mockDevice2.serviceName);
  Assert.ok(!listener.devices['C.local']);

  // temporarily disable to stop discovery and re-enable
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  provider.forceDiscovery(); // discover: N1, N3, going to remove: N2
  Assert.equal(listener.count(), 3);
  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
  Assert.equal(listener.devices['B.local'].name, mockDevice2.serviceName);
  Assert.equal(listener.devices['C.local'].name, mockDevice3.serviceName);

  // temporarily disable to stop discovery and re-enable
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  provider.forceDiscovery(); // discover: N1, N3, remove: N2
  Assert.equal(listener.count(), 2);
  Assert.equal(listener.devices['A.local'].name, mockDevice1.serviceName);
  Assert.ok(!listener.devices['B.local']);
  Assert.equal(listener.devices['C.local'].name, mockDevice3.serviceName);

  provider.listener = null;

  run_next_test();
}

function serverClosed() {
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);
  Services.prefs.setBoolPref(PREF_DISCOVERY, true);

  let mockDevice = createDevice("device.local",
                                12345,
                                "service.name",
                                SERVICE_TYPE);

  let mockObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {
      listener.onDiscoveryStarted(serviceType);
      listener.onServiceFound(createDevice("",
                                           0,
                                           mockDevice.serviceName,
                                           mockDevice.serviceType));
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: function() {}
      };
    },
    registerService: function(serviceInfo, listener) {
      this.serviceRegistered++;
      return {
        QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
        cancel: () => {
          this.serviceUnregistered++;
        }
      };
    },
    resolveService: function(serviceInfo, listener) {
      Assert.equal(serviceInfo.serviceName, mockDevice.serviceName);
      Assert.equal(serviceInfo.serviceType, mockDevice.serviceType);
      listener.onServiceResolved(createDevice(mockDevice.host,
                                              mockDevice.port,
                                              mockDevice.serviceName,
                                              mockDevice.serviceType));
    },
    serviceRegistered: 0,
    serviceUnregistered: 0
  };
  let contractHook = new ContractHook(SD_CONTRACT_ID, mockObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);

  Assert.equal(mockObj.serviceRegistered, 0);
  Assert.equal(mockObj.serviceUnregistered, 0);

  // Register
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) { this.devices.push(device); },
    removeDevice: function(device) {},
    updateDevice: function(device) {},
    devices: []
  };
  Assert.equal(listener.devices.length, 0);

  provider.listener = listener;
  Assert.equal(mockObj.serviceRegistered, 1);
  Assert.equal(mockObj.serviceUnregistered, 0);
  Assert.equal(listener.devices.length, 1);

  let serverListener = provider.QueryInterface(Ci.nsIPresentationControlServerListener);
  let randomPort = 9527;
  serverListener.onServerReady(randomPort, '');

  Assert.equal(mockObj.serviceRegistered, 2);
  Assert.equal(mockObj.serviceUnregistered, 1);
  Assert.equal(listener.devices.length, 1);

  // Unregister
  provider.listener = null;
  Assert.equal(mockObj.serviceRegistered, 2);
  Assert.equal(mockObj.serviceUnregistered, 2);
  Assert.equal(listener.devices.length, 1);

  run_next_test();
}

function serverRetry() {
  Services.prefs.setBoolPref(PREF_DISCOVERY, false);
  Services.prefs.setBoolPref(PREF_DISCOVERABLE, true);

  let isRetrying = false;

  let mockSDObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIDNSServiceDiscovery]),
    startDiscovery: function(serviceType, listener) {},
    registerService: function(serviceInfo, listener) {
      Assert.ok(isRetrying, "register service after retrying startServer");
      provider.listener = null;
      run_next_test();
    },
    resolveService: function(serviceInfo, listener) {}
  };

  let mockServerObj = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationControlService]),
    startServer: function(encrypted, port) {
      if (!isRetrying) {
        isRetrying = true;
        Services.tm.dispatchToMainThread(() => {
          this.listener.onServerStopped(Cr.NS_ERROR_FAILURE);
        });
      } else {
        this.port = 54321;
        Services.tm.dispatchToMainThread(() => {
          this.listener.onServerReady(this.port, this.certFingerprint);
        });
      }
    },
    sessionRequest: function() {},
    close: function() {},
    id: '',
    version: LATEST_VERSION,
    port: 0,
    certFingerprint: 'mock-cert-fingerprint',
    listener: null,
  };

  let contractHookSD = new ContractHook(SD_CONTRACT_ID, mockSDObj);
  let contractHookServer = new ContractHook(SERVER_CONTRACT_ID, mockServerObj);
  let provider = Cc[PROVIDER_CONTRACT_ID].createInstance(Ci.nsIPresentationDeviceProvider);
  let listener = {
    QueryInterface: ChromeUtils.generateQI([Ci.nsIPresentationDeviceListener,
                                            Ci.nsISupportsWeakReference]),
    addDevice: function(device) {},
    removeDevice: function(device) {},
    updateDevice: function(device) {},
    onSessionRequest: function(device, url, presentationId, controlChannel) {}
  };

  provider.listener = listener;
}

function run_test() {
  // Need profile dir to store the key / cert
  do_get_profile();
  // Ensure PSM is initialized
  Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);

  let infoHook = new ContractHook(INFO_CONTRACT_ID, MockDNSServiceInfo);

  registerCleanupFunction(() => {
    Services.prefs.clearUserPref(PREF_DISCOVERY);
    Services.prefs.clearUserPref(PREF_DISCOVERABLE);
  });

  add_test(registerService);
  add_test(noRegisterService);
  add_test(registerServiceDynamically);
  add_test(addDevice);
  add_test(filterDevice);
  add_test(handleSessionRequest);
  add_test(handleOnSessionRequest);
  add_test(handleOnSessionRequestFromUnknownDevice);
  add_test(noAddDevice);
  add_test(ignoreIncompatibleDevice);
  add_test(ignoreSelfDevice);
  add_test(addDeviceDynamically);
  add_test(updateDevice);
  add_test(diffDiscovery);
  add_test(serverClosed);
  add_test(serverRetry);

  run_next_test();
}
