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

const BaseDomain = require('../../src/domain/BaseDomain');
const UnitPolicy = require('../../src/domain/UnitPolicy');

// ── Subclasses ────────────────────────────────────────────────────────

// Minimal subclass — relies on every base default. Uses 'measurement' so the
// configManager finds a real config schema in src/configs/measurement.json.
class PlainMeasurement extends BaseDomain {
  static name = 'measurement';
}

// Subclass that records call ordering and exposes hooks.
class TrackingMeasurement extends BaseDomain {
  static name = 'measurement';

  configure() {
    this.calls = this.calls || [];
    // Pin the moment at which `configure` runs — these MUST be populated
    // before the hook fires.
    this.calls.push({
      hook: 'configure',
      hasConfig: !!this.config,
      hasMeasurements: !!this.measurements,
    });
  }

  _init() {
    this.calls = this.calls || [];
    this.calls.push({ hook: '_init' });
  }
}

// Subclass with a UnitPolicy — verify containerOptions reach MeasurementContainer.
class PolicyMeasurement extends BaseDomain {
  static name = 'measurement';
  static unitPolicy = UnitPolicy.declare({
    canonical: { flow: 'm3/s', pressure: 'Pa' },
    output:    { flow: 'L/s',  pressure: 'kPa' },
  });
}

// Subclass that declares a child getter in `configure`.
class ParentDomain extends BaseDomain {
  static name = 'measurement';

  configure() {
    this.declareChildGetter('machines', 'machine');
  }
}

// ── Helpers ──────────────────────────────────────────────────────────

function makeChild({ id = 'c1', name = id, softwareType = 'machine', category = 'centrifugal' } = {}) {
  return {
    config: {
      general: { id, name },
      functionality: { softwareType },
      asset: { category, type: 'pump' },
    },
    measurements: {
      emitter: new EventEmitter(),
      setChildId() {}, setChildName() {}, setParentRef() {},
    },
  };
}

// ── Tests ────────────────────────────────────────────────────────────

test('constructs successfully against a real config schema', () => {
  const m = new PlainMeasurement({});
  assert.ok(m.config?.general?.name);
  assert.ok(m.measurements);
  assert.ok(m.logger);
  assert.ok(m.emitter);
  assert.ok(m.childRegistrationUtils);
  assert.ok(m.router);
});

test('configure() runs after config + measurements are populated, exactly once', () => {
  const m = new TrackingMeasurement({});
  const configureCalls = m.calls.filter(c => c.hook === 'configure');
  assert.equal(configureCalls.length, 1);
  assert.equal(configureCalls[0].hasConfig, true);
  assert.equal(configureCalls[0].hasMeasurements, true);
});

test('_init() runs after configure()', () => {
  const m = new TrackingMeasurement({});
  const order = m.calls.map(c => c.hook);
  assert.deepEqual(order, ['configure', '_init']);
});

test('static unitPolicy is honored — defaultUnits reflect output map', () => {
  const m = new PolicyMeasurement({});
  // PolicyMeasurement declares output.flow='L/s', output.pressure='kPa'
  assert.equal(m.measurements.defaultUnits.flow, 'L/s');
  assert.equal(m.measurements.defaultUnits.pressure, 'kPa');
  // Canonical flow was declared as 'm3/s'
  assert.equal(m.measurements.canonicalUnits.flow, 'm3/s');
});

test('without unitPolicy, MeasurementContainer keeps its built-in defaults', () => {
  const m = new PlainMeasurement({});
  assert.equal(m.unitPolicy, null);
  // Built-in defaults from MeasurementContainer.
  assert.equal(m.measurements.defaultUnits.flow, 'm3/h');
  assert.equal(m.measurements.defaultUnits.pressure, 'mbar');
  assert.equal(m.measurements.autoConvert, true);
});

test('declareChildGetter flattens registry slice across categories', () => {
  const p = new ParentDomain({});
  // Empty before any registration.
  assert.deepEqual(p.machines, {});

  // Mirror what childRegistrationUtils._storeChild does: child.machine.<cat>=[...]
  const a = makeChild({ id: 'pumpA', category: 'centrifugal' });
  const b = makeChild({ id: 'pumpB', category: 'positivedisplacement' });
  p.child = { machine: { centrifugal: [a], positivedisplacement: [b] } };

  const flat = p.machines;
  assert.deepEqual(Object.keys(flat).sort(), ['pumpA', 'pumpB']);
  assert.equal(flat.pumpA, a);
  assert.equal(flat.pumpB, b);
});

test('notifyOutputChanged fires "output-changed" on emitter', () => {
  const m = new PlainMeasurement({});
  let count = 0;
  m.emitter.on('output-changed', () => count++);
  m.notifyOutputChanged();
  m.notifyOutputChanged();
  assert.equal(count, 2);
});

test('context() returns a frozen object with the documented keys', () => {
  const m = new PlainMeasurement({});
  const ctx = m.context();
  assert.ok(Object.isFrozen(ctx));
  for (const k of ['config', 'logger', 'measurements', 'emitter', 'child', 'unitPolicy', 'router']) {
    assert.ok(k in ctx, `context() missing key '${k}'`);
  }
  assert.equal(ctx.config, m.config);
  assert.equal(ctx.measurements, m.measurements);
});

test('close() removes emitter listeners and tears down router', () => {
  const m = new PlainMeasurement({});
  let teardownCount = 0;
  const origTeardown = m.router.tearDown.bind(m.router);
  m.router.tearDown = () => { teardownCount++; origTeardown(); };

  m.emitter.on('output-changed', () => {});
  assert.equal(m.emitter.listenerCount('output-changed'), 1);

  m.close();
  assert.equal(teardownCount, 1);
  assert.equal(m.emitter.listenerCount('output-changed'), 0);
});

test('registerChild delegates to router.dispatchRegister', () => {
  const m = new PlainMeasurement({});
  const seen = [];
  const origDispatch = m.router.dispatchRegister.bind(m.router);
  m.router.dispatchRegister = (child, st) => {
    seen.push({ id: child.config.general.id, st });
    return origDispatch(child, st);
  };

  const child = makeChild({ id: 'kid1', softwareType: 'measurement' });
  const result = m.registerChild(child, 'measurement');
  assert.equal(result, true);
  assert.deepEqual(seen, [{ id: 'kid1', st: 'measurement' }]);
});

test('childRegistrationUtils.registerChild flows through router (end-to-end handshake)', async () => {
  const m = new PlainMeasurement({});
  let routed = null;
  m.router.onRegister('measurement', (child, st) => {
    routed = { id: child.config.general.id, st };
  });

  const child = makeChild({ id: 'kid2', softwareType: 'measurement' });
  await m.childRegistrationUtils.registerChild(child, 'upstream', 0);

  assert.deepEqual(routed, { id: 'kid2', st: 'measurement' });
});

test('direct BaseDomain instantiation throws (abstract)', () => {
  assert.throws(() => new BaseDomain({}), /abstract/);
});
