// Basic tests for FlowAggregator. Pure node:test, no Node-RED runtime.

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

const { MeasurementContainer } = require('generalFunctions');
const FlowAggregator = require('../../src/measurement/flowAggregator');

function makeBasin() {
  // Constant-cross-section basin: 50 m3 / 5 m height ⇒ surfaceArea = 10 m2.
  const surfaceArea = 10;
  return {
    surfaceArea,
    minVol: 2,
    maxVol: 50,
    maxVolAtOverflow: 45,        // overflow at 4.5 m
    minVolAtOutflow: 2,
    minVolAtInflow: 30,
    overflowLevel: 4.5,
    outflowLevel: 0.2,
    inflowLevel: 3,
  };
}

function makeMeasurements() {
  return new MeasurementContainer({
    autoConvert: true,
    preferredUnits: { flow: 'm3/s', netFlowRate: 'm3/s', level: 'm', volume: 'm3' },
  });
}

function makeAggregator(overrides = {}) {
  const measurements = overrides.measurements || makeMeasurements();
  const basin = overrides.basin || makeBasin();
  // Seed predicted volume at minVol so update() has a starting point.
  measurements.type('volume').variant('predicted').position('atequipment')
    .value(basin.minVol).unit('m3');
  const fa = new FlowAggregator({ measurements, basin, flowThreshold: 1e-4 });
  return { fa, measurements, basin };
}

test('FlowAggregator.update integrates inflow-outflow over delta-t', async () => {
  const { fa, measurements } = makeAggregator();
  // Net flow = 0.01 m3/s (in) - 0.005 m3/s (out) = 0.005 m3/s.
  const t0 = Date.now() - 10_000; // 10 s ago
  measurements.type('flow').variant('predicted').position('in').child('src')
    .value(0.01, t0, 'm3/s');
  measurements.type('flow').variant('predicted').position('out').child('snk')
    .value(0.005, t0, 'm3/s');

  // Force the integrator to know we are starting 10 s in the past.
  fa._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: t0 };
  fa.update();

  const vol = measurements.type('volume').variant('predicted').position('atequipment')
    .getCurrentValue('m3');
  // Expect minVol(2) + 0.005 * ~10 ≈ 2.05 m3. Allow slack for clock jitter.
  assert.ok(vol > 2.04 && vol < 2.06, `volume after integration was ${vol}`);
});

test('FlowAggregator.update integrates measured inflow when predicted side is empty', async () => {
  // Regression: a real upstream sensor writes `flow.measured.upstream.<id>`
  // (the measurement node hard-codes variant='measured'), but the integrator
  // used to read variant='predicted' only — so level stayed flat while the
  // status row reported +N m³/h. The fix mirrors selectBestNetFlow's
  // variant precedence per side.
  const { fa, measurements } = makeAggregator();
  const t0 = Date.now() - 10_000;
  // Measured inflow at 'upstream' (one of the inflow position aliases),
  // no outflow side at all.
  measurements.type('flow').variant('measured').position('upstream').child('sensor-A')
    .value(0.01, t0, 'm3/s');

  fa._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: t0 };
  fa.update();

  const vol = measurements.type('volume').variant('predicted').position('atequipment')
    .getCurrentValue('m3');
  // Expect minVol(2) + 0.01 × ~10 ≈ 2.10 m3.
  assert.ok(vol > 2.09 && vol < 2.11, `measured inflow did not integrate: vol=${vol}`);
});

test('FlowAggregator.update mixes measured inflow with predicted outflow', async () => {
  // Realistic mix: real upstream sensor (measured) + pump-curve outflow
  // (predicted). The picker resolves each side independently, so the net
  // balance uses both.
  const { fa, measurements } = makeAggregator();
  const t0 = Date.now() - 10_000;
  measurements.type('flow').variant('measured').position('upstream').child('sensor-A')
    .value(0.01, t0, 'm3/s');
  measurements.type('flow').variant('predicted').position('downstream').child('pump-A')
    .value(0.004, t0, 'm3/s');

  fa._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: t0 };
  fa.update();

  const vol = measurements.type('volume').variant('predicted').position('atequipment')
    .getCurrentValue('m3');
  // minVol(2) + (0.01 - 0.004) × ~10 ≈ 2.06 m3.
  assert.ok(vol > 2.05 && vol < 2.07, `mixed-variant integration produced vol=${vol}`);
});

test('FlowAggregator.selectBestNetFlow prefers measured over predicted', async () => {
  const { fa, measurements } = makeAggregator();
  measurements.type('flow').variant('measured').position('in').child('m')
    .value(0.02, Date.now(), 'm3/s');
  measurements.type('flow').variant('measured').position('out').child('m')
    .value(0.01, Date.now(), 'm3/s');
  measurements.type('flow').variant('predicted').position('in').child('p')
    .value(0.5, Date.now(), 'm3/s');
  measurements.type('flow').variant('predicted').position('out').child('p')
    .value(0.0, Date.now(), 'm3/s');

  const r = fa.selectBestNetFlow();
  assert.equal(r.source, 'measured');
  assert.ok(Math.abs(r.value - 0.01) < 1e-9);
  assert.equal(r.direction, 'filling');
});

test('FlowAggregator.selectBestNetFlow falls back to level rate when no flow', async () => {
  const { fa, measurements, basin } = makeAggregator();
  // Seed two level samples 2 s apart, rising 0.1 m → rate 0.05 m/s
  // → net flow = 0.05 * 10 m2 = 0.5 m3/s (filling).
  const t0 = Date.now() - 2_000;
  const t1 = Date.now();
  measurements.type('level').variant('measured').position('atequipment').child('default')
    .value(1.0, t0, 'm');
  measurements.type('level').variant('measured').position('atequipment').child('default')
    .value(1.1, t1, 'm');

  const r = fa.selectBestNetFlow();
  assert.ok(r.source.startsWith('level:'), `source was ${r.source}`);
  assert.equal(r.direction, 'filling');
  assert.ok(Math.abs(r.value - basin.surfaceArea * 0.05) < 1e-3, `net flow was ${r.value}`);
});

test('FlowAggregator.deriveDirection threshold semantics', async () => {
  const { fa } = makeAggregator();
  assert.equal(fa.deriveDirection(0), 'steady');
  assert.equal(fa.deriveDirection(fa.flowThreshold * 2), 'filling');
  assert.equal(fa.deriveDirection(-fa.flowThreshold * 2), 'draining');
  assert.equal(fa.deriveDirection(fa.flowThreshold * 0.5), 'steady');
  assert.equal(fa.deriveDirection(-fa.flowThreshold * 0.5), 'steady');
});

test('FlowAggregator.computeRemainingTime — filling uses overflow ceiling', async () => {
  const { fa, measurements, basin } = makeAggregator();
  measurements.type('level').variant('predicted').position('atequipment')
    .value(2.0, Date.now(), 'm');
  // Net 0.05 m3/s upward; remaining height = 4.5 - 2.0 = 2.5 m.
  // seconds = 2.5 * 10 / 0.05 = 500 s.
  const r = fa.computeRemainingTime({ value: 0.05, source: 'measured', direction: 'filling' });
  assert.ok(Math.abs(r.seconds - 500) < 1e-6, `seconds was ${r.seconds}`);
  assert.equal(typeof r.source, 'string');
});

test('FlowAggregator.computeRemainingTime — draining uses outflow floor', async () => {
  const { fa, measurements } = makeAggregator();
  measurements.type('level').variant('predicted').position('atequipment')
    .value(1.0, Date.now(), 'm');
  // Net -0.05 m3/s; remaining height = 1.0 - 0.2 = 0.8 m.
  // seconds = 0.8 * 10 / 0.05 = 160 s.
  const r = fa.computeRemainingTime({ value: -0.05, source: 'measured', direction: 'draining' });
  assert.ok(Math.abs(r.seconds - 160) < 1e-6, `seconds was ${r.seconds}`);
});

test('FlowAggregator.snapshot exposes the expected shape', async () => {
  const { fa, measurements } = makeAggregator();
  measurements.type('flow').variant('measured').position('in').child('m')
    .value(0.02, Date.now(), 'm3/s');
  fa.tick();
  const snap = fa.snapshot();
  assert.ok(Object.prototype.hasOwnProperty.call(snap, 'direction'));
  assert.ok(Object.prototype.hasOwnProperty.call(snap, 'netFlow'));
  assert.ok(Object.prototype.hasOwnProperty.call(snap, 'flowSource'));
  assert.ok(Object.prototype.hasOwnProperty.call(snap, 'secondsRemaining'));
});

test('FlowAggregator.computeRemainingTime — below threshold returns null seconds', async () => {
  const { fa } = makeAggregator();
  const r = fa.computeRemainingTime({ value: 0, source: null, direction: 'steady' });
  assert.equal(r.seconds, null);
});
