import { Test, TestingModule } from '@nestjs/testing';
import { Prisma } from '@prisma/client';
import { OutboxRepository, CreateOutboxInput } from './outbox.repository';
import { PrismaService } from '../database/prisma.service';

describe('OutboxRepository', () => {
  let repo: OutboxRepository;

  const mockOutboxCreate = jest.fn();
  const mockOutboxFindMany = jest.fn();
  const mockOutboxFindUnique = jest.fn();
  const mockOutboxUpdate = jest.fn();
  const mockOutboxUpdateMany = jest.fn();

  const mockTx = {
    outboxEvent: {
      create: mockOutboxCreate,
    },
  } as any;

  beforeEach(async () => {
    mockOutboxCreate.mockReset();
    mockOutboxFindMany.mockReset();
    mockOutboxFindUnique.mockReset();
    mockOutboxUpdate.mockReset();
    mockOutboxUpdateMany.mockReset();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OutboxRepository,
        {
          provide: PrismaService,
          useValue: {
            outboxEvent: {
              create: mockOutboxCreate,
              findMany: mockOutboxFindMany,
              findUnique: mockOutboxFindUnique,
              update: mockOutboxUpdate,
              updateMany: mockOutboxUpdateMany,
            },
          },
        },
      ],
    }).compile();

    repo = module.get(OutboxRepository);
  });

  const sampleInput: CreateOutboxInput = {
    eventType: 'ai.job.completed',
    aggregateType: 'AiJob',
    aggregateId: 'job-1',
    dedupeKey: 'job-1:completed:v1',
    payload: { jobId: 'job-1', status: 'succeeded' },
  };

  describe('createInTransaction', () => {
    it('应在事务中创建 Outbox 事件', async () => {
      mockOutboxCreate.mockResolvedValue({ id: 'evt-1', ...sampleInput, status: 'pending', createdAt: new Date() });

      const result = await repo.createInTransaction(mockTx, sampleInput);
      expect(result).toHaveProperty('id', 'evt-1');
      expect(mockOutboxCreate).toHaveBeenCalledTimes(1);
    });

    it('事务回滚时 Outbox 不存在（duplicate key 测试）', async () => {
      // 模拟：事务中先写 Job，再写 Outbox 时遇到 dedupeKey 冲突
      // 事务整体回滚，Job 也不存在
      const p2002 = new Prisma.PrismaClientKnownRequestError('dup', {
        code: 'P2002', clientVersion: '5.22.0', meta: {},
      });
      mockOutboxCreate.mockRejectedValueOnce(p2002);

      await expect(repo.createInTransaction(mockTx, sampleInput)).rejects.toThrow();
      // 调用方应回滚整个事务
    });
  });

  describe('findDispatchable', () => {
    it('应返回 pending 且 availableAt 已到的事件', async () => {
      mockOutboxFindMany.mockResolvedValue([]);
      await repo.findDispatchable(20);
      expect(mockOutboxFindMany).toHaveBeenCalledWith({
        where: { status: 'pending', availableAt: { lte: expect.any(Date) } },
        orderBy: { availableAt: 'asc' },
        take: 20,
      });
    });
  });

  describe('markProcessing（CAS）', () => {
    it('CAS 成功（status=pending）应获取锁并返回记录', async () => {
      mockOutboxUpdateMany.mockResolvedValue({ count: 1 });
      mockOutboxFindUnique.mockResolvedValue({ id: 'evt-1', status: 'processing', lockedBy: 'worker-1' });

      const result = await repo.markProcessing('evt-1', 'worker-1');
      expect(result).toHaveProperty('status', 'processing');
      expect(mockOutboxUpdateMany).toHaveBeenCalledWith({
        where: { id: 'evt-1', status: 'pending' },
        data: { status: 'processing', lockedAt: expect.any(Date), lockedBy: 'worker-1' },
      });
    });

    it('CAS 失败（已被其他 Worker 抢占）应返回 null', async () => {
      mockOutboxUpdateMany.mockResolvedValue({ count: 0 }); // 另一个 Worker 已改 status

      const result = await repo.markProcessing('evt-1', 'worker-2');
      expect(result).toBeNull();
      expect(mockOutboxUpdateMany).toHaveBeenCalledTimes(1);
      expect(mockOutboxFindUnique).not.toHaveBeenCalled(); // 未获得锁，不查询
    });
  });

  describe('markPublished', () => {
    it('应设置 status=published, publishedAt', async () => {
      mockOutboxUpdate.mockResolvedValue({ id: 'evt-1', status: 'published' });
      const result = await repo.markPublished('evt-1');
      expect(result).toHaveProperty('status', 'published');
      expect(mockOutboxUpdate).toHaveBeenCalledWith({
        where: { id: 'evt-1' },
        data: expect.objectContaining({ status: 'published' }),
      });
    });
  });

  describe('markFailed', () => {
    it('应设置 status=failed, 递增 attemptCount', async () => {
      mockOutboxUpdate.mockResolvedValue({ id: 'evt-1', status: 'failed' });
      const result = await repo.markFailed('evt-1', 'PUBLISH_ERR', 'timeout');
      expect(result).toHaveProperty('status', 'failed');
      expect(mockOutboxUpdate).toHaveBeenCalledWith({
        where: { id: 'evt-1' },
        data: expect.objectContaining({
          status: 'failed',
          lastErrorCode: 'PUBLISH_ERR',
          lastErrorMessage: 'timeout',
          attemptCount: { increment: 1 },
        }),
      });
    });
  });

  describe('releaseExpiredLocks', () => {
    it('应释放超过阈值的 processing 事件锁', async () => {
      mockOutboxUpdateMany.mockResolvedValue({ count: 3 });
      const count = await repo.releaseExpiredLocks(60_000);
      expect(count).toBe(3);
      expect(mockOutboxUpdateMany).toHaveBeenCalledWith({
        where: { status: 'processing', lockedAt: { lt: expect.any(Date) } },
        data: { status: 'pending', lockedAt: null, lockedBy: null },
      });
    });

    it('应返回 0 当无过期锁', async () => {
      mockOutboxUpdateMany.mockResolvedValue({ count: 0 });
      const count = await repo.releaseExpiredLocks();
      expect(count).toBe(0);
    });
  });

  describe('约束验证', () => {
    it('不应暴露 publish/publishToQueue 方法', () => {
      expect(typeof (repo as any).publish).toBe('undefined');
      expect(typeof (repo as any).publishToQueue).toBe('undefined');
      expect(typeof (repo as any).sendToBullMQ).toBe('undefined');
    });

    it('不应暴露独立事务方法', () => {
      expect(typeof (repo as any).create).toBe('undefined');
      // createInTransaction 必须接受 tx 参数，不允许无 tx 调用
    });

    it('dedupeKey UNIQUE 由 Prisma schema 保证', () => {
      // OutboxEvent.dedupeKey @unique — 数据库层保证
      expect(repo).toBeDefined();
    });
  });
});
