const ChildRegistrationUtils = require('../src/helper/childRegistrationUtils');
const { POSITIONS } = require('../src/constants/positions');

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

/** Create a minimal mock parent (mainClass) that ChildRegistrationUtils expects. */
function createMockParent(opts = {}) {
  return {
    child: {},
    logger: {
      debug: jest.fn(),
      info: jest.fn(),
      warn: jest.fn(),
      error: jest.fn(),
    },
    // optionally provide a registerChild callback so the utils can delegate
    registerChild: opts.registerChild || undefined,
    ...opts,
  };
}

/** Create a minimal mock child node with the given overrides. */
function createMockChild(overrides = {}) {
  const defaults = {
    config: {
      general: {
        id: overrides.id || 'child-1',
        name: overrides.name || 'TestChild',
      },
      functionality: {
        softwareType: overrides.softwareType !== undefined ? overrides.softwareType : 'measurement',
        positionVsParent: overrides.position || POSITIONS.UPSTREAM,
      },
      asset: {
        category: overrides.category || 'sensor',
        type: overrides.assetType || 'pressure',
      },
    },
    measurements: overrides.measurements || null,
  };
  // allow caller to add extra top-level props
  return { ...defaults, ...(overrides.extra || {}) };
}

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

describe('ChildRegistrationUtils', () => {
  let parent;
  let utils;

  beforeEach(() => {
    parent = createMockParent();
    utils = new ChildRegistrationUtils(parent);
  });

  // ── Construction ─────────────────────────────────────────────────────────
  describe('constructor', () => {
    it('should store a reference to the mainClass', () => {
      expect(utils.mainClass).toBe(parent);
    });

    it('should initialise with an empty registeredChildren map', () => {
      expect(utils.registeredChildren.size).toBe(0);
    });

    it('should use the parent logger', () => {
      expect(utils.logger).toBe(parent.logger);
    });
  });

  // ── registerChild ────────────────────────────────────────────────────────
  describe('registerChild()', () => {
    it('should register a child and store it in the internal map', async () => {
      const child = createMockChild();
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(utils.registeredChildren.size).toBe(1);
      expect(utils.registeredChildren.has('child-1')).toBe(true);
    });

    it('should store softwareType, position and timestamp in the registry entry', async () => {
      const child = createMockChild({ softwareType: 'machine' });
      const before = Date.now();
      await utils.registerChild(child, POSITIONS.DOWNSTREAM);
      const after = Date.now();

      const entry = utils.registeredChildren.get('child-1');
      expect(entry.softwareType).toBe('machine');
      expect(entry.position).toBe(POSITIONS.DOWNSTREAM);
      expect(entry.registeredAt).toBeGreaterThanOrEqual(before);
      expect(entry.registeredAt).toBeLessThanOrEqual(after);
    });

    it('should store the child in mainClass.child[softwareType][category]', async () => {
      const child = createMockChild({ softwareType: 'measurement', category: 'sensor' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(parent.child.measurement).toBeDefined();
      expect(parent.child.measurement.sensor).toBeInstanceOf(Array);
      expect(parent.child.measurement.sensor).toContain(child);
    });

    it('should set the parent reference on the child', async () => {
      const child = createMockChild();
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(child.parent).toEqual([parent]);
    });

    it('should set positionVsParent on the child', async () => {
      const child = createMockChild();
      await utils.registerChild(child, POSITIONS.DOWNSTREAM);

      expect(child.positionVsParent).toBe(POSITIONS.DOWNSTREAM);
    });

    it('should lowercase the softwareType before storing', async () => {
      const child = createMockChild({ softwareType: 'Measurement' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      const entry = utils.registeredChildren.get('child-1');
      expect(entry.softwareType).toBe('measurement');
      expect(parent.child.measurement).toBeDefined();
    });

    it('should delegate to mainClass.registerChild when it is a function', async () => {
      const registerSpy = jest.fn();
      parent.registerChild = registerSpy;
      const child = createMockChild({ softwareType: 'measurement' });

      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(registerSpy).toHaveBeenCalledWith(child, 'measurement');
    });

    it('should NOT throw when mainClass has no registerChild method', async () => {
      delete parent.registerChild;
      const child = createMockChild();

      await expect(utils.registerChild(child, POSITIONS.UPSTREAM)).resolves.not.toThrow();
    });

    it('should log a debug message on registration', async () => {
      const child = createMockChild({ name: 'Pump1', id: 'p1' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(parent.logger.debug).toHaveBeenCalledWith(
        expect.stringContaining('Registering child: Pump1')
      );
    });

    it('should handle empty softwareType gracefully', async () => {
      const child = createMockChild({ softwareType: '' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      const entry = utils.registeredChildren.get('child-1');
      expect(entry.softwareType).toBe('');
    });
  });

  // ── Multiple children ────────────────────────────────────────────────────
  describe('multiple children registration', () => {
    it('should register multiple children of the same softwareType', async () => {
      const c1 = createMockChild({ id: 'c1', name: 'Sensor1', softwareType: 'measurement' });
      const c2 = createMockChild({ id: 'c2', name: 'Sensor2', softwareType: 'measurement' });

      await utils.registerChild(c1, POSITIONS.UPSTREAM);
      await utils.registerChild(c2, POSITIONS.DOWNSTREAM);

      expect(utils.registeredChildren.size).toBe(2);
      expect(parent.child.measurement.sensor).toHaveLength(2);
    });

    it('should register children of different softwareTypes', async () => {
      const sensor = createMockChild({ id: 's1', softwareType: 'measurement' });
      const machine = createMockChild({ id: 'm1', softwareType: 'machine', category: 'pump' });

      await utils.registerChild(sensor, POSITIONS.UPSTREAM);
      await utils.registerChild(machine, POSITIONS.AT_EQUIPMENT);

      expect(parent.child.measurement).toBeDefined();
      expect(parent.child.machine).toBeDefined();
      expect(parent.child.machine.pump).toContain(machine);
    });

    it('should register children of different categories under the same softwareType', async () => {
      const sensor = createMockChild({ id: 's1', softwareType: 'measurement', category: 'sensor' });
      const analyser = createMockChild({ id: 'a1', softwareType: 'measurement', category: 'analyser' });

      await utils.registerChild(sensor, POSITIONS.UPSTREAM);
      await utils.registerChild(analyser, POSITIONS.DOWNSTREAM);

      expect(parent.child.measurement.sensor).toHaveLength(1);
      expect(parent.child.measurement.analyser).toHaveLength(1);
    });

    it('should support multiple parents on a child (array append)', async () => {
      const parent2 = createMockParent();
      const utils2 = new ChildRegistrationUtils(parent2);
      const child = createMockChild();

      await utils.registerChild(child, POSITIONS.UPSTREAM);
      await utils2.registerChild(child, POSITIONS.DOWNSTREAM);

      expect(child.parent).toEqual([parent, parent2]);
    });
  });

  // ── Duplicate registration ───────────────────────────────────────────────
  describe('duplicate registration', () => {
    it('should overwrite the registry entry when the same child id is registered twice', async () => {
      const child = createMockChild({ id: 'dup-1' });

      await utils.registerChild(child, POSITIONS.UPSTREAM);
      await utils.registerChild(child, POSITIONS.DOWNSTREAM);

      // Map.set overwrites, so still size 1
      expect(utils.registeredChildren.size).toBe(1);
      const entry = utils.registeredChildren.get('dup-1');
      expect(entry.position).toBe(POSITIONS.DOWNSTREAM);
    });

    it('should push the child into the category array again on duplicate registration', async () => {
      const child = createMockChild({ id: 'dup-1' });

      await utils.registerChild(child, POSITIONS.UPSTREAM);
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      // _storeChild does a push each time
      expect(parent.child.measurement.sensor).toHaveLength(2);
    });
  });

  // ── Measurement context setup ────────────────────────────────────────────
  describe('measurement context on child', () => {
    it('should call setChildId, setChildName, setParentRef when child has measurements', async () => {
      const measurements = {
        setChildId: jest.fn(),
        setChildName: jest.fn(),
        setParentRef: jest.fn(),
      };
      const child = createMockChild({ id: 'mc-1', name: 'Sensor1', measurements });

      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(measurements.setChildId).toHaveBeenCalledWith('mc-1');
      expect(measurements.setChildName).toHaveBeenCalledWith('Sensor1');
      expect(measurements.setParentRef).toHaveBeenCalledWith(parent);
    });

    it('should skip measurement setup when child has no measurements object', async () => {
      const child = createMockChild({ measurements: null });

      // Should not throw
      await expect(utils.registerChild(child, POSITIONS.UPSTREAM)).resolves.not.toThrow();
    });
  });

  // ── getChildrenOfType ────────────────────────────────────────────────────
  describe('getChildrenOfType()', () => {
    beforeEach(async () => {
      const s1 = createMockChild({ id: 's1', softwareType: 'measurement', category: 'sensor' });
      const s2 = createMockChild({ id: 's2', softwareType: 'measurement', category: 'sensor' });
      const a1 = createMockChild({ id: 'a1', softwareType: 'measurement', category: 'analyser' });
      const m1 = createMockChild({ id: 'm1', softwareType: 'machine', category: 'pump' });

      await utils.registerChild(s1, POSITIONS.UPSTREAM);
      await utils.registerChild(s2, POSITIONS.DOWNSTREAM);
      await utils.registerChild(a1, POSITIONS.UPSTREAM);
      await utils.registerChild(m1, POSITIONS.AT_EQUIPMENT);
    });

    it('should return all children of a given softwareType', () => {
      const measurements = utils.getChildrenOfType('measurement');
      expect(measurements).toHaveLength(3);
    });

    it('should return children filtered by category', () => {
      const sensors = utils.getChildrenOfType('measurement', 'sensor');
      expect(sensors).toHaveLength(2);
    });

    it('should return empty array for unknown softwareType', () => {
      expect(utils.getChildrenOfType('nonexistent')).toEqual([]);
    });

    it('should return empty array for unknown category', () => {
      expect(utils.getChildrenOfType('measurement', 'nonexistent')).toEqual([]);
    });
  });

  // ── getChildById ─────────────────────────────────────────────────────────
  describe('getChildById()', () => {
    it('should return the child by its id', async () => {
      const child = createMockChild({ id: 'find-me' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(utils.getChildById('find-me')).toBe(child);
    });

    it('should return null for unknown id', () => {
      expect(utils.getChildById('does-not-exist')).toBeNull();
    });
  });

  // ── getAllChildren ───────────────────────────────────────────────────────
  describe('getAllChildren()', () => {
    it('should return an empty array when no children registered', () => {
      expect(utils.getAllChildren()).toEqual([]);
    });

    it('should return all registered child objects', async () => {
      const c1 = createMockChild({ id: 'c1' });
      const c2 = createMockChild({ id: 'c2' });
      await utils.registerChild(c1, POSITIONS.UPSTREAM);
      await utils.registerChild(c2, POSITIONS.DOWNSTREAM);

      const all = utils.getAllChildren();
      expect(all).toHaveLength(2);
      expect(all).toContain(c1);
      expect(all).toContain(c2);
    });
  });

  // ── logChildStructure ───────────────────────────────────────────────────
  describe('logChildStructure()', () => {
    it('should log the child structure via debug', async () => {
      const child = createMockChild({ id: 'log-1', name: 'LogChild' });
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      utils.logChildStructure();

      expect(parent.logger.debug).toHaveBeenCalledWith(
        'Current child structure:',
        expect.any(String)
      );
    });
  });

  // ── _storeChild (internal) ──────────────────────────────────────────────
  describe('_storeChild() internal behaviour', () => {
    it('should create the child object on parent if it does not exist', async () => {
      delete parent.child;
      const child = createMockChild();
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(parent.child).toBeDefined();
      expect(parent.child.measurement.sensor).toContain(child);
    });

    it('should use "sensor" as default category when asset.category is absent', async () => {
      const child = createMockChild();
      // remove asset.category to trigger default
      delete child.config.asset.category;
      await utils.registerChild(child, POSITIONS.UPSTREAM);

      expect(parent.child.measurement.sensor).toContain(child);
    });
  });
});
