const mockProcessors = new Map<string, (job: any) => Promise<unknown>>();
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 mockQueueAdd = jest.fn().mockResolvedValue(undefined);
const mockDeliverWebhook = jest.fn().mockResolvedValue({ success: true });
const mockTranslateStructured = jest.fn();
const mockLockProcessingJob = jest.fn().mockResolvedValue(undefined);
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(),
    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:test'),
  WorkerHeartbeat: class WorkerHeartbeat {
    start(): void {}
    stop(): Promise<void> { return Promise.resolve(); }
  },
}));
jest.mock('../../config', () => ({
  getRedisUrl: jest.fn(() => 'redis://localhost:6379'),
  initializeConfigFromDatabase: jest.fn(),
  startConfigSubscription: jest.fn(),
  useEnvironmentConfigFallback: jest.fn(),
}));

process.env.WORKER_ID = 'site-content-test-worker';

import { prismaMock } from '../setup';
import { registerProcessors } from '../../worker';
import { Tone } from '../../types';
import type { SiteContentJobSubmitRequest } from '../../services/siteContentTranslation';

const request: SiteContentJobSubmitRequest = {
  resourceType: 'site_content',
  contractVersion: 1,
  sourceLang: 'en',
  targetLang: 'he',
  sourceRevision: 'a'.repeat(64),
  attemptToken: 'b'.repeat(64),
  segments: [{
    id: `seg_${'A'.repeat(32)}`,
    text: 'Heading',
    context: { block_name: 'core/heading' },
  }],
  exceptions: [{ text: 'Heading', match_type: 'exact' }],
  tone: Tone.NEUTRAL,
  callbackUrl: 'https://site.example.test/callback',
  callbackSecret: 'c'.repeat(64),
  clientJobId: 'site-content-41',
};

const jobData = {
  jobId: 'job-1',
  clientJobId: request.clientJobId,
  userId: 'user-1',
  siteContent: request,
  exceptions: request.exceptions,
  sourceLang: request.sourceLang,
  targetLang: request.targetLang,
  tone: Tone.NEUTRAL,
  callbackUrl: request.callbackUrl,
  callbackSecret: request.callbackSecret,
};

const mockOutbox = {
  findMany: jest.fn().mockResolvedValue([]),
  updateMany: jest.fn().mockResolvedValue({ count: 0 }),
  create: jest.fn().mockResolvedValue({}),
};
const pendingOutboxEntries: any[] = [];

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

describe('segmented Site Content worker', () => {
  beforeEach(() => {
    mockProcessors.clear();
    mockQueueProcess.mockClear();
    mockDeliverWebhook.mockClear();
    mockTranslateStructured.mockReset();
    mockLockProcessingJob.mockClear();
    mockLoggerError.mockClear();
    pendingOutboxEntries.length = 0;
    mockOutbox.findMany.mockImplementation(async () => pendingOutboxEntries);
    mockOutbox.updateMany.mockResolvedValue({ count: 1 });
    mockOutbox.create.mockImplementation(async (args: any) => {
      const data = args.data;
      const entry = {
        ...data,
        attempts: 0,
        job: {
          callback_url: request.callbackUrl,
          callback_secret: request.callbackSecret,
        },
      };
      pendingOutboxEntries.push(entry);
      return entry;
    });
    Object.assign(prismaMock, { webhookOutbox: mockOutbox });
    (prismaMock.$transaction as jest.Mock).mockImplementation(
      async (callback: (tx: typeof prismaMock) => Promise<unknown>) => callback(prismaMock)
    );
  });

  it('settles translated segments and persists callback attempt authority in the outbox', async () => {
    prismaMock.translationJob.updateMany.mockResolvedValue({ count: 1 });
    prismaMock.subscription.findFirst.mockResolvedValue({ customer_cost_per_char: 0 } as never);
    prismaMock.user.findUnique.mockResolvedValue({ id: 'user-1' } as never);
    prismaMock.creditTransaction.findFirst.mockResolvedValue({ balance_after: 1000 } as never);
    prismaMock.creditTransaction.create.mockResolvedValue({} as never);
    prismaMock.user.update.mockResolvedValue({} as never);
    prismaMock.translationJob.update.mockResolvedValue({} as never);
    mockTranslateStructured.mockResolvedValue({
      fields: { segment_0000: '__EXCPT_0__' },
      translatedFields: { segment_0000: '__EXCPT_0__' },
      tokens_used: 3,
      input_tokens: 1,
      output_tokens: 2,
      processing_time_ms: 5,
      model_used: 'gemini-3.1-flash-lite',
    });

    await processor()({ data: jobData, opts: { attempts: 1 }, attemptsMade: 0 });

    expect(mockTranslateStructured).toHaveBeenCalledWith(
      { segment_0000: '__EXCPT_0__' },
      request.sourceLang,
      request.targetLang,
      Tone.NEUTRAL
    );
    const persisted = prismaMock.translationJob.update.mock.calls[0][0].data.translation;
    expect(typeof persisted).toBe('string');
    if (typeof persisted !== 'string') throw new Error('Expected string translation persistence.');
    expect(JSON.parse(persisted)).toEqual({
      resource_type: 'site_content',
      source_language: 'en',
      target_language: 'he',
      segments: [{ ...request.segments[0], text: 'Heading' }],
    });
    expect(mockOutbox.create).toHaveBeenCalledWith(expect.objectContaining({
      data: expect.objectContaining({
        event: 'translation.completed',
        payload: expect.objectContaining({
          resourceType: 'site_content',
          contractVersion: 1,
          sourceRevision: request.sourceRevision,
          targetLang: request.targetLang,
          attemptToken: request.attemptToken,
          translation: persisted,
        }),
      }),
    }));
    expect(mockDeliverWebhook).toHaveBeenCalledTimes(1);
  });

  it('returns bounded generic failure data while preserving callback attempt authority', async () => {
    prismaMock.translationJob.updateMany
      .mockResolvedValueOnce({ count: 1 })
      .mockResolvedValueOnce({ count: 1 });
    mockTranslateStructured.mockRejectedValue(new Error('provider included private source text'));

    await expect(processor()({
      data: jobData,
      opts: { attempts: 1 },
      attemptsMade: 0,
    })).rejects.toThrow('Site Content translation failed.');

    expect(prismaMock.translationJob.updateMany).toHaveBeenLastCalledWith(expect.objectContaining({
      data: expect.objectContaining({ error_message: 'Site Content translation failed.' }),
    }));
    expect(mockDeliverWebhook).toHaveBeenCalledWith(
      'job-1',
      expect.objectContaining({
        resourceType: 'site_content',
        contractVersion: 1,
        sourceRevision: request.sourceRevision,
        targetLang: request.targetLang,
        attemptToken: request.attemptToken,
        errorMessage: 'Site Content translation failed.',
      }),
      request.callbackUrl,
      request.callbackSecret,
      expect.any(String)
    );
    expect(JSON.stringify(mockDeliverWebhook.mock.calls)).not.toContain('private source text');
    expect(JSON.stringify(mockLoggerError.mock.calls)).not.toContain('private source text');
  });

  it('redacts provider failures before Bull persists a retry failure reason', async () => {
    prismaMock.translationJob.updateMany
      .mockResolvedValueOnce({ count: 1 })
      .mockResolvedValueOnce({ count: 1 });
    mockTranslateStructured.mockRejectedValue(new Error('provider included private source text'));

    await expect(processor()({
      data: jobData,
      opts: { attempts: 2 },
      attemptsMade: 0,
    })).rejects.toThrow('Site Content translation failed.');

    expect(prismaMock.translationJob.updateMany).toHaveBeenLastCalledWith(expect.objectContaining({
      data: expect.objectContaining({ status: 'pending' }),
    }));
    expect(mockDeliverWebhook).not.toHaveBeenCalled();
    expect(JSON.stringify(mockLoggerError.mock.calls)).not.toContain('private source text');
  });

  it('replays a completed terminal job without provider or billing work', async () => {
    prismaMock.translationJob.updateMany.mockResolvedValue({ count: 0 });
    prismaMock.translationJob.findUnique.mockResolvedValue({
      status: 'completed',
      translation: '{"resource_type":"site_content"}',
    } as never);

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

    expect(result).toEqual({ translation: '{"resource_type":"site_content"}' });
    expect(mockTranslateStructured).not.toHaveBeenCalled();
    expect(prismaMock.creditTransaction.create).not.toHaveBeenCalled();
  });
});
