'use strict';

// Run a set of tests for a given |sensorName|.
// |readingData| is an object with 3 keys, all of which are arrays of arrays:
// 1. "readings". Each value corresponds to one raw reading that will be
//    processed by a sensor.
// 2. "expectedReadings". Each value corresponds to the processed value a
//    sensor will make available to users (i.e. a capped or rounded value).
//    Its length must match |readings|'.
// 3. "expectedRemappedReadings" (optional). Similar to |expectedReadings|, but
//    used only by spatial sensors, whose reference frame can change the values
//    returned by a sensor.
//    Its length should match |readings|'.
// |verificationFunction| is called to verify that a given reading matches a
// value in |expectedReadings|.
// |featurePolicies| represents |sensorName|'s associated sensor feature name.

function runGenericSensorTests(sensorName,
                               readingData,
                               verificationFunction,
                               featurePolicies) {
  const sensorType = self[sensorName];

  function validateReadingFormat(data) {
    return Array.isArray(data) && data.every(element => Array.isArray(element));
  }

  const { readings, expectedReadings, expectedRemappedReadings } = readingData;
  if (!validateReadingFormat(readings)) {
    throw new TypeError('readingData.readings must be an array of arrays.');
  }
  if (!validateReadingFormat(expectedReadings)) {
    throw new TypeError('readingData.expectedReadings must be an array of ' +
                        'arrays.');
  }
  if (readings.length < expectedReadings.length) {
    throw new TypeError('readingData.readings\' length must be bigger than ' +
                        'or equal to readingData.expectedReadings\' length.');
  }
  if (expectedRemappedReadings &&
      !validateReadingFormat(expectedRemappedReadings)) {
    throw new TypeError('readingData.expectedRemappedReadings must be an ' +
                        'array of arrays.');
  }
  if (expectedRemappedReadings &&
      expectedReadings.length != expectedRemappedReadings.length) {
    throw new TypeError('readingData.expectedReadings and ' +
      'readingData.expectedRemappedReadings must have the same ' +
      'length.');
  }

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    sensorProvider.setGetSensorShouldFail(sensorName, true);
    const sensor = new sensorType;
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const event = await sensorWatcher.wait_for("error");

    assert_false(sensor.activated);
    assert_equals(event.error.name, 'NotReadableError');
  }, `${sensorName}: Test that onerror is sent when sensor is not supported.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    sensorProvider.setPermissionsDenied(sensorName, true);
    const sensor = new sensorType;
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const event = await sensorWatcher.wait_for("error");

    assert_false(sensor.activated);
    assert_equals(event.error.name, 'NotAllowedError');
  }, `${sensorName}: Test that onerror is sent when permissions are not\
 granted.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType({frequency: 560});
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setStartShouldFail(true);

    const event = await sensorWatcher.wait_for("error");

    assert_false(sensor.activated);
    assert_equals(event.error.name, 'NotReadableError');
  }, `${sensorName}: Test that onerror is send when start() call has failed.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType({frequency: 560});
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);

    await sensorWatcher.wait_for("activate");

    assert_less_than_equal(mockSensor.getSamplingFrequency(), 60);
    sensor.stop();
    assert_false(sensor.activated);
  }, `${sensorName}: Test that frequency is capped to allowed maximum.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const maxSupportedFrequency = 5;
    sensorProvider.setMaximumSupportedFrequency(maxSupportedFrequency);
    const sensor = new sensorType({frequency: 50});
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);

    await sensorWatcher.wait_for("activate");

    assert_equals(mockSensor.getSamplingFrequency(), maxSupportedFrequency);
    sensor.stop();
    assert_false(sensor.activated);
  }, `${sensorName}: Test that frequency is capped to the maximum supported\
 frequency.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const minSupportedFrequency = 2;
    sensorProvider.setMinimumSupportedFrequency(minSupportedFrequency);
    const sensor = new sensorType({frequency: -1});
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);

    await sensorWatcher.wait_for("activate");

    assert_equals(mockSensor.getSamplingFrequency(), minSupportedFrequency);
    sensor.stop();
    assert_false(sensor.activated);
  }, `${sensorName}: Test that frequency is limited to the minimum supported\
 frequency.`);

  promise_test(async t => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const iframe = document.createElement('iframe');
    iframe.allow = featurePolicies.join(' \'none\'; ') + ' \'none\';';
    iframe.srcdoc = '<script>' +
                    '  window.onmessage = message => {' +
                    '    if (message.data === "LOADED") {' +
                    '      try {' +
                    '        new ' + sensorName + '();' +
                    '        parent.postMessage("FAIL", "*");' +
                    '      } catch (e) {' +
                    '        parent.postMessage("PASS", "*");' +
                    '      }' +
                    '    }' +
                    '   };' +
                    '<\/script>';
    const iframeWatcher = new EventWatcher(t, iframe, "load");
    document.body.appendChild(iframe);
    await iframeWatcher.wait_for("load");
    iframe.contentWindow.postMessage('LOADED', '*');

    const windowWatcher = new EventWatcher(t, window, "message");
    const message = await windowWatcher.wait_for("message");
    assert_equals(message.data, 'PASS');
  }, `${sensorName}: Test that sensor cannot be constructed within iframe\
 disallowed to use feature policy.`);

  promise_test(async t => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const iframe = document.createElement('iframe');
    iframe.allow = featurePolicies.join(';') + ';';
    iframe.srcdoc = '<script>' +
                    '  window.onmessage = message => {' +
                    '    if (message.data === "LOADED") {' +
                    '      try {' +
                    '        new ' + sensorName + '();' +
                    '        parent.postMessage("PASS", "*");' +
                    '      } catch (e) {' +
                    '        parent.postMessage("FAIL", "*");' +
                    '      }' +
                    '    }' +
                    '   };' +
                    '<\/script>';
    const iframeWatcher = new EventWatcher(t, iframe, "load");
    document.body.appendChild(iframe);
    await iframeWatcher.wait_for("load");
    iframe.contentWindow.postMessage('LOADED', '*');

    const windowWatcher = new EventWatcher(t, window, "message");
    const message = await windowWatcher.wait_for("message");
    assert_equals(message.data, 'PASS');
  }, `${sensorName}: Test that sensor can be constructed within an iframe\
 allowed to use feature policy.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();
    assert_false(sensor.hasReading);

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    await sensorWatcher.wait_for("reading");
    const expected = new RingBuffer(expectedReadings).next().value;
    assert_true(verificationFunction(expected, sensor));
    assert_true(sensor.hasReading);

    sensor.stop();
    assert_true(verificationFunction(expected, sensor, /*isNull=*/true));
    assert_false(sensor.hasReading);
  }, `${sensorName}: Test that 'onreading' is called and sensor reading is\
 valid.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor1 = new sensorType();
    const sensorWatcher1 = new EventWatcher(t, sensor1, ["reading", "error"]);
    sensor1.start();

    const sensor2 = new sensorType();
    const sensorWatcher2 = new EventWatcher(t, sensor2, ["reading", "error"]);
    sensor2.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    await Promise.all([sensorWatcher1.wait_for("reading"),
                       sensorWatcher2.wait_for("reading")]);
    const expected = new RingBuffer(expectedReadings).next().value;
    // Reading values are correct for both sensors.
    assert_true(verificationFunction(expected, sensor1));
    assert_true(verificationFunction(expected, sensor2));

    // After first sensor stops its reading values are null,
    // reading values for the second sensor sensor remain.
    sensor1.stop();
    assert_true(verificationFunction(expected, sensor1, /*isNull=*/true));
    assert_true(verificationFunction(expected, sensor2));

    sensor2.stop();
    assert_true(verificationFunction(expected, sensor2, /*isNull=*/true));
  }, `${sensorName}: sensor reading is correct.`);

  // Tests that readings maps to expectedReadings correctly. Due to threshold
  // check and rounding some values might be discarded or changed.
  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    await mockSensor.setSensorReading(readings);

    for (let expectedReading of expectedReadings) {
      await sensorWatcher.wait_for("reading");
      assert_true(sensor.hasReading, "hasReading");
      assert_true(verificationFunction(expectedReading, sensor),
                                       "verification");
    }

    sensor.stop();
  }, `${sensorName}: Test that readings are all mapped to expectedReadings\
 correctly.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    await sensorWatcher.wait_for("reading");
    const cachedTimeStamp1 = sensor.timestamp;

    await sensorWatcher.wait_for("reading");
    const cachedTimeStamp2 = sensor.timestamp;

    assert_greater_than(cachedTimeStamp2, cachedTimeStamp1);
    sensor.stop();
  }, `${sensorName}: sensor timestamp is updated when time passes.`);

  sensor_test(async t => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    assert_false(sensor.activated);
    sensor.start();
    assert_false(sensor.activated);

    await sensorWatcher.wait_for("activate");
    assert_true(sensor.activated);

    sensor.stop();
    assert_false(sensor.activated);
  }, `${sensorName}: Test that sensor can be successfully created and its\
 states are correct.`);

  sensor_test(async t => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    sensor.start();
    sensor.start();

    await sensorWatcher.wait_for("activate");
    assert_true(sensor.activated);
    sensor.stop();
  }, `${sensorName}: no exception is thrown when calling start() on already\
 started sensor.`);

  sensor_test(async t => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
    sensor.start();

    await sensorWatcher.wait_for("activate");
    sensor.stop();
    sensor.stop();
    assert_false(sensor.activated);
  }, `${sensorName}: no exception is thrown when calling stop() on already\
 stopped sensor.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor = new sensorType();
    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
    sensor.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    const expectedBuffer = new RingBuffer(expectedReadings);
    await sensorWatcher.wait_for("reading");
    const expected1 = expectedBuffer.next().value;
    assert_true(sensor.hasReading);
    assert_true(verificationFunction(expected1, sensor));
    const timestamp = sensor.timestamp;
    sensor.stop();
    assert_false(sensor.hasReading);

    sensor.start();
    await sensorWatcher.wait_for("reading");
    assert_true(sensor.hasReading);
    // |readingData| may have a single reading/expectation value, and this
    // is the second reading we are getting. For that case, make sure we
    // also wrap around as if we had the same RingBuffer used in
    // generic_sensor_mocks.js.
    const expected2 = expectedBuffer.next().value;
    assert_true(verificationFunction(expected2, sensor));
    // Make sure that 'timestamp' is already initialized.
    assert_greater_than(timestamp, 0);
    // Check that the reading is updated.
    assert_greater_than(sensor.timestamp, timestamp);
    sensor.stop();
  }, `${sensorName}: Test that fresh reading is fetched on start().`);

//  TBD file a WPT issue: visibilityChangeWatcher times out.
//  sensor_test(async (t, sensorProvider) => {
//    assert_implements(sensorName in self, `${sensorName} is not supported.`);
//    const sensor = new sensorType();
//    const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
//    const visibilityChangeWatcher = new EventWatcher(t, document,
//                                                     "visibilitychange");
//    sensor.start();

//    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
//    mockSensor.setSensorReading(readings);

//    await sensorWatcher.wait_for("reading");
//    const expected = new RingBuffer(expectedReadings).next().value;
//    assert_true(verificationFunction(expected, sensor));
//    const cachedTimestamp1 = sensor.timestamp;

//    const win = window.open('', '_blank');
//    await visibilityChangeWatcher.wait_for("visibilitychange");
//    const cachedTimestamp2 = sensor.timestamp;

//    win.close();
//    sensor.stop();
//    assert_equals(cachedTimestamp1, cachedTimestamp2);
//  }, `${sensorName}: sensor readings can not be fired on the background tab.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);

    const fastSensor = new sensorType({ frequency: 60 });
    t.add_cleanup(() => { fastSensor.stop(); });
    let eventWatcher = new EventWatcher(t, fastSensor, "activate");
    fastSensor.start();

    // Wait for |fastSensor| to be activated so that the call to
    // getSamplingFrequency() below works.
    await eventWatcher.wait_for("activate");

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    // We need |fastSensorFrequency| because 60Hz might be higher than a sensor
    // type's maximum allowed frequency.
    const fastSensorFrequency = mockSensor.getSamplingFrequency();
    const slowSensorFrequency = fastSensorFrequency * 0.25;

    const slowSensor = new sensorType({ frequency: slowSensorFrequency });
    t.add_cleanup(() => { slowSensor.stop(); });
    eventWatcher = new EventWatcher(t, slowSensor, "activate");
    slowSensor.start();

    // Wait for |slowSensor| to be activated before we check if the mock
    // platform sensor's sampling frequency has changed.
    await eventWatcher.wait_for("activate");
    assert_equals(mockSensor.getSamplingFrequency(), fastSensorFrequency);

    // Now stop |fastSensor| and verify that the sampling frequency has dropped
    // to the one |slowSensor| had requested.
    fastSensor.stop();
    return t.step_wait(() => {
      return mockSensor.getSamplingFrequency() === slowSensorFrequency;
    }, "Sampling frequency has dropped to slowSensor's requested frequency");
  }, `${sensorName}: frequency hint works.`);

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);

    const sensor1 = new sensorType();
    const sensor2 = new sensorType();

    return new Promise((resolve, reject) => {
      sensor1.addEventListener('reading', () => {
        sensor2.addEventListener('activate', () => {
          try {
            assert_true(sensor1.activated);
            assert_true(sensor1.hasReading);
            assert_false(verificationFunction(null, sensor1, /*isNull=*/true));
            assert_not_equals(sensor1.timestamp, null);

            assert_true(sensor2.activated);
            assert_false(verificationFunction(null, sensor2, /*isNull=*/true));
            assert_not_equals(sensor2.timestamp, null);
          } catch (e) {
            reject(e);
          }
        }, { once: true });
        sensor2.addEventListener('reading', () => {
          try {
            assert_true(sensor2.activated);
            assert_true(sensor2.hasReading);
            assert_sensor_equals(sensor1, sensor2);
            resolve();
          } catch (e) {
            reject(e);
          }
        }, { once: true });
        sensor2.start();
      }, { once: true });
      sensor1.start();
    });
  }, `${sensorName}: Readings delivered by shared platform sensor are\
 immediately accessible to all sensors.`);

//  Re-enable after https://github.com/w3c/sensors/issues/361 is fixed.
//  test(() => {
//     assert_throws_dom("NotSupportedError",
//         () => { new sensorType({invalid: 1}) });
//     assert_throws_dom("NotSupportedError",
//         () => { new sensorType({frequency: 60, invalid: 1}) });
//     if (!expectedRemappedReadings) {
//       assert_throws_dom("NotSupportedError",
//           () => { new sensorType({referenceFrame: "screen"}) });
//     }
//  }, `${sensorName}: throw 'NotSupportedError' for an unsupported sensor\
// option.`);

  test(() => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const invalidFreqs = [
      "invalid",
      NaN,
      Infinity,
      -Infinity,
      {}
    ];
    invalidFreqs.map(freq => {
      assert_throws_js(TypeError,
                       () => { new sensorType({frequency: freq}) },
                       `when freq is ${freq}`);
    });
  }, `${sensorName}: throw 'TypeError' if frequency is invalid.`);

  if (!expectedRemappedReadings) {
    // The sensorType does not represent a spatial sensor.
    return;
  }

  sensor_test(async (t, sensorProvider) => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const sensor1 = new sensorType({frequency: 60});
    const sensor2 = new sensorType({frequency: 60, referenceFrame: "screen"});
    const sensorWatcher1 = new EventWatcher(t, sensor1, ["reading", "error"]);
    const sensorWatcher2 = new EventWatcher(t, sensor1, ["reading", "error"]);

    sensor1.start();
    sensor2.start();

    const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
    mockSensor.setSensorReading(readings);

    await Promise.all([sensorWatcher1.wait_for("reading"),
                       sensorWatcher2.wait_for("reading")]);

    const expected = new RingBuffer(expectedReadings).next().value;
    const expectedRemapped =
        new RingBuffer(expectedRemappedReadings).next().value;
    assert_true(verificationFunction(expected, sensor1));
    assert_true(verificationFunction(expectedRemapped, sensor2));

    sensor1.stop();
    assert_true(verificationFunction(expected, sensor1, /*isNull=*/true));
    assert_true(verificationFunction(expectedRemapped, sensor2));

    sensor2.stop();
    assert_true(verificationFunction(expectedRemapped, sensor2,
                                     /*isNull=*/true));
  }, `${sensorName}: sensor reading is correct when options.referenceFrame\
 is 'screen'.`);

  test(() => {
    assert_implements(sensorName in self, `${sensorName} is not supported.`);
    const invalidRefFrames = [
      "invalid",
      null,
      123,
      {},
      "",
      true
    ];
    invalidRefFrames.map(refFrame => {
      assert_throws_js(TypeError,
                       () => { new sensorType({referenceFrame: refFrame}) },
                       `when refFrame is ${refFrame}`);
    });
  }, `${sensorName}: throw 'TypeError' if referenceFrame is not one of\
 enumeration values.`);
}

function runGenericSensorInsecureContext(sensorName) {
  test(() => {
    assert_false(sensorName in window, `${sensorName} must not be exposed`);
  }, `${sensorName} is not exposed in an insecure context.`);
}