// Basic tests for the pumpingStation commands registry.
// Run with:  node --test test/basic/commands.basic.test.js

'use strict';

const test = require('node:test');
const assert = require('node:assert/strict');

const { createRegistry } = require('generalFunctions');
const commands = require('../../src/commands');

// --- helpers ---------------------------------------------------------------

function makeLogger() {
  const calls = { warn: [], error: [], info: [], debug: [] };
  return {
    calls,
    warn: (m) => calls.warn.push(String(m)),
    error: (m) => calls.error.push(String(m)),
    info: (m) => calls.info.push(String(m)),
    debug: (m) => calls.debug.push(String(m)),
  };
}

function makeSource({ mode = 'manual' } = {}) {
  const calls = {
    changeMode: [],
    calibratePredictedVolume: [],
    calibratePredictedLevel: [],
    setManualInflow: [],
    forwardDemandToChildren: [],
    registerChild: [],
  };
  const source = {
    mode,
    logger: makeLogger(),
    changeMode: (m) => calls.changeMode.push(m),
    calibratePredictedVolume: (v) => calls.calibratePredictedVolume.push(v),
    calibratePredictedLevel: (v) => calls.calibratePredictedLevel.push(v),
    setManualInflow: (v, ts, u) => calls.setManualInflow.push({ v, ts, u }),
    forwardDemandToChildren: async (d) => { calls.forwardDemandToChildren.push(d); },
    childRegistrationUtils: {
      registerChild: (childSource, position) =>
        calls.registerChild.push({ childSource, position }),
    },
  };
  return { source, calls };
}

function makeCtx({ child = null, logger = makeLogger() } = {}) {
  return {
    logger,
    RED: { nodes: { getNode: (id) => (child && child.id === id ? child : undefined) } },
    node: {},
    send: () => {},
  };
}

function makeRegistry(logger) {
  return createRegistry(commands, { logger });
}

// --- tests -----------------------------------------------------------------

test('canonical topics dispatch to their handlers', async () => {
  const { source, calls } = makeSource();
  const reg = makeRegistry(makeLogger());

  await reg.dispatch({ topic: 'set.mode', payload: 'levelbased' }, source, makeCtx());
  assert.deepEqual(calls.changeMode, ['levelbased']);

  await reg.dispatch({ topic: 'cmd.calibrate.volume', payload: '12.5' }, source, makeCtx());
  assert.deepEqual(calls.calibratePredictedVolume, [12.5]);

  await reg.dispatch({ topic: 'cmd.calibrate.level', payload: 1.25 }, source, makeCtx());
  assert.deepEqual(calls.calibratePredictedLevel, [1.25]);

  // Registry normalises to the descriptor's `units.default` (m3/h) before
  // the handler runs. 0.5 m3/s -> 1800 m3/h.
  await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s' }, source, makeCtx());
  assert.equal(calls.setManualInflow.length, 1);
  assert.equal(calls.setManualInflow[0].v, 1800);
  assert.equal(calls.setManualInflow[0].u, 'm3/h');

  await reg.dispatch({ topic: 'set.demand', payload: 100 }, source, makeCtx());
  assert.deepEqual(calls.forwardDemandToChildren, [100]);
});

test('child.register canonical resolves child via RED.nodes.getNode', async () => {
  const { source, calls } = makeSource();
  const child = { id: 'child-1', source: { tag: 'child-domain' } };
  const reg = makeRegistry(makeLogger());

  await reg.dispatch(
    { topic: 'child.register', payload: 'child-1', positionVsParent: 'upstream' },
    source,
    makeCtx({ child })
  );
  assert.equal(calls.registerChild.length, 1);
  assert.equal(calls.registerChild[0].childSource, child.source);
  assert.equal(calls.registerChild[0].position, 'upstream');
});

test('aliases dispatch to the same handler and log a one-time deprecation', async () => {
  const { source, calls } = makeSource();
  const ctxLogger = makeLogger();
  const reg = makeRegistry(ctxLogger);

  await reg.dispatch({ topic: 'changemode', payload: 'manual' }, source, makeCtx({ logger: ctxLogger }));
  await reg.dispatch({ topic: 'changemode', payload: 'manual' }, source, makeCtx({ logger: ctxLogger }));

  assert.deepEqual(calls.changeMode, ['manual', 'manual']);
  const deprecWarns = ctxLogger.calls.warn.filter((m) => m.includes("'changemode' is deprecated"));
  assert.equal(deprecWarns.length, 1, 'deprecation warning should log exactly once');
  assert.equal(reg.deprecationStats().changemode, 2);

  // q_in alias also routes to setInflow.
  await reg.dispatch({ topic: 'q_in', payload: 0.25, unit: 'm3/s' }, source, makeCtx({ logger: ctxLogger }));
  assert.equal(calls.setManualInflow.length, 1);
});

test('child.register with unknown child id logs warn and does not throw', async () => {
  const { source, calls } = makeSource();
  const ctxLogger = makeLogger();
  const reg = makeRegistry(makeLogger());

  await assert.doesNotReject(() =>
    reg.dispatch(
      { topic: 'child.register', payload: 'missing-id', positionVsParent: 'atEquipment' },
      source,
      makeCtx({ logger: ctxLogger })
    )
  );
  assert.equal(calls.registerChild.length, 0);
  assert.ok(
    ctxLogger.calls.warn.some((m) => m.includes('registerChild') && m.includes('missing-id')),
    `expected warn about missing child, got: ${JSON.stringify(ctxLogger.calls.warn)}`
  );
});

test('set.inflow accepts number payload and { value, unit, timestamp } object payload', async () => {
  const { source, calls } = makeSource();
  const reg = makeRegistry(makeLogger());

  // After registry units-normalisation the handler always sees a number in
  // the descriptor's default unit (m3/h). 0.5 m3/s -> 1800 m3/h.
  await reg.dispatch({ topic: 'set.inflow', payload: 0.5, unit: 'm3/s', timestamp: 1000 }, source, makeCtx());
  assert.deepEqual(calls.setManualInflow[0], { v: 1800, ts: 1000, u: 'm3/h' });

  // Object payload `{ value, unit }` is flattened to a number; 2 m3/h stays
  // 2 m3/h. The timestamp travels on the msg envelope after normalisation
  // (the per-payload `timestamp` field is not preserved by the flatten).
  await reg.dispatch(
    { topic: 'set.inflow', payload: { value: 2, unit: 'm3/h' }, timestamp: 2000 },
    source,
    makeCtx()
  );
  assert.deepEqual(calls.setManualInflow[1], { v: 2, ts: 2000, u: 'm3/h' });
});

test('set.demand in non-manual mode logs debug and does not call forwardDemandToChildren', async () => {
  const { source, calls } = makeSource({ mode: 'levelbased' });
  const ctxLogger = makeLogger();
  const reg = makeRegistry(makeLogger());

  await reg.dispatch({ topic: 'set.demand', payload: 50 }, source, makeCtx({ logger: ctxLogger }));
  assert.equal(calls.forwardDemandToChildren.length, 0);
  assert.ok(
    ctxLogger.calls.debug.some((m) => m.includes('set.demand') && m.includes('levelbased')),
    `expected debug about ignoring demand, got: ${JSON.stringify(ctxLogger.calls.debug)}`
  );
});

test('set.demand with non-numeric payload logs warn and does not call', async () => {
  const { source, calls } = makeSource({ mode: 'manual' });
  const ctxLogger = makeLogger();
  const reg = makeRegistry(makeLogger());

  await reg.dispatch({ topic: 'set.demand', payload: 'oops' }, source, makeCtx({ logger: ctxLogger }));
  assert.equal(calls.forwardDemandToChildren.length, 0);
  assert.ok(
    ctxLogger.calls.warn.some((m) => m.includes('set.demand') && m.includes('oops')),
    `expected warn about invalid Qd, got: ${JSON.stringify(ctxLogger.calls.warn)}`
  );
});
