/** Deterministic Generate All scale proof: one manifest fans out to max-20 backend jobs. */
const LIVE = /(api\.press\.zone|generativelanguage\.googleapis\.com|aiplatform\.googleapis\.com)/i;
const TEST_KEY = /^(?:test|fake|mock|fixture|dummy|placeholder)[-_]/i;
function rejectLiveConfig(): void {
  if (process.env.NODE_ENV !== 'test') throw new Error('This suite requires NODE_ENV=test');
  for (const key of ['API_URL', 'BACKEND_URL', 'TRANSLATION_API_URL', 'GEMINI_API_URL', 'GOOGLE_GEMINI_API_URL']) {
    if (LIVE.test(process.env[key] || '')) throw new Error(`Live translation URL is forbidden: ${key}`);
  }
  for (const key of ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY']) {
    const value = process.env[key];
    if (value && !TEST_KEY.test(value)) throw new Error(`Live provider key is forbidden: ${key}`);
  }
}
rejectLiveConfig();

const processors = new Map<string, (job: any) => Promise<any>>();
const queueAdd = jest.fn().mockResolvedValue(undefined);
const queueProcess = jest.fn((name: string, ...args: unknown[]) => processors.set(name, args.at(-1) as (job: any) => Promise<any>));
const drainBulkQueue = jest.fn();
const translateStructured = jest.fn();
const deliverWebhook = jest.fn();
const lockProcessingJob = jest.fn();
const loggerError = jest.fn();

jest.mock('../../queue', () => ({ translationQueue: {
  process: queueProcess, add: queueAdd, on: jest.fn(), close: jest.fn(), isReady: jest.fn().mockResolvedValue(true),
} }));
jest.mock('../../services/geminiClient', () => ({ geminiClient: { translate: jest.fn(), translateBulk: jest.fn(), translateStructured } }));
jest.mock('../../services/webhookService', () => ({ deliverWebhook }));
jest.mock('../../utils/logger', () => ({ logger: { info: jest.fn(), warn: jest.fn(), error: loggerError } }));
jest.mock('../../utils/metrics', () => ({ trackTranslationJob: jest.fn(), trackTokensProcessed: jest.fn() }));
jest.mock('../../workers/orphanDisputeReconciler', () => ({ reconcilePermanentOrphanDisputes: jest.fn() }));
jest.mock('../../queue/bulkQueueDispatcher', () => ({ configureBulkQueueDispatcher: jest.fn(), drainBulkQueue, startBulkQueueDispatcher: jest.fn(), stopBulkQueueDispatcher: jest.fn() }));
jest.mock('../../workerLifecycle', () => ({ startWorkerRuntime: jest.fn(), shutdownWorkerRuntime: jest.fn() }));
jest.mock('../../workerLock', () => ({ JobCancellationWonError: class extends Error {}, lockProcessingJob }));
jest.mock('../../workerHeartbeat', () => ({ getWorkerHeartbeatKey: jest.fn(() => 'worker:fixture'), WorkerHeartbeat: class { start(): void {} stop(): Promise<void> { return Promise.resolve(); } } }));
jest.mock('../../config', () => ({ getRedisUrl: jest.fn(() => 'redis://fixture.invalid'), initializeConfigFromDatabase: jest.fn(), startConfigSubscription: jest.fn(), useEnvironmentConfigFallback: jest.fn() }));

process.env.WORKER_ID = 'bulk-content-scale-fixture';
process.env.WORKER_GEMINI_CONCURRENCY = '8';

import { prismaMock } from '../setup';
import { bulkContentJobSchema, submitBulkContentJob } from '../../routes/jobs';
import { registerProcessors } from '../../worker';
import { Tone } from '../../types';
import type { BulkContentItemInput, BulkContentTranslationJobData } from '../../types';
import { countStructuredCharacters } from '../../utils/structuredFields';

const CHUNK = 20;
const MANIFEST = 'generate-all-fixture-manifest';
const CALLBACK = 'https://fixture.invalid/callback';
const SECRET = 'fixture-secret-0123456789';
const itemsFor = (n: number): BulkContentItemInput[] => Array.from({ length: n }, (_, i) => ({
  ref: `content:${i + 1}:es`, targetLang: 'es', fields: {
    title: `Title ${i + 1}`, excerpt: `Excerpt ${i + 1}`, content: `Body ${i + 1}`,
    'acf.subtitle': `Subtitle ${i + 1}`, 'seo.description': `Description ${i + 1}`,
  },
}));
const submissionId = (i: number): string => `00000000-0000-4000-8000-${String(i).padStart(12, '0')}`;
const deterministic = (fields: Record<string, string>, lang: string): Record<string, string> =>
  Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, `fixture:${lang}:${key}:${value}`]));

type StoredJob = {
  id: string; status: 'pending' | 'processing' | 'completed'; submissionId: string; clientJobId: string;
  secret: string; payload: any; translation?: string; characters?: number;
};
type Outbox = { jobId: string; event: string; payload: any; status: 'pending' | 'delivering' | 'delivered'; attempts: number; claimedAt?: Date };
let jobs: Map<string, StoredJob>;
let outbox: Outbox[];
let bull: Array<{ jobId: string; data: BulkContentTranslationJobData }>;
let nextJob: number;
let activeCalls: number;
let maxActiveCalls: number;

function resetFakes(): void {
  jobs = new Map(); outbox = []; bull = []; nextJob = 1; activeCalls = 0; maxActiveCalls = 0;
  processors.clear(); queueProcess.mockClear(); queueAdd.mockReset(); queueAdd.mockResolvedValue(undefined);
  drainBulkQueue.mockReset(); translateStructured.mockReset(); loggerError.mockReset(); lockProcessingJob.mockReset(); lockProcessingJob.mockResolvedValue(undefined);
  deliverWebhook.mockReset(); deliverWebhook.mockImplementation(async (_id: string, _payload: unknown, url: string) => {
    if (LIVE.test(url)) throw new Error('live callback URL in deterministic fixture');
    return { success: true };
  });
  const webhookOutbox = {
    findMany: jest.fn(async () => outbox.filter((row) => row.status === 'pending').map((row) => ({
      ...row,
      id: `${row.jobId}:outbox`,
      job: { callback_url: CALLBACK, callback_secret: SECRET },
    }))),
    updateMany: jest.fn(async (args: any) => {
      if (args.where?.status === 'pending' && args.where.attempts?.lt !== undefined) {
        const row = outbox.find((candidate) => candidate.status === 'pending');
        if (!row) return { count: 0 }; row.status = 'delivering'; row.attempts += 1; row.claimedAt = args.data.claimed_at; return { count: 1 };
      }
      if (args.where?.status === 'delivering' && args.where.claimed_at) {
        const row = outbox.find((candidate) => candidate.status === 'delivering');
        if (!row) return { count: 0 }; row.status = args.data.status || 'delivered'; return { count: 1 };
      }
      return { count: 0 };
    }),
    create: jest.fn(async (args: any) => {
      const row: Outbox = { jobId: args.data.job_id, event: args.data.event, payload: args.data.payload, status: 'pending', attempts: 0, job: { callback_url: CALLBACK, callback_secret: SECRET } };
      outbox.push(row); return row;
    }),
  };
  Object.assign(prismaMock, { webhookOutbox });
  (prismaMock.translationJob.findUnique as unknown as jest.Mock).mockImplementation(async (args: any) => {
    if (args.where?.submission_id) return [...jobs.values()].find((job) => job.submissionId === args.where.submission_id) || null;
    return jobs.get(args.where?.id) || null;
  });
  (prismaMock.translationJob.create as unknown as jest.Mock).mockImplementation(async ({ data }: any) => {
    const id = `fixture-job-${nextJob++}`;
    jobs.set(id, { id, status: 'pending', submissionId: data.submission_id, clientJobId: data.client_job_id, secret: data.callback_secret, payload: data.queue_payload });
    return { ...data, id, status: 'pending' };
  });
  (prismaMock.translationJob.updateMany as unknown as jest.Mock).mockImplementation(async (args: any) => {
    const job = jobs.get(args.where?.id);
    if (!job) return { count: 0 };
    if (Array.isArray(args.where?.OR) && job.status === 'pending') {
      job.status = 'processing'; return { count: 1 };
    }
    return { count: 0 };
  });
  (prismaMock.translationJob.update as unknown as jest.Mock).mockImplementation(async (args: any) => {
    const job = jobs.get(args.where.id); if (!job) throw new Error('missing fixture job');
    job.status = args.data.status; job.translation = args.data.translation; job.characters = args.data.characters_used; return job;
  });
  prismaMock.creditTransaction.findFirst.mockResolvedValue({ balance_after: 1_000_000 } as never);
  prismaMock.creditTransaction.create.mockResolvedValue({} as never); prismaMock.user.update.mockResolvedValue({} as never);
  (prismaMock.$transaction as jest.Mock).mockImplementation(async (callback: (tx: typeof prismaMock) => Promise<unknown>) => callback(prismaMock));
  drainBulkQueue.mockImplementation(async (jobId: string) => {
    const job = jobs.get(jobId); if (!job) throw new Error('missing fixture queue job');
    bull.push({ jobId, data: { ...job.payload, callbackSecret: job.secret } as BulkContentTranslationJobData });
    await queueAdd('bulk-content', job.payload, { jobId });
  });
  translateStructured.mockImplementation(async (fields: Record<string, string>, _source: string, lang: string) => {
    activeCalls += 1; maxActiveCalls = Math.max(maxActiveCalls, activeCalls); await Promise.resolve(); activeCalls -= 1;
    const translatedFields = deterministic(fields, lang); return { fields: translatedFields, translatedFields };
  });
}

function worker(): (job: any) => Promise<any> {
  registerProcessors(); const value = processors.get('bulk-content');
  if (!value) throw new Error('bulk-content processor not registered'); return value;
}

function status(itemCount: number): any {
  const completed = [...jobs.values()].filter((job) => job.status === 'completed').length;
  return {
    status: completed === 0 ? 'queued' : completed === jobs.size ? 'completed' : 'processing',
    manifestId: MANIFEST, backendJobs: jobs.size, completedBackendJobs: completed,
    storedCount: [...jobs.values()].reduce((n, job) => n + (job.translation ? JSON.parse(job.translation).length : 0), 0),
    total: itemCount,
  };
}

describe('bulk-content worker Generate All scale', () => {
  beforeEach(() => { rejectLiveConfig(); resetFakes(); });
  it.each([100, 1000])('admits %d items once, fans out exact max-20 jobs, and reconciles all results', async (itemCount) => {
    const items = itemsFor(itemCount); const backendJobs = itemCount / CHUNK; const expectedChars = items.reduce((n, item) => n + countStructuredCharacters(item.fields || {}), 0);
    let clicks = 0; const manifestRequests: any[] = [];
    clicks += 1; manifestRequests.push({ manifestId: MANIFEST, items });
    for (let offset = 0; offset < itemCount; offset += CHUNK) {
      const chunk = items.slice(offset, offset + CHUNK); const index = offset / CHUNK + 1;
      const request = { userId: 'fixture-user', plugin: 'international', siteUrl: 'https://fixture.invalid/site', submissionId: submissionId(index), clientJobId: `${MANIFEST}:${index}`, sourceLang: 'en', items: chunk, callbackUrl: CALLBACK, callbackSecret: SECRET, tone: Tone.NEUTRAL };
      const { userId: _userId, plugin: _plugin, siteUrl: _siteUrl, callbackSecret: _callbackSecret, ...publicRequest } = request;
      expect(bulkContentJobSchema.parse(publicRequest).items).toHaveLength(chunk.length);
      await submitBulkContentJob(request);
    }
    expect(clicks).toBe(1); expect(manifestRequests).toHaveLength(1); expect(manifestRequests[0].items).toHaveLength(itemCount);
    expect(jobs.size).toBe(backendJobs); expect(bull).toHaveLength(backendJobs); expect(queueAdd).toHaveBeenCalledTimes(backendJobs);
    expect(queueAdd.mock.calls.every((call) => call[1].items.length <= CHUNK)).toBe(true);
    expect(queueAdd.mock.calls.every((call) => !JSON.stringify(call[1]).includes(SECRET))).toBe(true);
    expect(status(itemCount)).toEqual({ status: 'queued', manifestId: MANIFEST, backendJobs, completedBackendJobs: 0, storedCount: 0, total: itemCount });

    const process = worker(); for (const job of bull) await process({ data: job.data, opts: { attempts: 1 }, attemptsMade: 0 });
    expect(status(itemCount)).toEqual({ status: 'completed', manifestId: MANIFEST, backendJobs, completedBackendJobs: backendJobs, storedCount: itemCount, total: itemCount });
    expect(outbox).toHaveLength(backendJobs); expect(outbox.every((row) => row.event === 'bulk_content_translation.completed' && row.status === 'delivered' && row.payload.failed_count === 0)).toBe(true);
    expect(outbox.reduce((n, row) => n + row.payload.results.length, 0)).toBe(itemCount); expect(deliverWebhook).toHaveBeenCalledTimes(backendJobs); expect(loggerError).not.toHaveBeenCalled();
    const results = [...jobs.values()].flatMap((job) => JSON.parse(job.translation || 'null')) as Array<Record<string, any>>;
    expect(results).toHaveLength(itemCount); expect(new Set(results.map((row) => row.ref)).size).toBe(itemCount); expect(results.every((row) => row.status === 'completed' && !row.error)).toBe(true);
    expect(results.map((row) => row.ref).sort()).toEqual(items.map((item) => item.ref).sort());
    for (const row of results) {
      const source = items.find((item) => item.ref === row.ref); expect(source).toBeDefined();
      expect(row.fields).toEqual(deterministic(source?.fields || {}, source?.targetLang || 'es'));
      expect(Object.values(row.fields).every((value) => typeof value === 'string' && value.length > 0)).toBe(true);
    }
    // One provider call/item is current behavior and remains a production scalability risk.
    expect(translateStructured).toHaveBeenCalledTimes(itemCount); expect(maxActiveCalls).toBeGreaterThan(0); expect(maxActiveCalls).toBeLessThanOrEqual(8);
    expect(prismaMock.creditTransaction.create).toHaveBeenCalledTimes(backendJobs);
    expect((prismaMock.creditTransaction.create.mock.calls as any[]).reduce((n, call) => n + call[0].data.amount, 0)).toBe(-expectedChars);

    const callsBeforeReload = translateStructured.mock.calls.length;
    expect(status(itemCount).storedCount).toBe(itemCount);
    for (const job of bull) expect(await process({ data: job.data, opts: { attempts: 1 }, attemptsMade: 0 })).toEqual({ translation: jobs.get(job.jobId)?.translation });
    expect(translateStructured).toHaveBeenCalledTimes(callsBeforeReload); expect(queueAdd).toHaveBeenCalledTimes(backendJobs); expect(prismaMock.creditTransaction.create).toHaveBeenCalledTimes(backendJobs); expect(clicks).toBe(1);
  }, 30000);
});
