import { Test, TestingModule } from '@nestjs/testing';
import { Prisma } from '@prisma/client';
import { AiJobArtifactRepository, ARTIFACT_TYPES, CreateArtifactInput } from './ai-job-artifact.repository';
import { PrismaService } from '../../infrastructure/database/prisma.service';

describe('AiJobArtifactRepository', () => {
  let repo: AiJobArtifactRepository;
  let mockCreate: jest.Mock;
  let mockFindUnique: jest.Mock;
  let mockFindMany: jest.Mock;

  const sampleInput: CreateArtifactInput = {
    jobId: 'job-1',
    artifactType: 'quiz',
    artifactId: 'quiz-42',
    ordinal: 0,
    metadata: { questionCount: 5 },
  };

  beforeEach(async () => {
    mockCreate = jest.fn();
    mockFindUnique = jest.fn();
    mockFindMany = jest.fn();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AiJobArtifactRepository,
        {
          provide: PrismaService,
          useValue: {
            aiJobArtifact: {
              create: mockCreate,
              findUnique: mockFindUnique,
              findMany: mockFindMany,
            },
          },
        },
      ],
    }).compile();

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

  describe('create（幂等）', () => {
    it('应创建 Artifact 并返回记录', async () => {
      mockCreate.mockResolvedValue({ id: 'art-1', ...sampleInput, createdAt: new Date() });
      const result = await repo.create(sampleInput);
      expect(result).toHaveProperty('id', 'art-1');
      expect(mockCreate).toHaveBeenCalledTimes(1);
    });

    it('重复创建同一 (jobId, artifactType, artifactId) 应幂等返回已有记录', async () => {
      const p2002 = new Prisma.PrismaClientKnownRequestError('Unique constraint', {
        code: 'P2002',
        clientVersion: '5.22.0',
        meta: { target: ['jobId_artifactType_artifactId'] },
      });
      mockCreate.mockRejectedValueOnce(p2002);
      mockFindUnique.mockResolvedValue({ id: 'art-1', ...sampleInput, createdAt: new Date() });

      const result = await repo.create(sampleInput);
      expect(result).toHaveProperty('id', 'art-1');
      expect(mockCreate).toHaveBeenCalledTimes(1);
      expect(mockFindUnique).toHaveBeenCalledTimes(1);
    });

    it('P2002 后 findUnique 返回 null（CASCADE 竞态）应重试 create 并成功', async () => {
      const p2002 = new Prisma.PrismaClientKnownRequestError('Unique constraint', {
        code: 'P2002', clientVersion: '5.22.0', meta: { target: ['jobId_artifactType_artifactId'] },
      });
      mockCreate.mockRejectedValueOnce(p2002);
      mockFindUnique.mockResolvedValueOnce(null); // CASCADE 竞态：记录已被删除
      mockCreate.mockResolvedValueOnce({ id: 'art-retry', ...sampleInput, createdAt: new Date() });

      const result = await repo.create(sampleInput);
      expect(result).toHaveProperty('id', 'art-retry');
      expect(mockCreate).toHaveBeenCalledTimes(2); // 原 create + 重试
      expect(mockFindUnique).toHaveBeenCalledTimes(1);
    });

    it('非 P2002 错误应抛出', async () => {
      const otherError = new Error('DB connection lost');
      mockCreate.mockRejectedValueOnce(otherError);
      await expect(repo.create(sampleInput)).rejects.toThrow('DB connection lost');
    });

    it('应只接受 ARTIFACT_TYPES 中定义的类型', () => {
      for (const t of ARTIFACT_TYPES) {
        const input: CreateArtifactInput = { jobId: 'j', artifactType: t, artifactId: 'a' };
        expect(() => repo.create(input)).toBeDefined();
      }
      // ARTIFACT_TYPES 编译期约束：非成员类型无法编译通过
    });
  });

  describe('createMany', () => {
    it('应批量创建多个不同类型的 Artifact', async () => {
      mockCreate.mockResolvedValueOnce({ id: 'art-1', jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1', ordinal: 0, createdAt: new Date() });
      mockCreate.mockResolvedValueOnce({ id: 'art-2', jobId: 'job-1', artifactType: 'review_card', artifactId: 'rc1', ordinal: 1, createdAt: new Date() });

      const inputs: CreateArtifactInput[] = [
        { jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1', ordinal: 0 },
        { jobId: 'job-1', artifactType: 'review_card', artifactId: 'rc1', ordinal: 1 },
      ];
      const results = await repo.createMany(inputs);
      expect(results).toHaveLength(2);
      expect(mockCreate).toHaveBeenCalledTimes(2);
    });

    it('createMany 中部分重复应幂等处理', async () => {
      mockCreate.mockResolvedValueOnce({ id: 'art-1', jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1', ordinal: 0, createdAt: new Date() });
      const p2002 = new Prisma.PrismaClientKnownRequestError('dup', { code: 'P2002', clientVersion: '5.22.0', meta: {} });
      mockCreate.mockRejectedValueOnce(p2002);
      mockFindUnique.mockResolvedValue({ id: 'art-2', jobId: 'job-1', artifactType: 'quiz', artifactId: 'q2', ordinal: 1, createdAt: new Date() });

      const inputs: CreateArtifactInput[] = [
        { jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1' },
        { jobId: 'job-1', artifactType: 'quiz', artifactId: 'q2' },
      ];
      const results = await repo.createMany(inputs);
      expect(results).toHaveLength(2);
      expect(results[0]!.id).toBe('art-1');
      expect(results[1]!.id).toBe('art-2');
    });
  });

  describe('findByJobId', () => {
    it('应按 ordinal 排序返回产物列表', async () => {
      mockFindMany.mockResolvedValue([
        { id: 'art-1', jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1', ordinal: 0, metadata: null, createdAt: new Date() },
        { id: 'art-2', jobId: 'job-1', artifactType: 'review_card', artifactId: 'rc1', ordinal: 1, metadata: null, createdAt: new Date() },
      ]);

      const results = await repo.findByJobId('job-1');
      expect(results).toHaveLength(2);
      expect(mockFindMany).toHaveBeenCalledWith({
        where: { jobId: 'job-1' },
        orderBy: { ordinal: 'asc' },
      });
    });

    it('应返回空数组当无产物', async () => {
      mockFindMany.mockResolvedValue([]);
      const results = await repo.findByJobId('empty-job');
      expect(results).toEqual([]);
    });
  });

  describe('findJobsByArtifact', () => {
    it('应按 artifactType + artifactId 反向查询', async () => {
      mockFindMany.mockResolvedValue([
        { jobId: 'job-1', ordinal: 0, createdAt: new Date('2026-06-19') },
        { jobId: 'job-2', ordinal: 0, createdAt: new Date('2026-06-20') },
      ]);

      const results = await repo.findJobsByArtifact('quiz', 'quiz-42');
      expect(results).toHaveLength(2);
      expect(mockFindMany).toHaveBeenCalledWith({
        where: { artifactType: 'quiz', artifactId: 'quiz-42' },
        select: { jobId: true, ordinal: true, createdAt: true },
        orderBy: { createdAt: 'asc' },
      });
    });
  });

  describe('约束验证', () => {
    it('不允许直接修改或删除 Artifact', () => {
      const forbidden = ['update', 'updateMany', 'delete', 'deleteMany', 'upsert'];
      for (const m of forbidden) {
        expect(typeof (repo as any)[m]).toBe('undefined');
      }
    });

    it('一个 Job 可关联多个不同类型产物', async () => {
      mockFindMany.mockResolvedValue([
        { id: 'a', jobId: 'job-1', artifactType: 'quiz', artifactId: 'q1', ordinal: 0, metadata: null, createdAt: new Date() },
        { id: 'b', jobId: 'job-1', artifactType: 'focus_item', artifactId: 'fi1', ordinal: 1, metadata: null, createdAt: new Date() },
        { id: 'c', jobId: 'job-1', artifactType: 'review_card', artifactId: 'rc1', ordinal: 2, metadata: null, createdAt: new Date() },
        { id: 'd', jobId: 'job-1', artifactType: 'analysis_result', artifactId: 'ar1', ordinal: 3, metadata: null, createdAt: new Date() },
      ]);
      const results = await repo.findByJobId('job-1');
      const types = results.map(r => r.artifactType);
      expect(new Set(types).size).toBe(4);
    });
  });
});
