const MeasurementContainer = require('../src/measurements/MeasurementContainer');

describe('MeasurementContainer', () => {
  let mc;

  beforeEach(() => {
    mc = new MeasurementContainer({ windowSize: 5, autoConvert: false });
  });

  // ── Construction ─────────────────────────────────────────────────────
  describe('constructor', () => {
    it('should initialise with default windowSize when none provided', () => {
      const m = new MeasurementContainer();
      expect(m.windowSize).toBe(10);
    });

    it('should accept a custom windowSize', () => {
      expect(mc.windowSize).toBe(5);
    });

    it('should start with an empty measurements map', () => {
      expect(mc.measurements).toEqual({});
    });

    it('should populate default units', () => {
      expect(mc.defaultUnits.pressure).toBe('mbar');
      expect(mc.defaultUnits.flow).toBe('m3/h');
    });

    it('should allow overriding default units', () => {
      const m = new MeasurementContainer({ defaultUnits: { pressure: 'Pa' } });
      expect(m.defaultUnits.pressure).toBe('Pa');
    });
  });

  // ── Chainable setters ───────────────────────────────────────────────
  describe('chaining API — type / variant / position', () => {
    it('should set type and return this for chaining', () => {
      const ret = mc.type('pressure');
      expect(ret).toBe(mc);
      expect(mc._currentType).toBe('pressure');
    });

    it('should reset variant and position when type is called', () => {
      mc.type('pressure').variant('measured').position('upstream');
      mc.type('flow');
      expect(mc._currentVariant).toBeNull();
      expect(mc._currentPosition).toBeNull();
    });

    it('should set variant and return this', () => {
      mc.type('pressure');
      const ret = mc.variant('measured');
      expect(ret).toBe(mc);
      expect(mc._currentVariant).toBe('measured');
    });

    it('should throw if variant is called without type', () => {
      expect(() => mc.variant('measured')).toThrow(/Type must be specified/);
    });

    it('should set position (lowercased) and return this', () => {
      mc.type('pressure').variant('measured');
      const ret = mc.position('Upstream');
      expect(ret).toBe(mc);
      expect(mc._currentPosition).toBe('upstream');
    });

    it('should throw if position is called without variant', () => {
      mc.type('pressure');
      expect(() => mc.position('upstream')).toThrow(/Variant must be specified/);
    });
  });

  // ── Storing and retrieving values ───────────────────────────────────
  describe('value() and retrieval methods', () => {
    beforeEach(() => {
      mc.type('pressure').variant('measured').position('upstream');
    });

    it('should store a value and retrieve it with getCurrentValue()', () => {
      mc.value(42, 1000);
      expect(mc.getCurrentValue()).toBe(42);
    });

    it('should return this for chaining from value()', () => {
      const ret = mc.value(1, 1000);
      expect(ret).toBe(mc);
    });

    it('should store multiple values and keep the latest', () => {
      mc.value(10, 1).value(20, 2).value(30, 3);
      expect(mc.getCurrentValue()).toBe(30);
    });

    it('should respect the windowSize (rolling window)', () => {
      for (let i = 1; i <= 8; i++) {
        mc.value(i, i);
      }
      const all = mc.getAllValues();
      // windowSize is 5, so only the last 5 values should remain
      expect(all.values.length).toBe(5);
      expect(all.values).toEqual([4, 5, 6, 7, 8]);
    });

    it('should compute getAverage() correctly', () => {
      mc.value(10, 1).value(20, 2).value(30, 3);
      expect(mc.getAverage()).toBe(20);
    });

    it('should compute getMin()', () => {
      mc.value(10, 1).value(5, 2).value(20, 3);
      expect(mc.getMin()).toBe(5);
    });

    it('should compute getMax()', () => {
      mc.value(10, 1).value(5, 2).value(20, 3);
      expect(mc.getMax()).toBe(20);
    });

    it('should return null for getCurrentValue() when no values exist', () => {
      expect(mc.getCurrentValue()).toBeNull();
    });

    it('should return null for getAverage() when no values exist', () => {
      expect(mc.getAverage()).toBeNull();
    });

    it('should return null for getMin() when no values exist', () => {
      expect(mc.getMin()).toBeNull();
    });

    it('should return null for getMax() when no values exist', () => {
      expect(mc.getMax()).toBeNull();
    });
  });

  // ── getAllValues() ──────────────────────────────────────────────────
  describe('getAllValues()', () => {
    it('should return values, timestamps, and unit', () => {
      mc.type('pressure').variant('measured').position('upstream');
      mc.unit('bar');
      mc.value(10, 100).value(20, 200);
      const all = mc.getAllValues();
      expect(all.values).toEqual([10, 20]);
      expect(all.timestamps).toEqual([100, 200]);
      expect(all.unit).toBe('bar');
    });

    it('should return null when chain is incomplete', () => {
      mc.type('pressure');
      expect(mc.getAllValues()).toBeNull();
    });
  });

  // ── unit() ──────────────────────────────────────────────────────────
  describe('unit()', () => {
    it('should set unit on the underlying measurement', () => {
      mc.type('pressure').variant('measured').position('upstream');
      mc.unit('bar');
      const measurement = mc.get();
      expect(measurement.unit).toBe('bar');
    });
  });

  // ── get() ───────────────────────────────────────────────────────────
  describe('get()', () => {
    it('should return the Measurement instance for a complete chain', () => {
      mc.type('pressure').variant('measured').position('upstream');
      mc.value(1, 1);
      const m = mc.get();
      expect(m).toBeDefined();
      expect(m.type).toBe('pressure');
      expect(m.variant).toBe('measured');
      expect(m.position).toBe('upstream');
    });

    it('should return null when chain is incomplete', () => {
      mc.type('pressure');
      expect(mc.get()).toBeNull();
    });
  });

  // ── exists() ────────────────────────────────────────────────────────
  describe('exists()', () => {
    it('should return false for a non-existent measurement', () => {
      mc.type('pressure').variant('measured').position('upstream');
      expect(mc.exists()).toBe(false);
    });

    it('should return true after a value has been stored', () => {
      mc.type('pressure').variant('measured').position('upstream').value(1, 1);
      expect(mc.exists()).toBe(true);
    });

    it('should support requireValues option', () => {
      mc.type('pressure').variant('measured').position('upstream');
      // Force creation of measurement without values
      mc.get();
      expect(mc.exists({ requireValues: false })).toBe(true);
      expect(mc.exists({ requireValues: true })).toBe(false);
    });

    it('should support explicit type/variant/position overrides', () => {
      mc.type('pressure').variant('measured').position('upstream').value(1, 1);
      // Reset chain, then query by explicit keys
      mc.type('flow');
      expect(mc.exists({ type: 'pressure', variant: 'measured', position: 'upstream' })).toBe(true);
      expect(mc.exists({ type: 'flow', variant: 'measured', position: 'upstream' })).toBe(false);
    });

    it('should return false when type is not set and not provided', () => {
      const fresh = new MeasurementContainer({ autoConvert: false });
      expect(fresh.exists()).toBe(false);
    });
  });

  // ── getLaggedValue() / getLaggedSample() ─────────────────────────────
  describe('getLaggedValue() and getLaggedSample()', () => {
    beforeEach(() => {
      mc.type('pressure').variant('measured').position('upstream');
      mc.value(10, 100).value(20, 200).value(30, 300);
    });

    it('should return the value at lag=1 (previous value)', () => {
      expect(mc.getLaggedValue(1)).toBe(20);
    });

    it('should return null when lag exceeds stored values', () => {
      expect(mc.getLaggedValue(10)).toBeNull();
    });

    it('should return a sample object from getLaggedSample()', () => {
      const sample = mc.getLaggedSample(0);
      expect(sample).toHaveProperty('value', 30);
      expect(sample).toHaveProperty('timestamp', 300);
    });

    it('should return null from getLaggedSample when not enough values', () => {
      expect(mc.getLaggedSample(10)).toBeNull();
    });
  });

  // ── Listing helpers ─────────────────────────────────────────────────
  describe('getTypes() / getVariants() / getPositions()', () => {
    beforeEach(() => {
      mc.type('pressure').variant('measured').position('upstream').value(1, 1);
      mc.type('flow').variant('predicted').position('downstream').value(2, 2);
    });

    it('should list all stored types', () => {
      const types = mc.getTypes();
      expect(types).toContain('pressure');
      expect(types).toContain('flow');
    });

    it('should list variants for a given type', () => {
      mc.type('pressure');
      expect(mc.getVariants()).toContain('measured');
    });

    it('should return empty array for type with no variants', () => {
      mc.type('temperature');
      expect(mc.getVariants()).toEqual([]);
    });

    it('should throw if getVariants() called without type', () => {
      const fresh = new MeasurementContainer({ autoConvert: false });
      expect(() => fresh.getVariants()).toThrow(/Type must be specified/);
    });

    it('should list positions for type+variant', () => {
      mc.type('pressure').variant('measured');
      expect(mc.getPositions()).toContain('upstream');
    });

    it('should throw if getPositions() called without type and variant', () => {
      const fresh = new MeasurementContainer({ autoConvert: false });
      expect(() => fresh.getPositions()).toThrow(/Type and variant must be specified/);
    });
  });

  // ── clear() ─────────────────────────────────────────────────────────
  describe('clear()', () => {
    it('should reset all measurements and chain state', () => {
      mc.type('pressure').variant('measured').position('upstream').value(1, 1);
      mc.clear();
      expect(mc.measurements).toEqual({});
      expect(mc._currentType).toBeNull();
      expect(mc._currentVariant).toBeNull();
      expect(mc._currentPosition).toBeNull();
    });
  });

  // ── Child context setters ───────────────────────────────────────────
  describe('child context', () => {
    it('should set childId and return this', () => {
      expect(mc.setChildId('c1')).toBe(mc);
      expect(mc.childId).toBe('c1');
    });

    it('should set childName and return this', () => {
      expect(mc.setChildName('pump1')).toBe(mc);
      expect(mc.childName).toBe('pump1');
    });

    it('should set parentRef and return this', () => {
      const parent = { id: 'p1' };
      expect(mc.setParentRef(parent)).toBe(mc);
      expect(mc.parentRef).toBe(parent);
    });
  });

  // ── Event emission ──────────────────────────────────────────────────
  describe('event emission', () => {
    it('should emit an event when a value is set', (done) => {
      mc.emitter.on('pressure.measured.upstream', (data) => {
        expect(data.value).toBe(42);
        expect(data.type).toBe('pressure');
        expect(data.variant).toBe('measured');
        expect(data.position).toBe('upstream');
        done();
      });
      mc.type('pressure').variant('measured').position('upstream').value(42, 1);
    });
  });

  // ── setPreferredUnit ────────────────────────────────────────────────
  describe('setPreferredUnit()', () => {
    it('should store preferred unit and return this', () => {
      const ret = mc.setPreferredUnit('pressure', 'Pa');
      expect(ret).toBe(mc);
      expect(mc.preferredUnits.pressure).toBe('Pa');
    });
  });
});
