import express from 'express';
import type { NextFunction, Request, Response } from 'express';
import request from 'supertest';
import { prismaMock } from '../../setup';
import router from '../../../routes/admin/translationWebhookOutbox';

jest.mock('../../../middleware/auth', () => ({
  authenticateAdmin: (req: Request, _res: Response, next: NextFunction) => {
    req.admin = {
      id: '00000000-0000-4000-8000-000000000001',
      email: 'support@example.com',
      role: 'support' as any,
    };
    next();
  },
}));

const OUTBOX_ID = '00000000-0000-4000-8000-000000000010';
const JOB_ID = '00000000-0000-4000-8000-000000000011';
const CREATED_AT = new Date('2026-08-24T10:00:00.000Z');

function createApp() {
  const app = express();
  app.use(express.json());
  app.use('/', router);
  return app;
}

function outboxRow(overrides: Record<string, unknown> = {}) {
  return {
    id: OUTBOX_ID,
    job_id: JOB_ID,
    event: 'translation.failed',
    delivery_sequence: 2,
    is_final: true,
    status: 'dead',
    attempts: 5,
    // Free-form errors are intentionally present in the fixture but must never
    // be selected or returned by the staff-admin projection.
    last_error: '{"callback_secret":"abc def","response_body":"source content must not be returned","url":"https://callback.example.test/hook"}',
    claimed_at: null,
    next_attempt_at: null,
    first_attempt_at: new Date('2026-08-24T09:00:00.000Z'),
    last_attempt_at: new Date('2026-08-24T09:30:00.000Z'),
    dead_at: new Date('2026-08-24T09:31:00.000Z'),
    response_status: 503,
    created_at: CREATED_AT,
    delivered_at: null,
    // The route must not select or serialize this field.
    payload: {
      source: 'source content must not be returned',
      callbackSecret: 'callback secret must not be returned',
    },
    job: {
      id: JOB_ID,
      status: 'failed',
      callback_url: 'https://callback.example.test/hook',
      callback_secret: 'callback secret must not be returned',
      content: 'source content must not be returned',
    },
    ...overrides,
  };
}

describe('admin translation webhook outbox routes', () => {
  it('lists bounded due/dead rows without callback or source data', async () => {
    prismaMock.webhookOutbox.findMany.mockResolvedValue([outboxRow()] as any);
    prismaMock.webhookOutbox.count.mockResolvedValue(1);

    const response = await request(createApp())
      .get('/')
      .query({ per_page: '2', state: 'dead' });

    expect(response.status).toBe(200);
    expect(response.body.data.entries).toHaveLength(1);
    expect(response.body.data.pagination).toEqual(expect.objectContaining({
      page: 1,
      per_page: 2,
      total: 1,
    }));
    const serialized = JSON.stringify(response.body);
    expect(serialized).not.toContain('source content must not be returned');
    expect(serialized).not.toContain('callback secret must not be returned');
    expect(serialized).not.toContain('callback.example.test');

    const findManyArgs = prismaMock.webhookOutbox.findMany.mock.calls[0][0] as any;
    expect(findManyArgs.take).toBe(2);
    expect(findManyArgs.where).toEqual(expect.objectContaining({ is_final: true }));
    expect(findManyArgs.select).not.toHaveProperty('payload');
    expect(findManyArgs.select).not.toHaveProperty('callback_secret');
    expect(findManyArgs.select).not.toHaveProperty('last_error');
    expect(serialized).not.toContain('abc def');
    expect(serialized).not.toContain('response_body');
  });

  it('rejects an out-of-bounds page size before querying the outbox', async () => {
    const response = await request(createApp())
      .get('/')
      .query({ per_page: '101' });

    expect(response.status).toBe(400);
    expect(prismaMock.webhookOutbox.findMany).not.toHaveBeenCalled();
    expect(prismaMock.webhookOutbox.count).not.toHaveBeenCalled();
  });

  it('does not expose a non-due or non-dead row through inspect', async () => {
    prismaMock.webhookOutbox.findUnique.mockResolvedValue(outboxRow({
      status: 'pending',
      next_attempt_at: new Date('2099-01-01T00:00:00.000Z'),
    }) as any);

    const response = await request(createApp()).get(`/${OUTBOX_ID}`);

    expect(response.status).toBe(404);
    expect(response.body.error.code).toBe('OUTBOX_NOT_FOUND');
  });

  it('keeps a future retry_wait row visible to the admin repair surface', async () => {
    prismaMock.webhookOutbox.findUnique.mockResolvedValue(outboxRow({
      status: 'retry_wait',
      next_attempt_at: new Date('2099-01-01T00:00:00.000Z'),
    }) as any);

    const response = await request(createApp()).get(`/${OUTBOX_ID}`);

    expect(response.status).toBe(200);
    expect(response.body.data.entry).toEqual(expect.objectContaining({
      id: OUTBOX_ID,
      status: 'retry_wait',
      next_attempt_at: '2099-01-01T00:00:00.000Z',
    }));
  });

  it('redrives a dead row in one transaction and audits only the state reset', async () => {
    const redriven = outboxRow({
      status: 'pending',
      attempts: 0,
      last_error: null,
      next_attempt_at: new Date('2026-08-24T10:05:00.000Z'),
      first_attempt_at: null,
      last_attempt_at: null,
      dead_at: null,
      response_status: null,
    });
    prismaMock.webhookOutbox.findUnique
      .mockResolvedValueOnce(outboxRow() as any)
      .mockResolvedValueOnce(redriven as any);
    prismaMock.webhookOutbox.updateMany.mockResolvedValue({ count: 1 });
    prismaMock.auditLog.create.mockResolvedValue({} as any);
    (prismaMock.$transaction as any).mockImplementation(async (callback: (tx: unknown) => Promise<unknown>) => callback(prismaMock));

    const response = await request(createApp()).post(`/${OUTBOX_ID}/redrive`);

    expect(response.status).toBe(202);
    expect(response.body.data.redriven).toBe(true);
    expect(prismaMock.webhookOutbox.updateMany).toHaveBeenCalledTimes(1);
    const updateArgs = prismaMock.webhookOutbox.updateMany.mock.calls[0][0] as any;
    expect(updateArgs.data).toEqual(expect.objectContaining({
      status: 'pending',
      attempts: 0,
      claimed_at: null,
      next_attempt_at: expect.any(Date),
      dead_at: null,
    }));
    expect(updateArgs.data).not.toHaveProperty('payload');
    expect(updateArgs.data).not.toHaveProperty('job_id');

    expect(prismaMock.auditLog.create).toHaveBeenCalledWith(expect.objectContaining({
      data: expect.objectContaining({
        action: 'admin.translation_webhook_outbox.redrive',
        resource_id: OUTBOX_ID,
      }),
    }));
    const auditArgs = prismaMock.auditLog.create.mock.calls[0][0] as any;
    expect(auditArgs.data.details).not.toHaveProperty('callback_secret');
    expect(auditArgs.data.details).not.toHaveProperty('payload');
  });

  it('refuses to redrive a pending row whose next attempt is still in the future', async () => {
    prismaMock.webhookOutbox.findUnique.mockResolvedValue(outboxRow({
      status: 'pending',
      next_attempt_at: new Date('2099-01-01T00:00:00.000Z'),
    }) as any);
    (prismaMock.$transaction as any).mockImplementation(async (callback: (tx: unknown) => Promise<unknown>) => callback(prismaMock));

    const response = await request(createApp()).post(`/${OUTBOX_ID}/redrive`);

    expect(response.status).toBe(409);
    expect(response.body.error.code).toBe('OUTBOX_STATE_NOT_REDRIVABLE');
    expect(prismaMock.webhookOutbox.updateMany).not.toHaveBeenCalled();
    expect(prismaMock.auditLog.create).not.toHaveBeenCalled();
  });
});
