// Test table service functionality with distance/duration matrices
import flatbuffers from 'flatbuffers';
import test from 'tape';
import { OSRM, FBResult, data_path, mld_data_path, three_test_coordinates, two_test_coordinates } from './constants.js';

test('table: flatbuffer format', (assert) => {
  assert.plan(3);
  const osrm = new OSRM(data_path);
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    format: 'flatbuffers'
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table instanceof Buffer);
    const fb = FBResult.getRootAsFBResult(new flatbuffers.ByteBuffer(table));
    assert.ok(fb.table());

  });
});

test('table: test annotations paramater combination', (assert) => {
  assert.plan(12);
  const osrm = new OSRM(data_path);
  let options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    annotations: ['distance']
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table['distances'], 'distances table result should exist');
    assert.notOk(table['durations'], 'durations table result should not exist');
  });

  options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    annotations: ['duration']
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table['durations'], 'durations table result should exist');
    assert.notOk(table['distances'], 'distances table result should not exist');
  });

  options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    annotations: ['duration', 'distance']
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table['durations'], 'durations table result should exist');
    assert.ok(table['distances'], 'distances table result should exist');
  });

  options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]]
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table['durations'], 'durations table result should exist');
    assert.notOk(table['distances'], 'distances table result should not exist');
  });
});

test('table: returns buffer', (assert) => {
  assert.plan(3);
  const osrm = new OSRM(data_path);
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
  };
  osrm.table(options, { format: 'json_buffer' }, (err, table) => {
    assert.ifError(err);
    assert.ok(table instanceof Buffer);
    table = JSON.parse(table);
    assert.ok(table['durations'], 'distances table result should exist');
  });
});

test('table: throws on invalid snapping values', (assert) => {
  assert.plan(1);
  const osrm = new OSRM(data_path);
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    snapping: 'zing'
  };
  assert.throws(() => { osrm.table(options, (err, response) => { }); },
    /'snapping' param must be one of \[default, any\]/);
});

test('table: snapping parameter passed through OK', (assert) => {
  assert.plan(2);
  const osrm = new OSRM(data_path);
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    snapping: 'any'
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table['durations'], 'distances table result should exist');
  });
});

const tables = ['distances', 'durations'];

// Generate tests for both 'distances' and 'durations' table types
tables.forEach((annotation) => {
  test(`table: ${  annotation  } table in Monaco`, (assert) => {
    assert.plan(11);
    const osrm = new OSRM(data_path);
    const options = {
      coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, table) => {
      assert.ifError(err);
      assert.ok(Array.isArray(table[annotation]), 'result must be an array');
      const row_count = table[annotation].length;
      for (let i = 0; i < row_count; ++i) {
        const column = table[annotation][i];
        const column_count = column.length;
        assert.equal(row_count, column_count);
        for (let j = 0; j < column_count; ++j) {
          if (i == j) {
            // check that diagonal is zero
            assert.equal(0, column[j], 'diagonal must be zero');
          } else {
            // everything else is non-zero
            assert.notEqual(0, column[j], 'other entries must be non-zero');
            // and finite (not nan, inf etc.)
            assert.ok(Number.isFinite(column[j]), 'distance is finite number');
          }
        }
      }
      assert.equal(options.coordinates.length, row_count);
    });
  });

  test(`table: ${  annotation  } table in Monaco with sources/destinations`, (assert) => {
    assert.plan(7);
    const osrm = new OSRM(data_path);
    const options = {
      coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
      sources: [0],
      destinations: [0,1],
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, table) => {
      assert.ifError(err);
      assert.ok(Array.isArray(table[annotation]), 'result must be an array');
      const row_count = table[annotation].length;
      for (let i = 0; i < row_count; ++i) {
        const column = table[annotation][i];
        const column_count = column.length;
        assert.equal(options.destinations.length, column_count);
        for (let j = 0; j < column_count; ++j) {
          if (i == j) {
            // check that diagonal is zero
            assert.equal(0, column[j], 'diagonal must be zero');
          } else {
            // everything else is non-zero
            assert.notEqual(0, column[j], 'other entries must be non-zero');
            // and finite (not nan, inf etc.)
            assert.ok(Number.isFinite(column[j]), 'distance is finite number');
          }
        }
      }
      assert.equal(options.sources.length, row_count);
    });
  });

  test(`table: ${  annotation  } throws on invalid arguments`, (assert) => {
    assert.plan(18);
    const osrm = new OSRM(data_path);
    const options = {annotations: [annotation.slice(0,-1)]};
    assert.throws(() => { osrm.table(options); },
      /Two arguments required/);
    options.coordinates = null;
    assert.throws(() => { osrm.table(options, () => {}); },
      /Coordinates must be an array of \(lon\/lat\) pairs/);
    options.coordinates = [three_test_coordinates[0]];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /At least two coordinates must be provided/);
    options.coordinates = three_test_coordinates[0];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Coordinates must be an array of \(lon\/lat\) pairs/);
    options.coordinates = [three_test_coordinates[0][0], three_test_coordinates[0][1]];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Coordinates must be an array of \(lon\/lat\) pairs/);

    options.coordinates = two_test_coordinates;
    assert.throws(() => { osrm.table(options, { format: 'invalid' }, (err, response) => {}); },
      /format must be a string:/);

    options.sources = true;
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Sources must be an array of indices \(or undefined\)/);
    options.sources = [0, 4];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Source indices must be less than the number of coordinates/);
    options.sources = [0.3, 1.1];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Source must be an integer/);
    options.sources = [0, 1, 2];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Source indices must be less than the number of coordinates/);

    options.destinations = true;
    delete options.sources;
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Destinations must be an array of indices \(or undefined\)/);
    options.destinations = [0, 4];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Destination indices must be less than the number of coordinates/);
    options.destinations = [0.3, 1.1];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Destination must be an integer/);
    options.destinations = [0, 4];
    assert.throws(() => { osrm.table(options, (err, response) => {}); },
      /Destination indices must be less than the number of coordinates/);

    // does not throw: the following two have been changed in OSRM v5
    options.sources = [0, 1];
    delete options.destinations;
    assert.doesNotThrow(() => { osrm.table(options, (err, response) => {}); },
      /Both sources and destinations need to be specified/);
    options.destinations = [0, 1];
    assert.doesNotThrow(() => { osrm.table(options, (err, response) => {}); },
      /You can either specify sources and destinations, or coordinates/);

    assert.throws(() => { osrm.route({coordinates: two_test_coordinates, generate_hints: null}, (err, route) => {}); },
      /generate_hints must be of type Boolean/);
    assert.throws(() => { osrm.route({coordinates: two_test_coordinates, skip_waypoints: null}, (err, route) => {}); },
      /skip_waypoints must be of type Boolean/);
  });

  test('table: throws on invalid arguments', (assert) => {
    assert.plan(1);
    const osrm = new OSRM(data_path);
    assert.throws(() => { osrm.table(null, () => {}); },
      /First arg must be an object/);
  });

  test(`table: ${  annotation  } table in Monaco with hints`, (assert) => {
    assert.plan(5);
    const osrm = new OSRM(data_path);
    const options = {
      coordinates: two_test_coordinates,
      generate_hints: true,   // true is default but be explicit here
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, table) => {
      assert.ifError(err);

      // Helper function to verify waypoint contains hint data
      function assertHasHints(waypoint) {
        assert.notStrictEqual(waypoint.hint, undefined);
      }

      table.sources.map(assertHasHints);
      table.destinations.map(assertHasHints);
    });
  });

  test(`table: ${  annotation  } table in Monaco without hints`, (assert) => {
    assert.plan(5);
    const osrm = new OSRM(data_path);
    const options = {
      coordinates: two_test_coordinates,
      generate_hints: false,  // true is default
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, table) => {
      assert.ifError(err);

      // Helper function to verify waypoint has no hint data
      function assertHasNoHints(waypoint) {
        assert.strictEqual(waypoint.hint, undefined);
      }

      table.sources.map(assertHasNoHints);
      table.destinations.map(assertHasNoHints);
    });
  });

  test(`table: ${  annotation  } table in Monaco without waypoints`, (assert) => {
    assert.plan(2);
    const osrm = new OSRM(data_path);
    const options = {
      coordinates: two_test_coordinates,
      skip_waypoints: true,  // false is default
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, table) => {
      assert.strictEqual(table.sources, undefined);
      assert.strictEqual(table.destinations, undefined);
    });
  });

  test(`table: ${  annotation  } table in Monaco without motorways`, (assert) => {
    assert.plan(2);
    const osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});
    const options = {
      coordinates: two_test_coordinates,
      exclude: ['motorway'],
      annotations: [annotation.slice(0,-1)]
    };
    osrm.table(options, (err, response) => {
      assert.equal(response[annotation].length, 2);
      assert.strictEqual(response.fallback_speed_cells, undefined);
    });
  });

  test(`table: ${  annotation  } table in Monaco with fallback speeds`, (assert) => {
    assert.plan(2);
    const osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});
    const options = {
      coordinates: two_test_coordinates,
      annotations: [annotation.slice(0,-1)],
      fallback_speed: 1,
      fallback_coordinate: 'input'
    };
    osrm.table(options, (err, response) => {
      assert.equal(response[annotation].length, 2);
      assert.equal(response['fallback_speed_cells'].length, 0);
    });
  });

  test(`table: ${  annotation  } table in Monaco with invalid fallback speeds and fallback coordinates`, (assert) => {
    assert.plan(4);
    const osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});
    const options = {
      coordinates: two_test_coordinates,
      annotations: [annotation.slice(0,-1)],
      fallback_speed: -1
    };

    assert.throws(()=>osrm.table(options, (err, res) => {}), /fallback_speed must be > 0/, 'should throw on invalid fallback_speeds');

    options.fallback_speed = '10';
    assert.throws(()=>osrm.table(options, (err, res) => {}), /fallback_speed must be a number/, 'should throw on invalid fallback_speeds');

    options.fallback_speed = 10;
    options.fallback_coordinate = 'bla';
    assert.throws(()=>osrm.table(options, (err, res) => {}), /fallback_coordinate' param must be one of \[input, snapped\]/, 'should throw on invalid fallback_coordinate');

    options.fallback_coordinate = 10;
    assert.throws(()=>osrm.table(options, (err, res) => {}), /fallback_coordinate must be a string: \[input, snapped\]/, 'should throw on invalid fallback_coordinate');

  });

  test(`table: ${  annotation  } table in Monaco with invalid scale factor`, (assert) => {
    assert.plan(3);
    const osrm = new OSRM({path: mld_data_path, algorithm: 'MLD'});
    const options = {
      coordinates: two_test_coordinates,
      annotations: [annotation.slice(0,-1)],
      scale_factor: -1
    };

    assert.throws(()=>osrm.table(options, (err, res) => {}), /scale_factor must be > 0/, 'should throw on invalid scale_factor value');

    options.scale_factor = '-1';
    assert.throws(()=>osrm.table(options, (err, res) => {}), /scale_factor must be a number/, 'should throw on invalid scale_factor value');

    options.scale_factor = 0;
    assert.throws(()=>osrm.table(options, (err, res) => {}), /scale_factor must be > 0/, 'should throw on invalid scale_factor value');

  });
});

test('table: throws on disabled geometry', (assert) => {
  assert.plan(1);
  const osrm = new OSRM({'path': data_path, 'disable_feature_dataset': ['ROUTE_GEOMETRY']});
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
  };
  osrm.table(options, (err, table) => {
    console.log(err);
    assert.match(err.message, /DisabledDatasetException/);
  });
});

test('table: ok on disabled geometry', (assert) => {
  assert.plan(4);
  const osrm = new OSRM({'path': data_path, 'disable_feature_dataset': ['ROUTE_GEOMETRY']});
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
    skip_waypoints: true
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table.durations, 'distances table result should exist');
    assert.notok(table.sources);
    assert.notok(table.destinations);
  });
});

test('table: ok on disabled steps', (assert) => {
  assert.plan(4);
  const osrm = new OSRM({'path': data_path, 'disable_feature_dataset': ['ROUTE_STEPS']});
  const options = {
    coordinates: [three_test_coordinates[0], three_test_coordinates[1]],
  };
  osrm.table(options, (err, table) => {
    assert.ifError(err);
    assert.ok(table.durations, 'distances table result should exist');
    assert.ok(table.sources.length, 2);
    assert.ok(table.destinations.length, 2);
  });
});
