import {
  QUEUE_DEFINITIONS,
  queueDef,
  workerOptionsFor,
  defaultJobOptionsFor,
} from './queue-definitions';
import {
  QUEUE_AI_ANALYSIS,
  QUEUE_DOCUMENT_IMPORT,
  QUEUE_NOTIFICATION,
  QUEUE_DOMAIN_EVENTS,
  QUEUE_AUDIT_LOG,
  QUEUE_FILE_CLEANUP,
  QUEUE_AI_INTERACTIVE,
  QUEUE_AI_BACKGROUND,
} from './queue.service';

describe('Queue Definition Registry', () => {
  // M-AI-03: +2 queues (ai-interactive / ai-background)
  const allQueues = [
    QUEUE_AI_ANALYSIS,
    QUEUE_DOCUMENT_IMPORT,
    QUEUE_NOTIFICATION,
    QUEUE_DOMAIN_EVENTS,
    QUEUE_AUDIT_LOG,
    QUEUE_FILE_CLEANUP,
    QUEUE_AI_INTERACTIVE,
    QUEUE_AI_BACKGROUND,
  ]; // total: 8 queues

  describe('QUEUE_DEFINITIONS', () => {
    it('contains all 8 queues', () => {
      expect(Object.keys(QUEUE_DEFINITIONS)).toHaveLength(8);
      for (const name of allQueues) {
        expect(QUEUE_DEFINITIONS[name]).toBeDefined();
      }
    });

    it('every queue has required workerOptions fields', () => {
      for (const def of Object.values(QUEUE_DEFINITIONS)) {
        expect(def.workerOptions.concurrency).toBeGreaterThanOrEqual(1);
        expect(def.workerOptions.lockDuration).toBeGreaterThan(0);
        expect(def.workerOptions.stalledInterval).toBeGreaterThan(0);
        expect(def.workerOptions.maxStalledCount).toBeGreaterThanOrEqual(1);
      }
    });

    it('every queue has required defaultJobOptions fields', () => {
      for (const def of Object.values(QUEUE_DEFINITIONS)) {
        expect(def.defaultJobOptions.attempts).toBeGreaterThanOrEqual(1);
        expect(def.defaultJobOptions.backoff.type).toBe('exponential');
        expect(def.defaultJobOptions.backoff.delay).toBeGreaterThan(0);
        expect(def.defaultJobOptions.removeOnComplete.count).toBeGreaterThan(0);
        expect(def.defaultJobOptions.removeOnComplete.age).toBeGreaterThan(0);
        expect(def.defaultJobOptions.removeOnFail.count).toBeGreaterThan(0);
        expect(def.defaultJobOptions.removeOnFail.age).toBeGreaterThan(0);
      }
    });

    it('applies first-stage default values (legacy 6 queues)', () => {
      const legacyQueues = [
        QUEUE_AI_ANALYSIS, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICATION,
        QUEUE_DOMAIN_EVENTS, QUEUE_AUDIT_LOG, QUEUE_FILE_CLEANUP,
      ];
      for (const name of legacyQueues) {
        const def = QUEUE_DEFINITIONS[name];
        expect(def.workerOptions.concurrency).toBe(1);
        expect(def.defaultJobOptions.attempts).toBe(3);
        expect(def.defaultJobOptions.backoff.delay).toBe(1000);
        expect(def.defaultJobOptions.backoff.type).toBe('exponential');
        expect(def.defaultJobOptions.removeOnComplete.count).toBe(1000);
        expect(def.defaultJobOptions.removeOnComplete.age).toBe(24 * 3600);
        expect(def.defaultJobOptions.removeOnFail.count).toBe(5000);
        expect(def.defaultJobOptions.removeOnFail.age).toBe(7 * 24 * 3600);
      }
    });

    it('M-AI-03 queues have ADR-003 per-queue config', () => {
      // ai-interactive: concurrency=2, attempts=2, backoff=2000ms, lockDuration=30s
      const iDef = QUEUE_DEFINITIONS[QUEUE_AI_INTERACTIVE];
      expect(iDef.workerOptions.concurrency).toBe(2);
      expect(iDef.defaultJobOptions.attempts).toBe(2);
      expect(iDef.defaultJobOptions.backoff.delay).toBe(2000);

      // ai-background: concurrency=1, attempts=3, backoff=5000ms, lockDuration=60s
      const bDef = QUEUE_DEFINITIONS[QUEUE_AI_BACKGROUND];
      expect(bDef.workerOptions.concurrency).toBe(1);
      expect(bDef.defaultJobOptions.attempts).toBe(3);
      expect(bDef.defaultJobOptions.backoff.delay).toBe(5000);
      expect(bDef.workerOptions.lockDuration).toBe(60_000);
    });
  });

  describe('queueDef', () => {
    it('returns definition for valid queue name', () => {
      const def = queueDef(QUEUE_AI_ANALYSIS);
      expect(def.name).toBe(QUEUE_AI_ANALYSIS);
    });

    it('throws for unknown queue name', () => {
      expect(() => queueDef('nonexistent')).toThrow('Unknown queue');
    });
  });

  describe('workerOptionsFor', () => {
    it('returns worker options for a queue', () => {
      const opts = workerOptionsFor(QUEUE_DOCUMENT_IMPORT);
      expect(opts.concurrency).toBe(1);
      expect(opts.lockDuration).toBe(30_000);
    });
  });

  describe('defaultJobOptionsFor', () => {
    it('returns job options for a queue', () => {
      const opts = defaultJobOptionsFor(QUEUE_NOTIFICATION);
      expect(opts.attempts).toBe(3);
      expect(opts.backoff.type).toBe('exponential');
    });
  });

  describe('environment variable overrides', () => {
    let queueDefs: typeof import('./queue-definitions');
    const savedEnv = { ...process.env };

    beforeEach(() => {
      jest.resetModules();
    });

    afterEach(() => {
      // Restore env to prevent cross-test leakage
      for (const key of Object.keys(process.env)) {
        if (!(key in savedEnv)) delete process.env[key];
        else process.env[key] = savedEnv[key];
      }
    });

    const loadDefs = () => {
      queueDefs = require('./queue-definitions');
    };

    it('respects BULL_AI_ANALYSIS_CONCURRENCY override', () => {
      process.env.BULL_AI_ANALYSIS_CONCURRENCY = '5';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.workerOptions.concurrency).toBe(5);
    });

    it('respects BULL_AI_ANALYSIS_ATTEMPTS override', () => {
      process.env.BULL_AI_ANALYSIS_ATTEMPTS = '1';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.defaultJobOptions.attempts).toBe(1);
    });

    it('rejects concurrency=0 via min bound and falls back', () => {
      process.env.BULL_AI_ANALYSIS_CONCURRENCY = '0';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.workerOptions.concurrency).toBe(1);
    });

    it('uses fallback when env var is empty string', () => {
      process.env.BULL_AI_ANALYSIS_ATTEMPTS = '';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.defaultJobOptions.attempts).toBe(3);
    });

    it('uses fallback when env var is non-numeric', () => {
      process.env.BULL_AI_ANALYSIS_CONCURRENCY = 'abc';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.workerOptions.concurrency).toBe(1);
    });

    it('uses fallback when env var is negative', () => {
      process.env.BULL_AI_ANALYSIS_CONCURRENCY = '-1';
      loadDefs();
      const def = queueDefs.QUEUE_DEFINITIONS[QUEUE_AI_ANALYSIS];
      expect(def.workerOptions.concurrency).toBe(1);
    });

    it('respects global BULL_COMPLETED_MAX_COUNT for all queues', () => {
      process.env.BULL_COMPLETED_MAX_COUNT = '500';
      loadDefs();
      for (const def of Object.values(queueDefs.QUEUE_DEFINITIONS)) {
        expect(def.defaultJobOptions.removeOnComplete.count).toBe(500);
      }
    });
  });

  describe('global cleanup env vars', () => {
    it('applies BULL_COMPLETED_MAX_COUNT to all queues', () => {
      for (const def of Object.values(QUEUE_DEFINITIONS)) {
        expect(def.defaultJobOptions.removeOnComplete.count).toBe(1000);
      }
    });

    it('applies BULL_FAILED_MAX_AGE_SEC to all queues', () => {
      for (const def of Object.values(QUEUE_DEFINITIONS)) {
        expect(def.defaultJobOptions.removeOnFail.age).toBe(7 * 24 * 3600);
      }
    });
  });

  describe('lockDuration vs stalled vs business timeout', () => {
    it('lockDuration ≠ business timeout — documented constraint', () => {
      // lockDuration is BullMQ lock renewal interval.
      // Business timeouts MUST be enforced inside processor logic.
      // Each queue may have different lockDuration (e.g. ai-background=60s).
      for (const def of Object.values(QUEUE_DEFINITIONS)) {
        expect(def.workerOptions.lockDuration).toBeGreaterThanOrEqual(30_000);
        expect(def.workerOptions.stalledInterval).toBeGreaterThanOrEqual(30_000);
        expect(def.workerOptions.maxStalledCount).toBeGreaterThanOrEqual(1);
      }
      // ai-background explicitly configured for longer jobs
      expect(QUEUE_DEFINITIONS[QUEUE_AI_BACKGROUND].workerOptions.lockDuration).toBe(60_000);
      expect(QUEUE_DEFINITIONS[QUEUE_AI_BACKGROUND].workerOptions.stalledInterval).toBe(60_000);
    });
  });
});
