/**
 * aiJobManager.completeJob — AI generation completion notifications
 *
 * Regression cover for the defect that stopped the "AI generation completed"
 * email from ever being sent: `completeJob` wrote `status = 'completed'`
 * BEFORE its own duplicate-completion guard, so the guard always matched and
 * the `notifyAiGenerationCompleted` call below it was unreachable. Every
 * module funnels success through this function (Business Profile's AI Auto
 * Fill / Generate / Regenerate reach it via the /ai-context/company-creation/*
 * routes), so the email was missing app-wide.
 *
 * Covers: email sent on success, in-app notification still sent, exactly-once
 * on repeated completion, and no email for failed jobs.
 */

// ---- Mocks -------------------------------------------------------------
const mockNotifyAiGenerationCompleted = jest.fn();
const mockNotifyUser = jest.fn();

jest.mock('../../aiGenerationNotifications', () => ({
  notifyAiGenerationCompleted: (...args: any[]) => mockNotifyAiGenerationCompleted(...args),
}));
jest.mock('../../notificationService', () => ({
  notificationService: { notifyUser: (...args: any[]) => mockNotifyUser(...args) },
}));
// createJob reads the actor from the ambient request context rather than an
// argument, so the in-app notification path needs a userId to be present.
jest.mock('../../../utils/requestContext', () => ({
  getRequestContext: () => ({ userId: 'user-1', organizationId: 'company-1' }),
}));

import { createJob, completeJob, failJob, getJob } from '../aiJobManager';

describe('completeJob', () => {
  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  it('sends the completion email when a job completes successfully', () => {
    const job = createJob('company-creation', 'company-1', 'business-profile');

    completeJob(job.jobId, { name: 'Acme Ltd' }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({
        jobId: job.jobId,
        moduleSource: 'company-creation',
        moduleId: 'business-profile',
        autoFillData: { name: 'Acme Ltd' },
      })
    );
  });

  it('still records the job result and the in-app notification', () => {
    const job = createJob('company-creation', 'company-1', 'business-profile');

    completeJob(job.jobId, { name: 'Acme Ltd' }, 'generated');

    const stored = getJob(job.jobId);
    expect(stored?.status).toBe('completed');
    expect(stored?.progress).toBe(100);
    expect(stored?.result).toEqual({ autoFillData: { name: 'Acme Ltd' }, source: 'generated' });
    expect(mockNotifyUser).toHaveBeenCalledTimes(1);
  });

  it('notifies exactly once when completeJob is invoked twice', () => {
    const job = createJob('company-creation', 'company-1', 'business-profile');

    completeJob(job.jobId, { name: 'Acme Ltd' }, 'generated');
    completeJob(job.jobId, { name: 'Acme Ltd' }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyUser).toHaveBeenCalledTimes(1);
  });

  it('sends no completion email when the job fails', () => {
    const job = createJob('company-creation', 'company-1', 'business-profile');

    failJob(job.jobId, 'model timed out');

    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
    expect(getJob(job.jobId)?.status).toBe('failed');
  });

  it('does nothing for an unknown jobId', () => {
    expect(() => completeJob('no-such-job', {}, 'generated')).not.toThrow();
    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
  });
});

/**
 * Product Quick Generate.
 *
 * Reported separately as "Quick Generate never sends the completion email", but
 * it is the same `completeJob` defect above rather than anything specific to the
 * Product module: POST /ai-context/product/quick-generate creates a job, and its
 * setImmediate worker calls completeJob() on success and failJob() in its catch.
 * These cases pin that module's path so a regression in either direction — a
 * missing email on success, or an email for a failed generation — fails here.
 */
describe('completeJob — Product Quick Generate', () => {
  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  it('sends the completion email for a successful Quick Generate', () => {
    // Mirrors createJob('product', companyId, req.body._moduleId) in the route.
    const job = createJob('product', 'company-1', 'products');

    completeJob(job.jobId, { name: 'Standing Desk', price: 24999 }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({
        jobId: job.jobId,
        moduleSource: 'product',
        autoFillData: { name: 'Standing Desk', price: 24999 },
      })
    );
  });

  it('sends no email when Quick Generate fails', () => {
    const job = createJob('product', 'company-1', 'products');

    failJob(job.jobId, 'AI generation failed');

    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
  });

  it('sends exactly one email per Quick Generate run', () => {
    const first = createJob('product', 'company-1', 'products');
    completeJob(first.jobId, { name: 'Product A' }, 'generated');

    const second = createJob('product', 'company-1', 'products');
    completeJob(second.jobId, { name: 'Product B' }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(2);
    expect(mockNotifyAiGenerationCompleted.mock.calls[0][0].jobId).not.toBe(
      mockNotifyAiGenerationCompleted.mock.calls[1][0].jobId
    );
  });
});

/**
 * AI Generate ICPs / AI Generate Personas.
 *
 * Same story as Quick Generate: both reach completeJob() on success and
 * failJob() in their catch, so the missing email was the shared defect above
 * rather than anything in the ICP module. These run as BULK jobs, so they carry
 * the 'icp-bulk' / 'persona-bulk' sources — that is what reaches the notifier,
 * and what the MODULE_LABELS entries added alongside these tests key off.
 */
describe('completeJob — AI Generate ICPs / Personas', () => {
  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  it('sends the completion email for a successful bulk ICP generation', () => {
    const job = createJob('icp-bulk', 'company-1', 'icp-personas');

    completeJob(job.jobId, { icps: [{ name: 'Mid-market SaaS' }], personas: [] }, 'bulk-generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({ jobId: job.jobId, moduleSource: 'icp-bulk' })
    );
  });

  it('sends the completion email for a successful bulk Persona generation', () => {
    const job = createJob('persona-bulk', 'company-1', 'icp-personas');

    completeJob(job.jobId, { personas: [{ name: 'Ops Olivia' }], icpId: 'icp-1' }, 'bulk-generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({ jobId: job.jobId, moduleSource: 'persona-bulk' })
    );
  });

  it('sends no email when either generation fails', () => {
    const icp = createJob('icp-bulk', 'company-1', 'icp-personas');
    failJob(icp.jobId, 'AI generated no ICPs');

    const persona = createJob('persona-bulk', 'company-1', 'icp-personas');
    failJob(persona.jobId, 'AI generated no complete personas');

    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
  });

  it('sends exactly one email per bulk run', () => {
    const job = createJob('icp-bulk', 'company-1', 'icp-personas');

    completeJob(job.jobId, { icps: [], personas: [] }, 'bulk-generated');
    completeJob(job.jobId, { icps: [], personas: [] }, 'bulk-generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
  });
});

/**
 * Competitor AI Generate + Regenerate.
 *
 * Both /ai-context/competitor/auto-fill|quick-generate and /regenerate/:id
 * create a 'competitor' job and reach completeJob() on success / failJob() in
 * their catch, so the missing email was the shared defect above rather than
 * anything module-specific. Regenerate is the notable one: it is a SEPARATE job
 * from the original generation, so it must produce its own email rather than
 * being swallowed as a repeat of the first.
 */
describe('completeJob — Competitor generate and regenerate', () => {
  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  it('sends the completion email for a successful AI Generate', () => {
    const job = createJob('competitor', 'company-1', 'competitors');

    completeJob(job.jobId, { name: 'Rival Inc' }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({ jobId: job.jobId, moduleSource: 'competitor' })
    );
  });

  it('sends a separate completion email for a Regenerate', () => {
    const generate = createJob('competitor', 'company-1', 'competitors');
    completeJob(generate.jobId, { name: 'Rival Inc' }, 'generated');

    const regenerate = createJob('competitor', 'company-1', 'competitors');
    completeJob(regenerate.jobId, { name: 'Rival Inc' }, 'regenerated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(2);
    expect(mockNotifyAiGenerationCompleted.mock.calls[1][0].jobId).toBe(regenerate.jobId);
  });

  it('sends no email when a regeneration fails', () => {
    const job = createJob('competitor', 'company-1', 'competitors');

    failJob(job.jobId, 'AI regeneration failed');

    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
  });

  it('sends exactly one email per regeneration', () => {
    const job = createJob('competitor', 'company-1', 'competitors');

    completeJob(job.jobId, { name: 'Rival Inc' }, 'regenerated');
    completeJob(job.jobId, { name: 'Rival Inc' }, 'regenerated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
  });
});

/**
 * Executive CV in-app notification grouping.
 *
 * notificationService collapses an unread notification carrying the same
 * `groupKey` for an hour. With the module-wide key every CV generated while an
 * earlier notification was still unread was absorbed into it and the user saw
 * nothing — the reported "sometimes it notifies, sometimes it doesn't".
 * Executive CV is one deliberate artifact per run, so its key is per-job.
 */
describe('completeJob — notification grouping', () => {
  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  const groupKeyOf = (call: number) => mockNotifyUser.mock.calls[call][1].groupKey;

  it('gives each Executive CV generation its own groupKey', () => {
    const first = createJob('executive-cv', 'company-1', 'executive-cv');
    completeJob(first.jobId, { cvId: 'cv-1', version: 1 }, 'generated');

    const second = createJob('executive-cv', 'company-1', 'executive-cv');
    completeJob(second.jobId, { cvId: 'cv-2', version: 1 }, 'generated');

    expect(mockNotifyUser).toHaveBeenCalledTimes(2);
    expect(groupKeyOf(0)).toContain(first.jobId);
    expect(groupKeyOf(1)).toContain(second.jobId);
    expect(groupKeyOf(0)).not.toBe(groupKeyOf(1));
  });

  it('keeps the shared module-wide groupKey for bulk-run modules', () => {
    const a = createJob('social-media-os', 'company-1', 'social-media-os');
    completeJob(a.jobId, { title: 'Post A' }, 'generated');

    const b = createJob('social-media-os', 'company-1', 'social-media-os');
    completeJob(b.jobId, { title: 'Post B' }, 'generated');

    expect(groupKeyOf(0)).toBe('ai.completed:social-media-os:company-1');
    expect(groupKeyOf(1)).toBe(groupKeyOf(0));
  });

  it('still de-duplicates a repeated completion of the same CV job', () => {
    const job = createJob('executive-cv', 'company-1', 'executive-cv');

    completeJob(job.jobId, { cvId: 'cv-1', version: 1 }, 'generated');
    completeJob(job.jobId, { cvId: 'cv-1', version: 1 }, 'generated');

    expect(mockNotifyUser).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
  });
});

/**
 * The six modules reported as "generation completes but no email arrives":
 * Presentations & Pitches, Magazine & Sponsorship, SOP Management, Referral
 * Programmes, Membership Plans and Loyalty Program.
 *
 * Their generation path was never broken — each already completes through
 * completeJob and so already reaches notifyAiGenerationCompleted. What was
 * missing was their module key in USER_EMAIL_MODULE_PREFIXES over in
 * aiGenerationNotifications, which left the (disabled-by-default) super-admin
 * ops list as the only address the notifier could ever resolve.
 *
 * These cases pin the half that lives here: the notifier is reached exactly
 * once per completion, carries the module key the allow-list matches on, and
 * carries the userId the recipient address is looked up from.
 */
describe('completeJob — modules reported as not emailing on completion', () => {
  const MODULES: Array<[string, string]> = [
    ['Presentations & Pitches', 'presentation-generator'],
    ['Magazine & Sponsorship', 'magazine-sponsorship'],
    ['SOP Management', 'sop'],
    ['Referral Programmes', 'referral'],
    ['Membership Plans', 'membership-plan'],
    ['Loyalty Program', 'loyalty-programme'],
  ];

  beforeEach(() => {
    mockNotifyAiGenerationCompleted.mockClear();
    mockNotifyUser.mockClear();
  });

  it.each(MODULES)('%s reaches the email notifier with its module key and userId', (_label, source) => {
    const job = createJob(source, 'company-1', `${source}-1`);

    completeJob(job.jobId, { generated: true }, 'generated');

    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledTimes(1);
    expect(mockNotifyAiGenerationCompleted).toHaveBeenCalledWith(
      expect.objectContaining({
        jobId: job.jobId,
        moduleSource: source,
        moduleId: `${source}-1`,
        // The allow-list resolves the recipient from this; without it the
        // notifier has no address to send the user's copy to.
        userId: 'user-1',
      })
    );
  });

  // Guards the duplicate that merges have now re-introduced into completeJob
  // twice. Each time, every completed generation raised two identical
  // dashboard notifications.
  it.each(MODULES)('%s raises exactly one in-app notification per completion', (_label, source) => {
    const job = createJob(source, 'company-1', `${source}-1`);

    completeJob(job.jobId, { generated: true }, 'generated');

    expect(mockNotifyUser).toHaveBeenCalledTimes(1);
  });

  it.each(MODULES)('%s sends no completion email when the generation fails', (_label, source) => {
    const job = createJob(source, 'company-1', `${source}-1`);

    failJob(job.jobId, 'model timed out');

    expect(mockNotifyAiGenerationCompleted).not.toHaveBeenCalled();
    expect(getJob(job.jobId)?.status).toBe('failed');
  });
});
