<!DOCTYPE HTML>
<html>
<head>
  <title>Test UDPSocket API</title>
  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<iframe id="iframe"></iframe>
<pre id="test">
<script type="application/javascript">
'use strict';
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("untriaged");

const HELLO_WORLD = 'hlo wrld. ';
const DATA_ARRAY = [0, 255, 254, 0, 1, 2, 3, 0, 255, 255, 254, 0];
const DATA_ARRAY_BUFFER = new ArrayBuffer(DATA_ARRAY.length);
const TYPED_DATA_ARRAY = new Uint8Array(DATA_ARRAY_BUFFER);
const BIG_ARRAY = new Array(4096);
const BIG_ARRAY_BUFFER = new ArrayBuffer(BIG_ARRAY.length);
const BIG_TYPED_ARRAY = new Uint8Array(BIG_ARRAY_BUFFER);

for (let i = 0; i < BIG_ARRAY.length; i++) {
  BIG_ARRAY[i] = Math.floor(Math.random() * 256);
}

TYPED_DATA_ARRAY.set(DATA_ARRAY);
BIG_TYPED_ARRAY.set(BIG_ARRAY);

function is_same_buffer(recv_data, expect_data) {
  let recv_dataview = new Uint8Array(recv_data);
  let expected_dataview = new Uint8Array(expect_data);

  if (recv_dataview.length !== expected_dataview.length) {
    return false;
  }

  for (let i = 0; i < recv_dataview.length; i++) {
    if (recv_dataview[i] != expected_dataview[i]) {
      info('discover byte differenct at ' + i);
      return false;
    }
  }
  return true;
}

function testOpen() {
  info('test for creating an UDP Socket');
  let socket = new UDPSocket();
  is(socket.localPort, null, 'expect no local port before socket opened');
  is(socket.localAddress, null, 'expect no local address before socket opened');
  is(socket.remotePort, null, 'expected no default remote port');
  is(socket.remoteAddress, null, 'expected no default remote address');
  is(socket.readyState, 'opening', 'expected ready state = opening');
  is(socket.loopback, false, 'expected no loopback');
  is(socket.addressReuse, true, 'expect to reuse address');

  return socket.opened.then(function() {
    ok(true, 'expect openedPromise to be resolved after successful socket binding');
    ok(!(socket.localPort === 0), 'expect allocated a local port');
    is(socket.localAddress, '0.0.0.0', 'expect assigned to default address');
    is(socket.readyState, 'open', 'expected ready state = open');

    return socket;
  });
}

function testSendString(socket) {
  info('test for sending string data');

  socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    socket.addEventListener('message', function(msg) {
      let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data));
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      is(recvData, HELLO_WORLD, 'expected same string data');
      resolve(socket);
    }, {once: true});
  });
}

function testSendArrayBuffer(socket) {
  info('test for sending ArrayBuffer');

  socket.send(DATA_ARRAY_BUFFER, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    socket.addEventListener('message', function(msg) {
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      ok(is_same_buffer(msg.data, DATA_ARRAY_BUFFER), 'expected same buffer data');
      resolve(socket);
    }, {once: true});
  });
}

function testSendArrayBufferView(socket) {
  info('test for sending ArrayBufferView');

  socket.send(TYPED_DATA_ARRAY, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    socket.addEventListener('message', function(msg) {
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      ok(is_same_buffer(msg.data, TYPED_DATA_ARRAY), 'expected same buffer data');
      resolve(socket);
    }, {once: true});
  });
}

function testSendBlob(socket) {
  info('test for sending Blob');

  let blob = new Blob([HELLO_WORLD], {type : 'text/plain'});
  socket.send(blob, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    socket.addEventListener('message', function(msg) {
      let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data));
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      is(recvData, HELLO_WORLD, 'expected same string data');
      resolve(socket);
    }, {once: true});
  });
}

function testSendBigArray(socket) {
  info('test for sending Big ArrayBuffer');

  socket.send(BIG_TYPED_ARRAY, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    let byteReceived = 0;
    socket.addEventListener('message', function recv_callback(msg) {
      let byteBegin = byteReceived;
      byteReceived += msg.data.byteLength;
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']');
      if (byteReceived >= BIG_TYPED_ARRAY.length) {
        socket.removeEventListener('message', recv_callback);
        resolve(socket);
      }
    });
  });
}

function testSendBigBlob(socket) {
  info('test for sending Big Blob');

  let blob = new Blob([BIG_TYPED_ARRAY]);
  socket.send(blob, '127.0.0.1', socket.localPort);

  return new Promise(function(resolve, reject) {
    let byteReceived = 0;
    socket.addEventListener('message', function recv_callback(msg) {
      let byteBegin = byteReceived;
      byteReceived += msg.data.byteLength;
      is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
      ok(is_same_buffer(msg.data, BIG_TYPED_ARRAY.subarray(byteBegin, byteReceived)), 'expected same buffer data [' + byteBegin+ '-' + byteReceived + ']');
      if (byteReceived >= BIG_TYPED_ARRAY.length) {
        socket.removeEventListener('message', recv_callback);
        resolve(socket);
      }
    });
  });
}

function testUDPOptions(socket) {
  info('test for UDP init options');

  let remoteSocket = new UDPSocket({addressReuse: false,
                                    loopback: true,
                                    localAddress: '127.0.0.1',
                                    remoteAddress: '127.0.0.1',
                                    remotePort: socket.localPort});
  is(remoteSocket.localAddress, '127.0.0.1', 'expected local address');
  is(remoteSocket.remoteAddress, '127.0.0.1', 'expected remote address');
  is(remoteSocket.remotePort, socket.localPort, 'expected remote port');
  is(remoteSocket.addressReuse, false, 'expected address not reusable');
  is(remoteSocket.loopback, true, 'expected loopback mode is on');

  return remoteSocket.opened.then(function() {
    remoteSocket.send(HELLO_WORLD);
    return new Promise(function(resolve, reject) {
      socket.addEventListener('message', function(msg) {
        let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data));
        is(msg.remotePort, remoteSocket.localPort, 'expected packet from ' + remoteSocket.localPort);
        is(recvData, HELLO_WORLD, 'expected same string data');
        resolve(socket);
      }, {once: true});
    });
  });
}

function testClose(socket) {
  info('test for close');

  socket.close();
  is(socket.readyState, 'closed', 'expect ready state to be "closed"');
  try {
    socket.send(HELLO_WORLD, '127.0.0.1', socket.localPort);
    ok(false, 'unexpect to send successfully');
  } catch (e) {
    ok(true, 'expected send fail after socket closed');
  }

  return socket.closed.then(function() {
    ok(true, 'expected closedPromise is resolved after socket.close()');
  });
}

function testMulticast() {
  info('test for multicast');

  let socket = new UDPSocket({loopback: true});

  const MCAST_ADDRESS = '224.0.0.255';
  socket.joinMulticastGroup(MCAST_ADDRESS);

  return socket.opened.then(function() {
    socket.send(HELLO_WORLD, MCAST_ADDRESS, socket.localPort);

    return new Promise(function(resolve, reject) {
      socket.addEventListener('message', function(msg) {
        let recvData= String.fromCharCode.apply(null, new Uint8Array(msg.data));
        is(msg.remotePort, socket.localPort, 'expected packet from ' + socket.localPort);
        is(recvData, HELLO_WORLD, 'expected same string data');
        socket.leaveMulticastGroup(MCAST_ADDRESS);
        resolve();
      }, {once: true});
    });
  });
}

function testInvalidUDPOptions() {
  info('test for invalid UDPOptions');
  try {
    new UDPSocket({localAddress: 'not-a-valid-address'});
    ok(false, 'should not create an UDPSocket with an invalid localAddress');
  } catch (e) {
    is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localAddress is not a valid IPv4/6 address');
  }

  try {
    new UDPSocket({localPort: 0});
    ok(false, 'should not create an UDPSocket with an invalid localPort');
  } catch (e) {
    is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number');
  }

  try {
    new UDPSocket({remotePort: 0});
    ok(false, 'should not create an UDPSocket with an invalid remotePort');
  } catch (e) {
    is(e.name, 'InvalidAccessError', 'expected InvalidAccessError will be thrown if localPort is not a valid port number');
  }
}

function testOpenFailed() {
  info('test for falied on open');

  //according to RFC5737, address block 192.0.2.0/24 should not be used in both local and public contexts
  let socket = new UDPSocket({localAddress: '192.0.2.0'});

  return socket.opened.then(function() {
    ok(false, 'should not resolve openedPromise while fail to bind socket');
    socket.close();
  }).catch(function(reason) {
    is(reason.name, 'NetworkError', 'expected openedPromise to be rejected while fail to bind socket');
  });
}

function testSendBeforeOpen() {
  info('test for send before open');

  let socket = new UDPSocket();

  try {
    socket.send(HELLO_WORLD, '127.0.0.1', 9);
    ok(false, 'unexpect to send successfully');
  } catch (e) {
    ok(true, 'expected send fail before openedPromise is resolved');
  }

  return socket.opened.then(function() {
    socket.close();
  });
}

function testCloseBeforeOpened() {
  info('test for close socket before opened');

  let socket = new UDPSocket();
  socket.opened.then(function() {
    ok(false, 'should not resolve openedPromise if it has already been closed');
  }).catch(function(reason) {
    is(reason.name, 'AbortError', 'expected openedPromise to be rejected while socket is closed during opening');
  });

  return socket.close().then(function() {
    ok(true, 'expected closedPromise to be resolved');
  }).then(socket.opened);
}

function testOpenWithoutClose() {
  info('test for open without close');

  let closed = [];
  for (let i = 0; i < 50; i++) {
    let socket = new UDPSocket();
    closed.push(socket.closed);
  }

  SpecialPowers.gc();
  info('all unrefereced socket should be closed right after GC');

  return Promise.all(closed);
}

function awaitEvent(target, event) {
  return new Promise(resolve => {
    target.addEventListener(event, resolve, { once: true });
  });
}

async function testBFCache() {
  info('test for bfcache behavior');

  let socket = new UDPSocket();

  await socket.opened;

  let win = window.open(`file_udpsocket_iframe.html?${socket.localPort}`);

  let msg = await awaitEvent(socket, "message");

  win.location = "file_postMessage_opener.html";
  await awaitEvent(window, "message");

  socket.send(HELLO_WORLD, '127.0.0.1', msg.remotePort);

  await new Promise(resolve => {
    function recv_again_callback(message) {
      socket.removeEventListener('message', recv_again_callback);
      ok(false, 'should not receive packet after page unload');
    }

    socket.addEventListener('message', recv_again_callback);

    setTimeout(function() {
      socket.removeEventListener('message', recv_again_callback);
      socket.close();
      resolve();
    }, 5000);
  });

  win.close();
}

function runTest() {
  testOpen()
  .then(testSendString)
  .then(testSendArrayBuffer)
  .then(testSendArrayBufferView)
  .then(testSendBlob)
  .then(testSendBigArray)
  .then(testSendBigBlob)
  .then(testUDPOptions)
  .then(testClose)
  .then(testMulticast)
  .then(testInvalidUDPOptions)
  .then(testOpenFailed)
  .then(testSendBeforeOpen)
  .then(testCloseBeforeOpened)
  .then(testOpenWithoutClose)
  .then(testBFCache)
  .then(function() {
    info('test finished');
    SimpleTest.finish();
  })
  .catch(function(err) {
    ok(false, 'test failed due to: ' + err);
    SimpleTest.finish();
  });
}

window.addEventListener('load', function () {
  SpecialPowers.pushPrefEnv({
    'set': [
      ['dom.udpsocket.enabled', true],
      ['browser.sessionhistory.max_total_viewers', 10]
    ]
  }, runTest);
});

</script>
</pre>
</body>
</html>
