// Step definitions for testing map matching API functionality

import util from 'util';
import polyline from '@mapbox/polyline';
import { When } from '@cucumber/cucumber';

When(/^I match I should get$/, async function (table) {
  let got;

  await this.reprocessAndLoadData();
  const testRow = function (row, ri) {
    return new Promise((resolve, reject) => {
      const afterRequest = function (err, res, body) {
        if (err) return reject(err);
        let json;

        const headers = new Set(table.raw()[0]);

        got.code = 'unknown';
        if (body.length) {
          json = JSON.parse(body);
          got.code = json.code;
        }

        if (headers.has('status')) {
          got.status = json.status.toString();
        }

        if (headers.has('message')) {
          got.message = json.status_message;
        }

        if (headers.has('#')) {
          // comment column
          got['#'] = row['#'];
        }

        let subMatchings = [''],
          turns = '',
          route = '',
          duration = '',
          annotation = '',
          geometry = '',
          alternatives = '';

        if (res.statusCode === 200) {
          if (headers.has('matchings')) {
            subMatchings = [];

            // find the first matched
            let start_index = 0;
            while (
              start_index < json.tracepoints.length &&
              json.tracepoints[start_index] === null
            )
              start_index++;

            let sub = [];
            let prev_index = null;
            for (let i = start_index; i < json.tracepoints.length; i++) {
              if (json.tracepoints[i] === null) continue;

              const current_index = json.tracepoints[i].matchings_index;

              if (prev_index !== current_index) {
                if (sub.length > 0) subMatchings.push(sub);
                sub = [];
                prev_index = current_index;
              }

              sub.push(json.tracepoints[i].location);
            }
            subMatchings.push(sub);
          }

          if (headers.has('turns')) {
            if (json.matchings.length != 1)
              throw new Error(
                '*** Checking turns only supported for matchings with one subtrace',
              );
            turns = this.turnList(json.matchings[0]);
          }

          if (headers.has('route')) {
            if (json.matchings.length != 1)
              throw new Error(
                '*** Checking route only supported for matchings with one subtrace',
              );
            route = this.wayList(json.matchings[0]);
          }

          if (headers.has('duration')) {
            if (json.matchings.length != 1)
              throw new Error(
                '*** Checking duration only supported for matchings with one subtrace',
              );
            duration = json.matchings[0].duration;
          }

          // annotation response values are requested by 'a:{type_name}'
          let found = false;
          headers.forEach((h) => {
            if (h.match(/^a:/)) found = true;
          });
          if (found) {
            if (json.matchings.length != 1)
              throw new Error(
                '*** Checking annotation only supported for matchings with one subtrace',
              );
            annotation = this.annotationList(json.matchings[0]);
          }

          if (headers.has('geometry')) {
            if (json.matchings.length != 1)
              throw new Error(
                '*** Checking geometry only supported for matchings with one subtrace',
              );
            geometry = json.matchings[0].geometry;
          }

          if (headers.has('alternatives')) {
            alternatives = this.alternativesList(json);
          }
        }

        if (headers.has('turns')) {
          got.turns = turns;
        }

        if (headers.has('route')) {
          got.route = route;
        }

        if (headers.has('duration')) {
          got.duration = duration.toString();
        }

        if (headers.has('data_version')) {
          got.data_version = json.data_version || '';
        }

        // if header matches 'a:*', parse out the values for *
        // and return in that header
        headers.forEach((k) => {
          const whitelist = [
            'duration',
            'distance',
            'datasources',
            'nodes',
            'weight',
          ];
          if (k.match(/^a:/)) {
            const a_type = k.slice(2);
            if (whitelist.indexOf(a_type) == -1)
              return reject(
                new Error(`*** Unrecognized annotation field: ${a_type}`),
              );
            if (!annotation[a_type])
              return reject(
                new Error(`*** Annotation not found in response: ${a_type}`),
              );
            got[k] = annotation[a_type];
          }
        });

        if (headers.has('geometry')) {
          if (this.queryParams['geometries'] === 'polyline') {
            got.geometry = polyline.decode(geometry).toString();
          } else if (this.queryParams['geometries'] === 'polyline6') {
            got.geometry = polyline.decode(geometry, 6).toString();
          } else {
            got.geometry = geometry.coordinates;
          }
        }

        if (headers.has('OSM IDs')) {
          got['OSM IDs'] = OSMIDs;
        }

        if (headers.has('alternatives')) {
          got['alternatives'] = alternatives;
        }
        let ok = true;
        let encodedResult = '',
          extendedTarget = '';

        const testSubMatching = function (sub, si) {
          const testSubNode = function (ni) {
            const node = this.findNodeByName(sub[ni]),
              outNode = subMatchings[si][ni];

            if (this.FuzzyMatch.matchLocation(outNode, node)) {
              encodedResult += sub[ni];
              extendedTarget += sub[ni];
            } else {
              if (outNode != null) {
                encodedResult += util.format(
                  '? [%s,%s]',
                  outNode[0],
                  outNode[1],
                );
              } else {
                encodedResult += '?';
              }
              extendedTarget += util.format('%s [%d,%d]', node.lat, node.lon);
              ok = false;
            }
          }.bind(this);

          for (let i = 0; i < sub.length; i++) {
            testSubNode(i);
          }
        }.bind(this);

        if (headers.has('matchings')) {
          if (subMatchings.length != row.matchings.split(',').length) {
            return reject(
              new Error(
                '*** table matchings and api response are not the same',
              ),
            );
          }

          row.matchings.split(',').forEach((sub, si) => {
            testSubMatching(sub, si);
          });
        }

        if (headers.has('waypoints')) {
          const got_loc = [];
          const resultWaypoints = [];
          for (let i = 0; i < json.tracepoints.length; i++) {
            if (!json.tracepoints[i]) continue;
            if (json.tracepoints[i].waypoint_index != null)
              got_loc.push(json.tracepoints[i].location);
          }

          if (row.waypoints.length != got_loc.length)
            return reject(
              new Error(
                `Expected ${row.waypoints.length} waypoints, got ${got_loc.length}`,
              ),
            );

          for (let i = 0; i < row.waypoints.length; i++) {
            const want_node = this.findNodeByName(row.waypoints[i]);
            if (!this.FuzzyMatch.matchLocation(got_loc[i], want_node)) {
              resultWaypoints.push(
                util.format('? [%s,%s]', got_loc[i][0], got_loc[i][1]),
              );
              ok = false;
            } else {
              resultWaypoints.push(row.waypoints[i]);
            }
          }
        }

        if (ok) {
          if (headers.has('matchings')) {
            got.matchings = row.matchings;
          }

          if (headers.has('timestamps')) {
            got.timestamps = row.timestamps;
          }

          if (headers.has('waypoints')) {
            got.waypoints = row.waypoints;
          }
        } else {
          got.waypoints = resultWaypoints.join(';');
          got.matchings = encodedResult;
          row.matchings = extendedTarget;
        }

        resolve(got);
      }.bind(this);

      if (row.request) {
        got = {};
        got.request = row.request;
        this.requestUrl(row.request, afterRequest);
      } else {
        const params = this.queryParams;
        got = {};
        for (const k in row) {
          const match = k.match(/param:(.*)/);
          if (match) {
            if (row[k] === '(nil)') {
              params[match[1]] = null;
            } else if (row[k]) {
              params[match[1]] = [row[k]];
            }
            got[k] = row[k];
          }
        }

        const trace = [];
        let timestamps = [];

        if (row.trace) {
          for (let i = 0; i < row.trace.length; i++) {
            const n = row.trace[i],
              node = this.findNodeByName(n);
            if (!node)
              throw new Error(util.format('*** unknown waypoint node "%s"', n));
            trace.push(node);
          }
          if (row.timestamps) {
            timestamps = row.timestamps
              .split(' ')
              .filter((s) => !!s)
              .map((t) => parseInt(t, 10));
          }
          got.trace = row.trace;
          this.requestMatching(trace, timestamps, params, afterRequest);
        } else {
          return reject(new Error('*** no trace'));
        }
      }
    });
  }.bind(this);

  await this.processRowsAndDiff(table, testRow);
});
