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

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

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

const baseSpec = {
  canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
  output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
  curve: { flow: 'm3/h', pressure: 'mbar', power: 'kW', control: '%' },
};

test('declare returns a policy whose canonical/output match the input', () => {
  const policy = UnitPolicy.declare(baseSpec);
  assert.equal(policy.canonical('flow'), 'm3/s');
  assert.equal(policy.canonical('pressure'), 'Pa');
  assert.equal(policy.canonical('power'), 'W');
  assert.equal(policy.canonical('temperature'), 'K');
  assert.equal(policy.output('flow'), 'm3/h');
  assert.equal(policy.output('pressure'), 'mbar');
  assert.equal(policy.output('power'), 'kW');
  assert.equal(policy.output('temperature'), 'C');
  assert.equal(policy.curve('flow'), 'm3/h');
  assert.equal(policy.curve('control'), '%');
});

test('canonical/output/curve are also frozen property bags (dot access)', () => {
  const policy = UnitPolicy.declare(baseSpec);
  // Property-access form — equivalent to the method-call form above.
  assert.equal(policy.canonical.flow, 'm3/s');
  assert.equal(policy.canonical.pressure, 'Pa');
  assert.equal(policy.output.flow, 'm3/h');
  assert.equal(policy.output.temperature, 'C');
  assert.equal(policy.curve.flow, 'm3/h');
  assert.equal(policy.curve.control, '%');
  // Method-call form keeps working alongside it.
  assert.equal(policy.canonical('flow'), 'm3/s');
  assert.equal(policy.output('power'), 'kW');
});

test('canonical/output/curve property bags are frozen — no assignment / delete / redefine', () => {
  'use strict';
  const policy = UnitPolicy.declare(baseSpec);
  // Existing own-properties are non-writable.
  assert.throws(() => { policy.canonical.flow = 'tampered'; }, TypeError);
  // Existing own-properties are non-configurable: delete throws.
  assert.throws(() => { delete policy.canonical.pressure; }, TypeError);
  // Redefining an existing prop throws.
  assert.throws(
    () => Object.defineProperty(policy.canonical, 'flow', { value: 'tampered' }),
    TypeError
  );
  // Object.isFrozen reports the accessor as frozen.
  assert.equal(Object.isFrozen(policy.canonical), true);
  assert.equal(Object.isFrozen(policy.output), true);
  assert.equal(Object.isFrozen(policy.curve), true);
  // Original values survive the failed attempts.
  assert.equal(policy.canonical.flow, 'm3/s');
  assert.equal(policy.canonical.pressure, 'Pa');
});

test('curve property bag is present (empty) even when no curve was declared', () => {
  const policy = UnitPolicy.declare({
    canonical: baseSpec.canonical,
    output: baseSpec.output,
  });
  // Method form returns null for unknown types.
  assert.equal(policy.curve('flow'), null);
  // Property form is an empty frozen function — accessing missing keys is undefined.
  assert.equal(policy.curve.flow, undefined);
  assert.equal(Object.isFrozen(policy.curve), true);
});

test('declare throws when canonical or output is missing', () => {
  assert.throws(() => UnitPolicy.declare({ output: {} }), /canonical/);
  assert.throws(() => UnitPolicy.declare({ canonical: {} }), /output/);
});

test('resolve returns the candidate when it matches the expected measure', () => {
  const logger = makeFakeLogger();
  const policy = UnitPolicy.declare(baseSpec).setLogger(logger);
  assert.equal(policy.resolve('m3/h', 'flow', 'm3/s', 'general.flow'), 'm3/h');
  assert.equal(policy.resolve('bar', 'pressure', 'mbar', 'asset.pressure'), 'bar');
  assert.equal(policy.resolve('kW', 'power', 'W', 'asset.power'), 'kW');
  // No warnings on valid inputs.
  assert.equal(logger.calls.warn.length, 0);
});

test('resolve falls back when given an invalid candidate, warns once', () => {
  const logger = makeFakeLogger();
  const policy = UnitPolicy.declare(baseSpec).setLogger(logger);

  // Wrong measure family (mass unit declared as a flow unit).
  assert.equal(policy.resolve('kg', 'flow', 'm3/s', 'general.flow'), 'm3/s');
  // Same call again — the warn-once memo must suppress.
  assert.equal(policy.resolve('kg', 'flow', 'm3/s', 'general.flow'), 'm3/s');
  assert.equal(logger.calls.warn.length, 1);
  assert.match(logger.calls.warn[0], /Invalid general\.flow unit 'kg'/);

  // A different invalid candidate logs a separate warning.
  assert.equal(policy.resolve('not-a-unit', 'pressure', 'Pa', 'asset.pressure'), 'Pa');
  assert.equal(logger.calls.warn.length, 2);
});

test('resolve falls back to the default when candidate is empty/whitespace', () => {
  const policy = UnitPolicy.declare(baseSpec);
  assert.equal(policy.resolve('', 'flow', 'm3/s'), 'm3/s');
  assert.equal(policy.resolve('   ', 'flow', 'm3/s'), 'm3/s');
  assert.equal(policy.resolve(undefined, 'flow', 'm3/s'), 'm3/s');
});

test('resolve accepts type-name shorthand as well as convert-module measure', () => {
  const policy = UnitPolicy.declare(baseSpec);
  // 'flow' shorthand should map to volumeFlowRate, not be passed through raw.
  assert.equal(policy.resolve('m3/h', 'flow', 'm3/s'), 'm3/h');
  assert.equal(policy.resolve('m3/h', 'volumeFlowRate', 'm3/s'), 'm3/h');
});

test('convert is a no-op when from === to (still coerces to Number)', () => {
  const policy = UnitPolicy.declare(baseSpec);
  assert.equal(policy.convert('5', 'm3/h', 'm3/h'), 5);
  assert.equal(typeof policy.convert(5, 'm3/h', 'm3/h'), 'number');
  // Missing units also no-op.
  assert.equal(policy.convert(7, '', 'm3/h'), 7);
  assert.equal(policy.convert(7, 'm3/h', null), 7);
});

test('convert across compatible units returns the expected numeric', () => {
  const policy = UnitPolicy.declare(baseSpec);
  // 1 m3/s -> 3600 m3/h
  assert.equal(policy.convert(1, 'm3/s', 'm3/h'), 3600);
  // 1 bar -> 100000 Pa
  assert.equal(policy.convert(1, 'bar', 'Pa'), 100000);
  // 1 kW -> 1000 W
  assert.equal(policy.convert(1, 'kW', 'W'), 1000);
});

test('convert throws when value is not finite', () => {
  const policy = UnitPolicy.declare(baseSpec);
  assert.throws(() => policy.convert('not-a-number', 'm3/h', 'm3/s'), /not finite/);
  assert.throws(() => policy.convert(NaN, 'm3/h', 'm3/s'), /not finite/);
  assert.throws(() => policy.convert(Infinity, 'm3/h', 'm3/s'), /not finite/);
});

test('containerOptions returns the exact shape consumed by MeasurementContainer', () => {
  const policy = UnitPolicy.declare(baseSpec);
  const opts = policy.containerOptions();

  assert.deepEqual(opts.defaultUnits, baseSpec.output);
  assert.deepEqual(opts.preferredUnits, baseSpec.output);
  assert.deepEqual(opts.canonicalUnits, baseSpec.canonical);
  assert.equal(opts.storeCanonical, true);
  assert.equal(opts.strictUnitValidation, true);
  assert.equal(opts.throwOnInvalidUnit, true);
  assert.deepEqual(opts.requireUnitForTypes, ['flow', 'pressure', 'power', 'temperature']);

  // Mutating the returned bag must not leak back into the policy.
  opts.defaultUnits.flow = 'tampered';
  opts.requireUnitForTypes.push('volume');
  assert.equal(policy.output('flow'), 'm3/h');
  assert.deepEqual(policy.containerOptions().requireUnitForTypes, ['flow', 'pressure', 'power', 'temperature']);
});

test('containerOptions honours custom requireUnitForTypes from declare', () => {
  const policy = UnitPolicy.declare({
    ...baseSpec,
    requireUnitForTypes: ['flow', 'pressure'],
  });
  assert.deepEqual(policy.containerOptions().requireUnitForTypes, ['flow', 'pressure']);
});

test('containerOptions output works with a real MeasurementContainer', () => {
  const { MeasurementContainer } = require('../../src/measurements/index.js');
  const policy = UnitPolicy.declare(baseSpec);
  const mc = new MeasurementContainer(policy.containerOptions());
  // No throw on construction — proves the option bag is a valid input shape.
  assert.equal(mc.storeCanonical, true);
  assert.equal(mc.strictUnitValidation, true);
  assert.equal(mc.throwOnInvalidUnit, true);
  assert.equal(mc.canonicalUnits.flow, 'm3/s');
  assert.equal(mc.defaultUnits.flow, 'm3/h');
});
