// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.net.xpc.NativeMessagingTransportTest');
goog.setTestOnly('goog.net.xpc.NativeMessagingTransportTest');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannel');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.testing.jsunit');

// This test only tests the native messaing transport protocol version 2.
// Testing of previous versions and of backward/forward compatibility is done
// in crosspagechannel_test.html.


function tearDown() {
  goog.net.xpc.NativeMessagingTransport.activeCount_ = {};
  goog.events.removeAll(window.postMessage ? window : document, 'message');
}


function testConstructor() {
  var xpc = getTestChannel();

  var t = new goog.net.xpc.NativeMessagingTransport(
      xpc, 'http://g.com:80', undefined /* opt_domHelper */,
      false /* opt_oneSidedHandshake */, 2 /* opt_protocolVersion */);
  assertEquals('http://g.com:80', t.peerHostname_);

  var t = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, undefined /* opt_domHelper */,
      false /* opt_oneSidedHandshake */, 2 /* opt_protocolVersion */);
  assertEquals('*', t.peerHostname_);
  t.dispose();
}


function testConstructorDom() {
  var xpc = getTestChannel();

  var t = new goog.net.xpc.NativeMessagingTransport(
      xpc, 'http://g.com:80', goog.dom.getDomHelper(),
      false /* opt_oneSidedHandshake */, 2 /* opt_protocolVersion */);
  assertEquals('http://g.com:80', t.peerHostname_);

  var t = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  assertEquals('*', t.peerHostname_);
  t.dispose();
}


function testDispose() {
  var xpc = getTestChannel();
  var listenedObj = window.postMessage ? window : document;

  var t0 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));
  t0.dispose();
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));

  var t1 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  t1.connect();
  t1.dispose();
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));

  var t2 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  var t3 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  t2.connect();
  t3.connect();
  t2.dispose();
  assertEquals(1, goog.events.removeAll(listenedObj, 'message'));
}


function testDisposeWithDom() {
  var xpc = getTestChannel(goog.dom.getDomHelper());
  var listenedObj = window.postMessage ? window : document;

  var t0 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));
  t0.dispose();
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));

  var t1 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  t1.connect();
  t1.dispose();
  assertEquals(0, goog.events.removeAll(listenedObj, 'message'));

  var t2 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  var t3 = new goog.net.xpc.NativeMessagingTransport(
      xpc, null /* peerHostName */, false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);
  t2.connect();
  t3.connect();
  t2.dispose();
  assertEquals(1, goog.events.removeAll(listenedObj, 'message'));
}


function testBogusMessages() {
  var e = createMockEvent('origin_unknown', 'bogus_message');
  assertFalse(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));

  e = createMockEvent('origin_unknown', 'bogus|message');
  assertFalse(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));

  e = createMockEvent('origin_unknown', 'bogus|message:data');
  assertFalse(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));
}


function testSendingMessagesToUnconnectedInnerPeer() {
  var xpc = getTestChannel();

  var serviceResult, payloadResult;
  xpc.xpcDeliver = function(service, payload) {
    serviceResult = service;
    payloadResult = payload;
  };

  // Construct an unconnected inner peer.
  xpc.getRole = function() { return goog.net.xpc.CrossPageChannelRole.INNER; };
  xpc.isConnected = function() { return false; };
  var t = new goog.net.xpc.NativeMessagingTransport(
      xpc, 'http://g.com', false /* opt_oneSidedHandshake */,
      2 /* opt_protocolVersion */);

  // Test a valid message.
  var e = createMockEvent(
      'origin_unknown', 'test_channel|test_service:test_payload');
  assertTrue(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));
  assertEquals('test_service', serviceResult);
  assertEquals('test_payload', payloadResult);
  assertEquals(
      'Ensure channel name has not been changed.', 'test_channel',
      t.channel_.name);

  // Test that sending a SETUP message from an untrusted origin doesn't update
  // the channel name.  This is a regression test for b/33746803.
  var e = createMockEvent('untrusted_origin', 'new_channel|tp:SETUP');
  assertFalse(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));
  assertEquals(
      'Channel name should not change from untrusted origin', 'test_channel',
      t.channel_.name);

  // Test updating a stale inner peer.
  var e = createMockEvent('trusted_origin', 'new_channel|tp:SETUP');
  assertTrue(goog.net.xpc.NativeMessagingTransport.messageReceived_(e));
  assertEquals('tp', serviceResult);
  assertEquals('SETUP', payloadResult);
  assertEquals(
      'Ensure channel name has been updated.', 'new_channel', t.channel_.name);
  t.dispose();
}


function testSignalConnected_innerFrame() {
  checkSignalConnected(false /* oneSidedHandshake */, true /* innerFrame */);
}


function testSignalConnected_outerFrame() {
  checkSignalConnected(false /* oneSidedHandshake */, false /* innerFrame */);
}


function testSignalConnected_singleSided_innerFrame() {
  checkSignalConnected(true /* oneSidedHandshake */, true /* innerFrame */);
}


function testSignalConnected_singleSided_outerFrame() {
  checkSignalConnected(true /* oneSidedHandshake */, false /* innerFrame */);
}


function checkSignalConnected(
    oneSidedHandshake, innerFrame, peerProtocolVersion, protocolVersion) {
  var xpc = getTestChannel();
  var connected = false;
  xpc.notifyConnected = function() {
    if (connected) {
      fail();
    } else {
      connected = true;
    }
  };
  xpc.getRole = function() {
    return innerFrame ? goog.net.xpc.CrossPageChannelRole.INNER :
                        goog.net.xpc.CrossPageChannelRole.OUTER;
  };
  xpc.isConnected = function() { return false; };

  var transport = new goog.net.xpc.NativeMessagingTransport(
      xpc, 'http://g.com', undefined /* opt_domHelper */,
      oneSidedHandshake /* opt_oneSidedHandshake */, 2 /* protocolVerion */);
  var sentPayloads = [];
  transport.send = function(service, payload) {
    assertEquals(goog.net.xpc.TRANSPORT_SERVICE_, service);
    sentPayloads.push(payload);
  };
  function assertSent(payloads) {
    assertArrayEquals(payloads, sentPayloads);
    sentPayloads = [];
  }
  var endpointId = transport.endpointId_;
  var peerEndpointId1 = 'abc123';
  var peerEndpointId2 = 'def234';

  assertFalse(connected);
  if (!oneSidedHandshake || innerFrame) {
    transport.transportServiceHandler(
        goog.net.xpc.SETUP_NTPV2 + ',' + peerEndpointId1);
    transport.transportServiceHandler(goog.net.xpc.SETUP);
    assertSent([goog.net.xpc.SETUP_ACK_NTPV2]);
    assertFalse(connected);
    transport.transportServiceHandler(goog.net.xpc.SETUP_ACK_NTPV2);
    assertSent([]);
    assertTrue(connected);
  } else {
    transport.transportServiceHandler(goog.net.xpc.SETUP_ACK_NTPV2);
    assertSent([]);
    assertFalse(connected);
    transport.transportServiceHandler(
        goog.net.xpc.SETUP_NTPV2 + ',' + peerEndpointId1);
    transport.transportServiceHandler(goog.net.xpc.SETUP);
    assertSent([goog.net.xpc.SETUP_ACK_NTPV2]);
    assertTrue(connected);
  }

  // Verify that additional transport service traffic doesn't cause duplicate
  // notifications.
  transport.transportServiceHandler(
      goog.net.xpc.SETUP_NTPV2 + ',' + peerEndpointId1);
  transport.transportServiceHandler(goog.net.xpc.SETUP);
  assertSent([goog.net.xpc.SETUP_ACK_NTPV2]);
  transport.transportServiceHandler(goog.net.xpc.SETUP_ACK_NTPV2);
  assertSent([]);

  // Simulate a reconnection by sending a SETUP message from a frame with a
  // different endpoint id.  No further connection callbacks should fire, but
  // a new SETUP message should be triggered.
  transport.transportServiceHandler(
      goog.net.xpc.SETUP_NTPV2 + ',' + peerEndpointId2);
  transport.transportServiceHandler(goog.net.xpc.SETUP);
  assertSent([
    goog.net.xpc.SETUP_ACK_NTPV2, goog.net.xpc.SETUP_NTPV2 + ',' + endpointId
  ]);
  transport.transportServiceHandler(goog.net.xpc.SETUP_ACK_NTPV2);
  assertSent([]);
}


/**
 * Creates a Mock Event object used to test browser events.
 * @param {string} origin The URI origin, or '*', of the event.
 * @param {string} data The data to associate with the event.
 * @return {Object} The created object representing a browser event.
 */
function createMockEvent(origin, data) {
  var event = {};
  event.getBrowserEvent = function() {
    return {origin: origin, data: data};
  };
  return event;
}


function getTestChannel(opt_domHelper) {
  var cfg = {};
  cfg[goog.net.xpc.CfgFields.CHANNEL_NAME] = 'test_channel';
  cfg[goog.net.xpc.CfgFields.PEER_HOSTNAME] = 'trusted_origin';
  return new goog.net.xpc.CrossPageChannel(
      cfg, opt_domHelper, undefined /* opt_domHelper */,
      false /* opt_oneSidedHandshake */, 2 /* opt_protocolVersion */);
}
