import { prismaMock as mockPrisma } from '../../setup';

jest.mock('@prisma/client', () => ({
  ...jest.requireActual('@prisma/client'),
  PrismaClient: jest.fn(() => mockPrisma),
}));

jest.mock('../../../utils/logger', () => ({
  logger: {
    info: jest.fn(),
    warn: jest.fn(),
    error: jest.fn(),
    debug: jest.fn(),
  },
}));

jest.mock('../../../queue', () => ({
  translationQueue: { add: jest.fn(), process: jest.fn(), on: jest.fn(), isReady: jest.fn(), close: jest.fn() },
}));

jest.mock('../../../queue/bulkQueueDispatcher', () => ({
  drainBulkQueue: jest.fn(),
}));

import { bulkStringsJobSchema, jobSubmitSchema, submitBulkStringsJob } from '../../../routes/jobs';
import { translationQueue } from '../../../queue';
import { drainBulkQueue } from '../../../queue/bulkQueueDispatcher';

const prismaMock = mockPrisma;

describe('async job request validation', () => {
  const base = { sourceLang: 'en', targetLang: 'es' };

  it('accepts content-only legacy requests', () => {
    expect(jobSubmitSchema.parse({ ...base, content: 'hello' })).toMatchObject({ content: 'hello' });
  });

  it('accepts core and generic structured fields', () => {
    const parsed = jobSubmitSchema.parse({
      ...base,
      title: 'Title',
      fields: { seo_description: 'Description' },
    });
    expect(parsed).toMatchObject({ fields: { seo_description: 'Description' } });
  });

  it('accepts content-only payload emitted when PHP has no ACF fields', () => {
    const phpPayload = JSON.parse('{"sourceLang":"en","targetLang":"es","title":"Title","excerpt":"","content":"Body"}');
    expect(phpPayload.fields).toBeUndefined();
    expect(jobSubmitSchema.parse(phpPayload)).toMatchObject({ content: 'Body' });
    expect(() => jobSubmitSchema.parse({ ...base, title: 'Title', fields: [] })).toThrow();
  });

  it('rejects reserved collisions and aggregate overflow', () => {
    expect(() => jobSubmitSchema.parse({ ...base, fields: { title: 'collision' } })).toThrow('reserved');
    expect(() => jobSubmitSchema.parse({
      ...base,
      title: 'a'.repeat(30_000),
      fields: { custom: 'b'.repeat(20_001) },
    })).toThrow('character limit');
  });

  it('accepts raw HTML markup above the character limit when visible aggregate fits', () => {
    const content = Array.from(
      { length: 600 },
      () => `<p title="${'x'.repeat(100)}">visible</p>`
    ).join('');

    expect(content.length).toBeGreaterThan(50_000);
    expect(jobSubmitSchema.parse({ ...base, content })).toMatchObject({ content });
  });

  it('accepts the exact segmented Site Content contract', () => {
    const parsed = jobSubmitSchema.parse({
      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' },
      }],
      callbackUrl: 'https://site.example.test/callback',
      callbackSecret: 'c'.repeat(64),
      clientJobId: 'site-content-41',
    });

    expect(parsed).toMatchObject({ resourceType: 'site_content', sourceRevision: 'a'.repeat(64) });
  });

  it.each([
    ['unknown field', { extra: true }],
    ['missing contract version', { contractVersion: undefined }],
    ['wrong contract version', { contractVersion: 2 }],
    ['post fields mixed into segments', { content: 'legacy' }],
    ['missing callback', { callbackUrl: undefined }],
    ['missing attempt token', { attemptToken: undefined }],
    ['wrong segment shape', { segments: [{ id: `seg_${'A'.repeat(32)}`, text: 'Heading', context: {}, extra: true }] }],
  ])('rejects segmented Site Content with %s', (_label, change) => {
    expect(() => jobSubmitSchema.parse({
      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: {} }],
      callbackUrl: 'https://site.example.test/callback',
      callbackSecret: 'c'.repeat(64),
      clientJobId: 'site-content-41',
      ...change,
    })).toThrow();
  });

  it('preserves bulk-string client identity for exact pending retries', () => {
    const parsed = bulkStringsJobSchema.parse({
      ...base,
      targetLangs: ['es'],
      strings: [{ id: 'hello', content: 'Hello' }],
      callbackUrl: 'https://site.example.test/callback',
      callbackSecret: 'sixteen-character-secret',
      clientJobId: 'wp_41',
    });
    expect(parsed.clientJobId).toBe('wp_41');
  });

  it('atomically creates and queues one job for concurrent identical bulk requests', async () => {
    const request = {
      userId: 'user-1', plugin: 'international', sourceLang: 'en', targetLangs: ['es'],
      strings: [{ id: 'title', content: 'Hello' }], callbackUrl: 'https://site.example.test/callback',
      callbackSecret: 'sixteen-character-secret', tone: 'neutral' as const, clientJobId: 'wp_41',
    };
    const jobs: Array<Record<string, unknown>> = [];
    let lockTail = Promise.resolve();
    let advisoryLockUsed = false;
    (prismaMock.$transaction as jest.Mock).mockImplementation(async (callback: (tx: typeof prismaMock) => Promise<unknown>) => {
      let releaseLock!: () => void;
      const previousLock = lockTail;
      lockTail = new Promise<void>((resolve) => { releaseLock = resolve; });
      const tx = {
        $executeRaw: jest.fn(async () => {
          advisoryLockUsed = true;
          await previousLock;
          return 1;
        }),
        translationJob: prismaMock.translationJob,
      } as unknown as typeof prismaMock;
      try {
        return await callback(tx);
      } finally {
        releaseLock();
      }
    });
    (prismaMock.translationJob.findFirst as unknown as jest.Mock).mockImplementation(async () => jobs[0] ?? null);
    (prismaMock.translationJob.create as unknown as jest.Mock).mockImplementation(async ({ data }: any) => {
      const job = { ...data, id: `job-${jobs.length + 1}`, status: 'pending', queued_at: null };
      jobs.push(job);
      return job as never;
    });
    const results = await Promise.all([
      submitBulkStringsJob(request),
      submitBulkStringsJob(request),
    ]);

    expect(advisoryLockUsed).toBe(true);
    expect(jobs).toHaveLength(1);
    expect(results[0].id).toBe(results[1].id);
    expect(drainBulkQueue).toHaveBeenCalledWith('job-1');
    expect(JSON.stringify((prismaMock.$executeRaw as jest.Mock).mock.calls)).not.toContain(request.callbackSecret);
    expect(JSON.stringify(jobs[0].queue_payload)).not.toContain(request.callbackSecret);
  });

  it('keeps a committed job durable when immediate dispatch fails', async () => {
    const request = {
      userId: 'user-1', plugin: 'international', sourceLang: 'en', targetLangs: ['es'],
      strings: [{ id: 'title', content: 'Hello' }], callbackUrl: 'https://site.example.test/callback',
      callbackSecret: 'sixteen-character-secret', tone: 'neutral' as const, clientJobId: 'wp_41',
    };
    const job = { id: 'job-1', status: 'pending', queued_at: null };
    prismaMock.$executeRaw.mockResolvedValue(1);
    const tx = {
      $executeRaw: prismaMock.$executeRaw,
      translationJob: prismaMock.translationJob,
    } as unknown as typeof prismaMock;
    (prismaMock.$transaction as jest.Mock).mockImplementation(async (callback: (tx: typeof prismaMock) => Promise<unknown>) => callback(tx));
    prismaMock.translationJob.findFirst.mockResolvedValue(job as never);
    prismaMock.translationJob.create.mockResolvedValue(job as never);
    (drainBulkQueue as jest.Mock).mockRejectedValueOnce(new Error('Redis unavailable'));

    await expect(submitBulkStringsJob(request)).resolves.toMatchObject({ id: 'job-1' });

    expect(prismaMock.translationJob.create).not.toHaveBeenCalled();
    expect(translationQueue.add).not.toHaveBeenCalled();
  });

});
