const { test } = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('events');

const ChildRouter = require('../../src/domain/ChildRouter');

// ── helpers ────────────────────────────────────────────────────────

function makeDomain() {
  const logs = [];
  return {
    logger: {
      debug: (...a) => logs.push(['debug', ...a]),
      info:  (...a) => logs.push(['info', ...a]),
      warn:  (...a) => logs.push(['warn', ...a]),
      error: (...a) => logs.push(['error', ...a]),
    },
    _logs: logs,
  };
}

function makeChild({ id = 'c1', name = id, softwareType = 'measurement' } = {}) {
  return {
    config: {
      general: { id, name },
      functionality: { softwareType },
      asset: { type: 'pressure' },
    },
    measurements: { emitter: new EventEmitter() },
  };
}

function emitMeasured(child, type, position, value, extra = {}) {
  child.measurements.emitter.emit(`${type}.measured.${position}`, { value, ...extra });
}

function emitPredicted(child, type, position, value, extra = {}) {
  child.measurements.emitter.emit(`${type}.predicted.${position}`, { value, ...extra });
}

// ── tests ─────────────────────────────────────────────────────────

test('onRegister fires for the matching softwareType', () => {
  const domain = makeDomain();
  const router = new ChildRouter(domain);
  const seen = [];

  router.onRegister('measurement', (child, st) => seen.push({ id: child.config.general.id, st }));

  const ch = makeChild({ id: 'm1' });
  router.dispatchRegister(ch, 'measurement');

  assert.equal(seen.length, 1);
  assert.equal(seen[0].id, 'm1');
  assert.equal(seen[0].st, 'measurement');
});

test('onMeasurement with full filter only fires for matching events', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
    (data, child) => hits.push({ v: data.value, id: child.config.general.id }));

  const ch = makeChild({ id: 'p-up' });
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 100);
  emitMeasured(ch, 'pressure', 'downstream', 200);   // ignored: wrong position
  emitMeasured(ch, 'flow',     'upstream',   5);     // ignored: wrong type
  emitPredicted(ch, 'pressure', 'upstream', 999);    // ignored: wrong variant

  assert.deepEqual(hits, [{ v: 100, id: 'p-up' }]);
});

test('onMeasurement without position filter fires for all positions of the type', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onMeasurement('measurement', { type: 'pressure' },
    (data) => hits.push(data.value));

  const ch = makeChild();
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 1);
  emitMeasured(ch, 'pressure', 'downstream', 2);
  emitMeasured(ch, 'pressure', 'atequipment', 3);
  emitMeasured(ch, 'flow', 'upstream', 99);          // ignored: wrong type
  emitPredicted(ch, 'pressure', 'upstream', 50);     // ignored: wrong variant

  assert.deepEqual(hits.sort(), [1, 2, 3]);
});

test('onPrediction works analogously to onMeasurement', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onPrediction('machinegroup', { type: 'flow', position: 'downstream' },
    (data) => hits.push(data.value));

  const ch = makeChild({ softwareType: 'machinegroupcontrol' });
  router.dispatchRegister(ch, 'machinegroupcontrol');

  emitPredicted(ch, 'flow', 'downstream', 42);
  emitPredicted(ch, 'flow', 'upstream', 7);          // ignored: wrong position
  emitMeasured(ch,  'flow', 'downstream', 99);       // ignored: wrong variant

  assert.deepEqual(hits, [42]);
});

test('software-type alias resolution: onRegister("machine") matches softwareType="rotatingmachine"', () => {
  const router = new ChildRouter(makeDomain());
  const seen = [];

  router.onRegister('machine', (child) => seen.push(child.config.general.id));

  const rm = makeChild({ id: 'rm-1', softwareType: 'rotatingmachine' });
  router.dispatchRegister(rm, 'rotatingmachine');

  assert.deepEqual(seen, ['rm-1']);
});

test('alias resolution also flows through measurement subscriptions', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  // Declare with the canonical 'machine' alias.
  router.onMeasurement('machine', { type: 'flow', position: 'downstream' },
    (data) => hits.push(data.value));

  // Child reports the raw, non-canonical softwareType.
  const rm = makeChild({ id: 'rm-1', softwareType: 'rotatingmachine' });
  router.dispatchRegister(rm, 'rotatingmachine');

  emitMeasured(rm, 'flow', 'downstream', 17);
  assert.deepEqual(hits, [17]);
});

test('tearDown removes listeners — re-emitting after tearDown does not invoke handler', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
    (data) => hits.push(['concrete', data.value]));
  router.onMeasurement('measurement', { type: 'pressure' },           // wildcard branch
    (data) => hits.push(['wild', data.value]));

  const ch = makeChild();
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 1);
  assert.equal(hits.length, 2);

  router.tearDown();

  emitMeasured(ch, 'pressure', 'upstream', 2);
  emitMeasured(ch, 'pressure', 'downstream', 3);
  assert.equal(hits.length, 2, 'no further hits after tearDown');

  // Original emit should be restored after teardown — sanity-check it still works
  // for unrelated listeners on the same emitter.
  let other = 0;
  ch.measurements.emitter.on('flow.measured.upstream', () => other++);
  emitMeasured(ch, 'flow', 'upstream', 9);
  assert.equal(other, 1);
});

test('multiple onMeasurement subscriptions for same softwareType all fire', () => {
  const router = new ChildRouter(makeDomain());
  const a = []; const b = []; const c = [];

  router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
    (d) => a.push(d.value));
  router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
    (d) => b.push(d.value));   // duplicate concrete sub
  router.onMeasurement('measurement', { type: 'pressure' },
    (d) => c.push(d.value));   // wildcard-position sub

  const ch = makeChild();
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 7);

  assert.deepEqual(a, [7]);
  assert.deepEqual(b, [7]);
  assert.deepEqual(c, [7]);
});

test('chainable API returns the router instance', () => {
  const router = new ChildRouter(makeDomain());
  const r = router
    .onRegister('measurement', () => {})
    .onMeasurement('measurement', { type: 'flow' }, () => {})
    .onPrediction('machine', { type: 'flow', position: 'downstream' }, () => {});
  assert.equal(r, router);
});

test('multi-parent: two routers on the same child both receive every event and tear down independently', () => {
  // Regression for the pre-2026-05-11 emit-patching stack: two parents
  // subscribing partial-filter wildcards on the same child must compose
  // without stacking wrappers, and either teardown order must work.
  const routerA = new ChildRouter(makeDomain());
  const routerB = new ChildRouter(makeDomain());
  const a = []; const b = [];

  routerA.onMeasurement('measurement', { type: 'pressure' },
    (data) => a.push(data.value));
  routerB.onMeasurement('measurement', { type: 'pressure' },
    (data) => b.push(data.value));

  const ch = makeChild();
  routerA.dispatchRegister(ch, 'measurement');
  routerB.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 11);
  emitMeasured(ch, 'pressure', 'downstream', 22);
  assert.deepEqual(a.sort(), [11, 22]);
  assert.deepEqual(b.sort(), [11, 22]);

  // Tear down B first — A must continue to fire on subsequent events.
  routerB.tearDown();
  emitMeasured(ch, 'pressure', 'upstream', 33);
  assert.deepEqual(a.sort(), [11, 22, 33]);
  assert.deepEqual(b.sort(), [11, 22], 'B receives nothing after its teardown');

  // Now tear down A in the reverse order; neither should fire.
  routerA.tearDown();
  emitMeasured(ch, 'pressure', 'upstream', 44);
  assert.deepEqual(a.sort(), [11, 22, 33], 'A receives nothing after its teardown');
  assert.deepEqual(b.sort(), [11, 22]);
});

test('position-only filter fans out across every known type for that position', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onMeasurement('measurement', { position: 'upstream' },
    (data) => hits.push(data.value));

  const ch = makeChild();
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 1);
  emitMeasured(ch, 'flow', 'upstream', 2);
  emitMeasured(ch, 'temperature', 'upstream', 3);
  emitMeasured(ch, 'pressure', 'downstream', 99);   // wrong position
  emitPredicted(ch, 'pressure', 'upstream', 99);    // wrong variant

  assert.deepEqual(hits.sort(), [1, 2, 3]);
});

test('empty filter ({}) fires for every type/position combination', () => {
  const router = new ChildRouter(makeDomain());
  const hits = [];

  router.onMeasurement('measurement', {}, (data) => hits.push(data.value));

  const ch = makeChild();
  router.dispatchRegister(ch, 'measurement');

  emitMeasured(ch, 'pressure', 'upstream', 1);
  emitMeasured(ch, 'flow', 'downstream', 2);
  emitMeasured(ch, 'level', 'atequipment', 3);
  emitPredicted(ch, 'flow', 'upstream', 99);        // wrong variant

  assert.deepEqual(hits.sort(), [1, 2, 3]);
});
