// With autosave on, the map must be posted once per settle and once per drag,
// never once per mouse movement.
//
// force-graph reheats the simulation on every drag event. Once the first
// layout has finished, alpha is already below d3AlphaMin, so the engine stops
// again immediately having run no ticks at all. Hanging the save off
// onEngineStop therefore fires it once per mousemove, each post carrying the
// whole map.
//
// So two things are needed together. A tick count tells a real settle apart
// from a stop that did no work, and onNodeDragEnd is what persists a node the
// user moved by hand, since the stop that follows a drag runs no ticks.
//
// That second half restores parity rather than adding anything: the old d3
// renderer saved a dragged node too. Its save hangs off the same spinner loop,
// which netmap.tt re-arms on every force start event, so a drag restarts the
// force and the save follows when it settles. An earlier version of this
// comment claimed the old renderer saved nothing, on the strength of one run
// that sampled a POST count after a fixed wait; three instrumented runs across
// two map sizes disagree, each showing one force restart and one post. A
// single missing request is not evidence that none was coming.
//
// A success handler here only runs at all because the route sets its own
// content type; inheriting Dancer::Plugin::Ajax's text/xml default makes jQuery
// reject a 200 as invalid XML. That is asserted in
// xt/36-ajax-content-response.t. Do not add a dataType override here to paper
// over it if the route ever regresses.
//
// The same distinction decides who gets told. With autosave on the map saves
// itself at every settle and every drag, so a toast for each would be noise.
// The sidebar Save button is the one case where the user asked for a save and
// has nothing else to confirm it happened, so it announces, and only when
// autosave is off.

'use strict';

const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const repoRoot = path.join(__dirname, '..', '..');
const netmapJs = () => fs.readFileSync(
  path.join(repoRoot, 'share', 'views', 'js', 'netmap.js'), 'utf8');
const deviceJs = () => fs.readFileSync(
  path.join(repoRoot, 'share', 'views', 'js', 'device.js'), 'utf8');

// balanced to the handler's own closing paren, so an assertion cannot pass by
// matching text that belongs to a different handler. A line-indent heuristic
// was tried first and cut the onEngineStop body at its own fg.graphData() call.
const handlerBody = (src, name) => {
  const at = src.indexOf('.' + name + '(');
  assert.notEqual(at, -1, `netmap.js must register ${name}`);
  const open = src.indexOf('(', at);
  let depth = 0;
  for (let i = open; i < src.length; i++) {
    if (src[i] === '(') { depth++ }
    else if (src[i] === ')') { depth--; if (depth === 0) { return src.slice(open, i + 1) } }
  }
  assert.fail(`unbalanced parentheses after ${name} in netmap.js`);
};

describe('netmap autosave', () => {
  test('engineTick__while_the_simulation_runs__increments_a_counter', () => {
    const body = handlerBody(netmapJs(), 'onEngineTick');
    assert.match(
      body,
      /\+\+/,
      'the tick handler must count, or there is no way to tell a settle from an engine ' +
      'stop that ran nothing, and force-graph produces one of those per drag event. ' +
      'Count inside the existing handler: onEngineTick is a setter, so registering a ' +
      'second one would silently replace the spinner reset'
    );
  });

  test('engineStop__having_run_no_ticks__does_not_post_the_map', () => {
    const body = handlerBody(netmapJs(), 'onEngineStop');
    assert.match(
      body,
      /saveMapPositions/,
      'the settle after the first layout is still what saves it'
    );
    assert.match(
      body,
      /ticks[\s\S]{0,300}?saveMapPositions/i,
      'the save in onEngineStop must be guarded by the tick count, or every drag event ' +
      'posts the whole map again'
    );
  });

  test('nodeDragEnd__with_autosave_on__posts_the_map_once', () => {
    const body = handlerBody(netmapJs(), 'onNodeDragEnd');
    assert.match(
      body,
      /saveMapPositions/,
      'the engine stop after a drag runs no ticks, so without a save here a node the ' +
      'user moved by hand is never persisted, which the renderer this replaced did ' +
      'do'
    );
  });
});

describe('netmap autosave toggle', () => {
  // Nothing re-fetches the netmap fragment when the toggle is clicked, so a
  // value baked in at render time cannot follow it.
  test('autosaveOn__after_the_sidebar_toggle__is_read_live_not_baked_at_render', () => {
    const src = netmapJs();
    assert.doesNotMatch(
      src,
      /autosaveOn\s*=\s*\('\[%/,
      'autosave must not be baked from a template param: the toggle changes the ' +
      'checkbox without re-rendering this fragment, so a baked value cannot follow it'
    );
    assert.match(
      src,
      /getElementById\(\s*'nd_autosave'\s*\)/,
      'the autosave state must be read from the sidebar checkbox, the same way ' +
      'showips and showspeed already are in this file'
    );
  });
});

describe('netmap manual save', () => {
  test('saveMapPositions__called_by_the_button_with_autosave_off__announces', () => {
    const src = netmapJs();
    const at = src.indexOf('saveMapPositions = function');
    assert.notEqual(at, -1, 'netmap.js must define saveMapPositions');
    const body = src.slice(at, src.indexOf('\n  };', at));
    assert.match(
      body,
      /toastr\.success/,
      'a manual save has nothing else to confirm it happened, so it must say so'
    );
    assert.match(
      body,
      /if\s*\([^)]*!\s*autosaveOn[^)]*\)[\s\S]{0,120}?toastr\.success/,
      'the toast must be suppressed when autosave is on, or the map announces itself ' +
      'at every settle and every drag'
    );
    assert.match(
      body,
      /function\s*\(\s*\w+\s*\)/,
      'saveMapPositions must take the caller\'s intent as an argument: the autosave ' +
      'calls and the button call are otherwise indistinguishable from inside it'
    );
  });

  test('saveButton__on_click__tells_saveMapPositions_it_was_the_user', () => {
    const src = deviceJs();
    const at = src.indexOf("'#nd_netmap-save'");
    assert.notEqual(at, -1, 'device.js must bind the netmap Save button');
    const handler = src.slice(at, at + 260);
    assert.match(
      handler,
      /saveMapPositions\(\s*\w+/,
      'the button must pass an argument, or netmap.js cannot tell a click from the ' +
      'saves autosave makes on its own'
    );
  });
});
