import type { PrismaClient } from '@prisma/client';
import { GatewayOutboxWorker } from '@/workers/gateway-outbox.worker';

jest.mock('@/core/gateway/adapters/gateway-adapter-registry', () => {
	const actual = jest.requireActual<typeof import('@/core/gateway/adapters/gateway-adapter-registry')>(
		'@/core/gateway/adapters/gateway-adapter-registry',
	);
	return {
		...actual,
		createGatewayRailAdapter: jest.fn(actual.createGatewayRailAdapter),
	};
});

import { createGatewayRailAdapter } from '@/core/gateway/adapters/gateway-adapter-registry';

type OutboxRow = {
	id: string;
	txnId: string;
	adapterId: string;
	payloadHash: string;
	sendAttempts: number;
	status: string;
	createdAt: Date;
	lastAttemptAt: Date | null;
};

function makeMockPrisma(rows: OutboxRow[]): {
	prisma: PrismaClient;
	getRow: (id: string) => OutboxRow | undefined;
} {
	const store = new Map(rows.map((r) => [r.id, { ...r }]));

	const prisma = {
		gateway_outbox: {
			findMany: jest.fn(async ({ where, take }: { where: { status: string; sendAttempts: { lt: number } }; take: number }) => {
				const max = where.sendAttempts.lt;
				const list = [...store.values()].filter(
					(r) => r.status === where.status && r.sendAttempts < max,
				);
				list.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
				return list.slice(0, take);
			}),
			updateMany: jest.fn(
				async ({
					where,
					data,
				}: {
					where: { id: string; status: string; sendAttempts: number };
					data: { sendAttempts: number; lastAttemptAt: Date };
				}) => {
					const r = store.get(where.id);
					if (!r) return { count: 0 };
					if (r.status !== where.status || r.sendAttempts !== where.sendAttempts) return { count: 0 };
					r.sendAttempts = data.sendAttempts;
					r.lastAttemptAt = data.lastAttemptAt;
					return { count: 1 };
				},
			),
			update: jest.fn(
				async ({
					where,
					data,
				}: {
					where: { id: string };
					data: Partial<Pick<OutboxRow, 'status' | 'sendAttempts' | 'lastAttemptAt'>>;
				}) => {
					const r = store.get(where.id);
					if (!r) throw new Error('gateway_outbox row not found');
					Object.assign(r, data);
					return r;
				},
			),
		},
	} as unknown as PrismaClient;

	return {
		prisma,
		getRow: (id: string) => store.get(id),
	};
}

describe('GatewayOutboxWorker', () => {
	const mockCreate = createGatewayRailAdapter as jest.MockedFunction<typeof createGatewayRailAdapter>;

	beforeEach(() => {
		mockCreate.mockClear();
		mockCreate.mockImplementation((id) =>
			jest.requireActual<typeof import('@/core/gateway/adapters/gateway-adapter-registry')>(
				'@/core/gateway/adapters/gateway-adapter-registry',
			).createGatewayRailAdapter(id),
		);
	});

	it('returns 0 when queue is empty', async () => {
		const { prisma } = makeMockPrisma([]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 5 });
		await expect(w.runOnce()).resolves.toBe(0);
	});

	it('claims row, sends SENT, marks gateway_outbox SENT', async () => {
		mockCreate.mockReturnValue({
			initialize: jest.fn().mockResolvedValue(undefined),
			send: jest.fn().mockResolvedValue({ status: 'SENT' as const, railMessageId: 'mid' }),
		} as never);

		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'dbis.adapter.thirdweb',
			payloadHash: '0xabc',
			sendAttempts: 0,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: null,
		};
		const { prisma, getRow } = makeMockPrisma([row]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 5 });
		const n = await w.runOnce();
		expect(n).toBe(1);
		expect(getRow('o1')?.status).toBe('SENT');
	});

	it('skips row when compare-and-swap loses (concurrent claim)', async () => {
		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'dbis.adapter.thirdweb',
			payloadHash: '0xabc',
			sendAttempts: 1,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: new Date(),
		};
		const { prisma } = makeMockPrisma([row]);
		(prisma.gateway_outbox.updateMany as jest.Mock).mockImplementationOnce(async () => ({ count: 0 }));
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 5 });
		const n = await w.runOnce();
		expect(n).toBe(0);
		expect(mockCreate).not.toHaveBeenCalled();
	});

	it('on send throw, sets PENDING when attempts remain', async () => {
		mockCreate.mockReturnValue({
			initialize: jest.fn().mockResolvedValue(undefined),
			send: jest.fn().mockRejectedValue(new Error('network down')),
		} as never);

		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'dbis.adapter.thirdweb',
			payloadHash: '0xabc',
			sendAttempts: 0,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: null,
		};
		const { prisma, getRow } = makeMockPrisma([row]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 5 });
		await w.runOnce();
		expect(getRow('o1')?.status).toBe('PENDING');
		expect(getRow('o1')?.sendAttempts).toBe(1);
	});

	it('on send throw at max attempts, sets FAILED', async () => {
		mockCreate.mockReturnValue({
			initialize: jest.fn().mockResolvedValue(undefined),
			send: jest.fn().mockRejectedValue(new Error('still down')),
		} as never);

		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'dbis.adapter.thirdweb',
			payloadHash: '0xabc',
			sendAttempts: 2,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: null,
		};
		const { prisma, getRow } = makeMockPrisma([row]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 3 });
		await w.runOnce();
		expect(getRow('o1')?.status).toBe('FAILED');
	});

	it('unknown adapterId marks FAILED when no retries left after claim', async () => {
		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'not.a.registered.adapter',
			payloadHash: '0xabc',
			sendAttempts: 2,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: null,
		};
		const { prisma, getRow } = makeMockPrisma([row]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 3 });
		await w.runOnce();
		expect(mockCreate).not.toHaveBeenCalled();
		expect(getRow('o1')?.status).toBe('FAILED');
	});

	it('non-SENT adapter result leaves PENDING when under max attempts', async () => {
		mockCreate.mockReturnValue({
			initialize: jest.fn().mockResolvedValue(undefined),
			send: jest.fn().mockResolvedValue({
				status: 'FAILED',
				error: { code: 'X', retryClass: 'RETRYABLE' },
			}),
		} as never);

		const row: OutboxRow = {
			id: 'o1',
			txnId: 'TX-1',
			adapterId: 'dbis.adapter.ktt-evidence',
			payloadHash: '0xabc',
			sendAttempts: 0,
			status: 'PENDING',
			createdAt: new Date('2020-01-01'),
			lastAttemptAt: null,
		};
		const { prisma, getRow } = makeMockPrisma([row]);
		const w = new GatewayOutboxWorker(prisma, { batchSize: 10, maxAttempts: 5 });
		await w.runOnce();
		expect(getRow('o1')?.status).toBe('PENDING');
	});
});
