/**
 * Deterministic Generate All scale proof for the async bulk-content worker.
 *
 * This is intentionally a unit-only harness. The provider, queue, webhook, and
 * durable database are all fakes, and every URL is a fixture URL. Keep the
 * guard below: this test must fail rather than silently switching to a live
 * translation configuration.
 */

const LIVE_TRANSLATION_HOST = /(api\.press\.zone|generativelanguage\.googleapis\.com|aiplatform\.googleapis\.com)/i;
const TEST_KEY = /^(?:test|fake|mock|fixture|dummy|placeholder)[-_]/i;
const LIVE_CONFIGURATION_KEYS = [
  'API_URL',
  'BACKEND_URL',
  'TRANSLATION_API_URL',
  'GEMINI_API_URL',
  'GOOGLE_GEMINI_API_URL',
  'GOOGLE_GENERATIVE_AI_BASE_URL',
] as const;
const LIVE_PROVIDER_KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY'] as const;

function rejectLiveTranslationConfiguration(): void {
  if (process.env.NODE_ENV !== 'test') {
    throw new Error('workerBulkContentScale.test.ts requires NODE_ENV=test');
  }

  const liveUrls = LIVE_CONFIGURATION_KEYS
    .map((name) => ({ name, value: process.env[name] }))
    .filter(({ value }) => value !== undefined && LIVE_TRANSLATION_HOST.test(value));
  if (liveUrls.length > 0) {
    throw new Error(`Live translation URL configuration is forbidden: ${liveUrls.map(({ name }) => name).join(', ')}`);
  }

  const liveKeys = LIVE_PROVIDER_KEY_NAMES
    .map((name) => ({ name, value: process.env[name] }))
    .filter(({ value }) => value !== undefined && value !== '' && !TEST_KEY.test(value));
  if (liveKeys.length > 0) {
    throw new Error(`Live translation provider keys are forbidden: ${liveKeys.map(({ name }) => name).join(', ')}`);
  }
}

rejectLiveTranslationConfiguration();

const mockProcessors = new Map<string, (job: any) => Promise<unknown>>();
const mockQueueAdd = jest.fn();
const mockQueueProcess = jest.fn((name: string, ...rest: unknown[]) => {
  // Bull accepts process(name, handler) or process(name, concurrency, handler).
  const processor = rest[rest.length - 1] as (job: any) => Promise<unknown>;
  mockProcessors.set(name, processor);
});
const mockTranslateStructured = jest.fn();
const mockDeliverWebhook = jest.fn();
const mockLockProcessingJob = jest.fn();
const mockLoggerError = jest.fn();

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

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

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

const CALLBACK_URL = 'https://fixture.invalid/translation-callback';
const CALLBACK_SECRET = 'fixture-callback-secret-0123456789';
const SUBMISSION_ID = '00000000-0000-4000-8000-000000000777';
const JOB_ID = 'fixture-bulk-content-job';

interface StoredJob {
  id: string;
  status: 'queued' | 'processing' | 'completed';
  submissionId: string;
  translation?: string;
  charactersUsed?: number;
}

interface StoredOutboxEntry {
  id: string;
  job_id: string;
  event: string;
  payload: Record<string, any>;
  status: 'pending' | 'delivering' | 'delivered';
  attempts: number;
  claimed_at?: Date;
  job: { callback_url: string; callback_secret: string };
}

interface GenerateAllHarness {
  readonly body: BulkContentTranslationJobData;
  readonly clickCount: number;
  readonly requestBodies: BulkContentTranslationJobData[];
  clickGenerateAll(): Promise<{ status: 202; jobId: string; submissionId: string }>;
  reloadStatus(): {
    status: StoredJob['status'];
    jobId: string;
    submissionId: string;
    storedCount: number;
  };
}

let storedJob: StoredJob;
let storedOutbox: StoredOutboxEntry[];
let providerActiveCalls: number;
let providerMaxActiveCalls: number;

function makeItems(itemCount: number): BulkContentItemInput[] {
  return Array.from({ length: itemCount }, (_, index) => ({
    ref: `content:${index + 1}:es`,
    targetLang: 'es',
    fields: {
      title: `Fixture title ${index + 1}`,
      excerpt: `Fixture excerpt ${index + 1}`,
      content: `Fixture body ${index + 1}`,
      'acf.subtitle': `Fixture subtitle ${index + 1}`,
      'seo.description': `Fixture description ${index + 1}`,
    },
  }));
}

function makeJobData(itemCount: number): BulkContentTranslationJobData {
  return {
    jobId: JOB_ID,
    submissionId: SUBMISSION_ID,
    clientJobId: 'generate-all-fixture-777',
    userId: 'fixture-user',
    items: makeItems(itemCount),
    sourceLang: 'en',
    tone: Tone.NEUTRAL,
    callbackUrl: CALLBACK_URL,
    callbackSecret: CALLBACK_SECRET,
  };
}

function deterministicTranslation(
  fields: Record<string, string>,
  targetLang: string
): Record<string, string> {
  return Object.fromEntries(
    Object.entries(fields).map(([key, value]) => [
      key,
      `fixture-translation:${targetLang}:${key}:${value}`,
    ])
  );
}

function makeHarness(itemCount: number): GenerateAllHarness {
  const body = makeJobData(itemCount);
  let clickCount = 0;
  const requestBodies: BulkContentTranslationJobData[] = [];

  return {
    body,
    get clickCount() {
      return clickCount;
    },
    requestBodies,
    async clickGenerateAll() {
      clickCount += 1;
      requestBodies.push(body);

      if (storedJob.status !== 'queued') {
        throw new Error('Generate All admission must happen before worker execution');
      }

      // The persisted queue payload is secret-free. The worker receives the
      // callback secret only when the durable dispatcher reconstructs Bull data.
      const queuePayload = { ...body } as Partial<BulkContentTranslationJobData>;
      delete queuePayload.callbackSecret;
      await mockQueueAdd('bulk-content', queuePayload, { jobId: JOB_ID });

      return { status: 202, jobId: JOB_ID, submissionId: SUBMISSION_ID };
    },
    reloadStatus() {
      const storedCount = storedJob.translation
        ? JSON.parse(storedJob.translation).length
        : 0;
      return {
        status: storedJob.status,
        jobId: storedJob.id,
        submissionId: storedJob.submissionId,
        storedCount,
      };
    },
  };
}

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

function resetDurableFakes(): void {
  storedJob = {
    id: JOB_ID,
    status: 'queued',
    submissionId: SUBMISSION_ID,
  };
  storedOutbox = [];
  providerActiveCalls = 0;
  providerMaxActiveCalls = 0;

  mockProcessors.clear();
  mockQueueProcess.mockClear();
  mockQueueAdd.mockReset();
  mockQueueAdd.mockResolvedValue(undefined);
  mockTranslateStructured.mockReset();
  mockDeliverWebhook.mockImplementation(async (...args: any[]) => {
    const callbackUrl = args[2];
    if (typeof callbackUrl !== 'string' || LIVE_TRANSLATION_HOST.test(callbackUrl)) {
      throw new Error('Fixture webhook URL unexpectedly points at a live translation host');
    }
    return { success: true };
  });
  mockLockProcessingJob.mockReset();
  mockLockProcessingJob.mockResolvedValue(undefined);
  mockLoggerError.mockReset();

  const outbox = {
    findMany: jest.fn(async () => storedOutbox.filter((entry) => entry.status === 'pending')),
    updateMany: jest.fn(async (args: any) => {
      const where = args.where || {};
      if (where.status === 'pending' && where.attempts?.lt !== undefined) {
        const entry = storedOutbox.find((candidate) => candidate.status === 'pending');
        if (!entry) return { count: 0 };
        entry.status = 'delivering';
        entry.attempts += 1;
        entry.claimed_at = args.data.claimed_at;
        return { count: 1 };
      }
      if (where.status === 'delivering' && where.claimed_at) {
        const entry = storedOutbox.find((candidate) => candidate.status === 'delivering');
        if (!entry) return { count: 0 };
        entry.status = args.data.status || 'delivered';
        entry.claimed_at = args.data.claimed_at ?? undefined;
        return { count: 1 };
      }
      return { count: 0 };
    }),
    create: jest.fn(async (args: any) => {
      const entry: StoredOutboxEntry = {
        id: `fixture-delivery-${storedOutbox.length + 1}`,
        job_id: args.data.job_id,
        event: args.data.event,
        payload: args.data.payload,
        status: 'pending',
        attempts: 0,
        job: { callback_url: CALLBACK_URL, callback_secret: CALLBACK_SECRET },
      };
      storedOutbox.push(entry);
      return entry;
    }),
  };

  Object.assign(prismaMock, { webhookOutbox: outbox });
  prismaMock.translationJob.updateMany.mockImplementation(async (args: any) => {
    if (args.where?.id !== JOB_ID || args.where?.status !== 'pending') {
      return { count: 0 } as never;
    }
    if (storedJob.status !== 'queued') {
      return { count: 0 } as never;
    }
    storedJob.status = 'processing';
    return { count: 1 } as never;
  });
  prismaMock.translationJob.findUnique.mockImplementation(async () => ({
    id: storedJob.id,
    status: storedJob.status,
    translation: storedJob.translation,
  }) as never);
  (prismaMock.translationJob.update as unknown as jest.Mock).mockImplementation(async (args: any) => {
    storedJob.status = args.data.status;
    storedJob.translation = args.data.translation;
    storedJob.charactersUsed = args.data.characters_used;
    return storedJob;
  });
  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)
  );
}

function installDeterministicProvider(): void {
  mockTranslateStructured.mockImplementation(async (
    fields: Record<string, string>,
    _sourceLang: string,
    targetLang: string,
  ) => {
    if (LIVE_TRANSLATION_HOST.test(CALLBACK_URL)) {
      throw new Error('Fixture callback URL unexpectedly points at a live translation host');
    }
    providerActiveCalls += 1;
    providerMaxActiveCalls = Math.max(providerMaxActiveCalls, providerActiveCalls);
    await Promise.resolve();
    providerActiveCalls -= 1;

    const translatedFields = deterministicTranslation(fields, targetLang);
    return { fields: translatedFields, translatedFields };
  });
}

describe('bulk-content worker Generate All scale', () => {
  beforeEach(() => {
    rejectLiveTranslationConfiguration();
    resetDurableFakes();
    installDeterministicProvider();
  });

  it.each([100, 1000])(
    'admits exactly %d items once and stores every deterministic translation without loss',
    async (itemCount) => {
      const harness = makeHarness(itemCount);
      const expectedItems = harness.body.items;
      const expectedCharacters = expectedItems.reduce(
        (total, item) => total + countStructuredCharacters(item.fields || {}),
        0
      );

      const admission = await harness.clickGenerateAll();
      expect(admission).toEqual({ status: 202, jobId: JOB_ID, submissionId: SUBMISSION_ID });
      expect(harness.clickCount).toBe(1);
      expect(harness.requestBodies).toHaveLength(1);
      expect(harness.requestBodies[0].items).toHaveLength(itemCount);
      expect(mockQueueAdd).toHaveBeenCalledTimes(1);
      expect(JSON.stringify(mockQueueAdd.mock.calls[0][1])).not.toContain(CALLBACK_SECRET);

      const queued = mockQueueAdd.mock.calls[0][1] as BulkContentTranslationJobData;
      const queuedWithSecret: BulkContentTranslationJobData = {
        ...queued,
        callbackSecret: CALLBACK_SECRET,
      };
      expect(harness.reloadStatus()).toEqual({
        status: 'queued',
        jobId: JOB_ID,
        submissionId: SUBMISSION_ID,
        storedCount: 0,
      });

      const result = await processor()({
        data: queuedWithSecret,
        opts: { attempts: 1 },
        attemptsMade: 0,
      });

      expect(result.totalFailedCount).toBe(0);
      expect(result.totalCharactersUsed).toBe(expectedCharacters);
      expect(storedJob.status).toBe('completed');
      expect(storedJob.charactersUsed).toBe(expectedCharacters);
      expect(storedJob.translation).toEqual(expect.any(String));
      expect(storedOutbox).toHaveLength(1);
      expect(storedOutbox[0].event).toBe('bulk_content_translation.completed');
      expect(storedOutbox[0].status).toBe('delivered');
      expect(storedOutbox[0].payload.failed_count).toBe(0);
      expect(storedOutbox[0].payload.total_characters_used).toBe(expectedCharacters);
      expect(mockDeliverWebhook).toHaveBeenCalledTimes(1);
      expect(mockLoggerError).not.toHaveBeenCalled();

      const storedResults = JSON.parse(storedJob.translation || 'null') as Array<Record<string, any>>;
      expect(storedResults).toHaveLength(itemCount);
      expect(storedResults.map((item) => item.ref)).toEqual(expectedItems.map((item) => item.ref));
      expect(new Set(storedResults.map((item) => item.ref)).size).toBe(itemCount);
      expect(storedResults.every((item) => item.status === 'completed')).toBe(true);
      expect(storedResults.some((item) => item.error !== undefined)).toBe(false);

      storedResults.forEach((storedResult, index) => {
        const expectedFields = deterministicTranslation(
          expectedItems[index].fields || {},
          expectedItems[index].targetLang
        );
        expect(storedResult.fields).toEqual(expectedFields);
        expect(Object.keys(storedResult.fields).sort()).toEqual(Object.keys(expectedFields).sort());
        expect(Object.values(storedResult.fields).every((value) => typeof value === 'string' && value.length > 0)).toBe(true);
      });

      expect(mockTranslateStructured).toHaveBeenCalledTimes(itemCount);
      expect(providerMaxActiveCalls).toBeGreaterThan(0);
      expect(providerMaxActiveCalls).toBeLessThanOrEqual(8);
      expect(prismaMock.creditTransaction.create).toHaveBeenCalledTimes(1);
      expect((prismaMock.creditTransaction.create.mock.calls[0][0] as any).data.amount).toBe(-expectedCharacters);
      expect(harness.reloadStatus()).toEqual({
        status: 'completed',
        jobId: JOB_ID,
        submissionId: SUBMISSION_ID,
        storedCount: itemCount,
      });

      // A page reload resumes from the durable terminal record. It must not
      // re-admit the click or call the provider a second time.
      const providerCallsBeforeReload = mockTranslateStructured.mock.calls.length;
      const resumed = harness.reloadStatus();
      expect(resumed).toMatchObject({ status: 'completed', jobId: JOB_ID, submissionId: SUBMISSION_ID });
      expect(resumed.storedCount).toBe(itemCount);

      const replay = await processor()({
        data: queuedWithSecret,
        opts: { attempts: 1 },
        attemptsMade: 0,
      });
      expect(replay).toEqual({ translation: storedJob.translation });
      expect(mockTranslateStructured).toHaveBeenCalledTimes(providerCallsBeforeReload);
      expect(mockQueueAdd).toHaveBeenCalledTimes(1);
      expect(prismaMock.creditTransaction.create).toHaveBeenCalledTimes(1);
      expect(harness.clickCount).toBe(1);
      expect(harness.requestBodies).toHaveLength(1);
    },
  );
});
