import { Test, TestingModule } from '@nestjs/testing';
import { AiJobSnapshotRepository } from './ai-job-snapshot.repository';
import { PrismaService } from '../../infrastructure/database/prisma.service';

describe('AiJobSnapshotRepository', () => {
  let repo: AiJobSnapshotRepository;
  let prisma: PrismaService;

  const mockCreate = jest.fn();
  const mockFindUnique = jest.fn();
  const mockDeleteMany = jest.fn();

  beforeEach(async () => {
    mockCreate.mockReset();
    mockFindUnique.mockReset();
    mockDeleteMany.mockReset();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AiJobSnapshotRepository,
        {
          provide: PrismaService,
          useValue: {
            aiJobSnapshot: {
              create: mockCreate,
              findUnique: mockFindUnique,
              deleteMany: mockDeleteMany,
            },
          },
        },
      ],
    }).compile();

    repo = module.get(AiJobSnapshotRepository);
    prisma = module.get(PrismaService);
  });

  describe('create', () => {
    it('应创建快照并返回完整记录', async () => {
      const input = {
        jobId: 'job-1',
        jobType: 'learning_state_analysis',
        schemaVersion: 'snapshot_v1',
        content: { targetType: 'knowledge_base', targetId: 'kb-1' },
        contentHash: 'sha256:abc',
      };
      const expected = { id: 'snap-1', ...input, createdAt: new Date() };
      mockCreate.mockResolvedValue(expected);

      const result = await repo.create(input);
      expect(result).toEqual(expected);
      expect(mockCreate).toHaveBeenCalledWith({ data: input });
    });

    it('应创建带 expiresAt 的快照', async () => {
      const expiresAt = new Date('2026-09-01');
      const input = {
        jobId: 'job-2',
        jobType: 'quiz_generation',
        schemaVersion: 'snapshot_v1',
        content: {},
        expiresAt,
      };
      mockCreate.mockResolvedValue({ id: 'snap-2', ...input, createdAt: new Date() });

      const result = await repo.create(input);
      expect(result.expiresAt).toEqual(expiresAt);
    });
  });

  describe('findByJobId', () => {
    it('应返回匹配的快照', async () => {
      const snap = { id: 'snap-1', jobId: 'job-1', jobType: 'test', schemaVersion: 'v1', content: {}, createdAt: new Date() };
      mockFindUnique.mockResolvedValue(snap);

      const result = await repo.findByJobId('job-1');
      expect(result).toEqual(snap);
      expect(mockFindUnique).toHaveBeenCalledWith({ where: { jobId: 'job-1' } });
    });

    it('应返回 null 当快照不存在', async () => {
      mockFindUnique.mockResolvedValue(null);
      const result = await repo.findByJobId('nonexistent');
      expect(result).toBeNull();
    });
  });

  describe('deleteExpired', () => {
    it('应删除过期快照并返回删除数量', async () => {
      const before = new Date('2026-06-01');
      mockDeleteMany.mockResolvedValue({ count: 3 });

      const count = await repo.deleteExpired(before);
      expect(count).toBe(3);
      expect(mockDeleteMany).toHaveBeenCalledWith({
        where: { expiresAt: { lt: before } },
      });
    });

    it('应返回 0 当无过期快照', async () => {
      mockDeleteMany.mockResolvedValue({ count: 0 });
      const count = await repo.deleteExpired(new Date());
      expect(count).toBe(0);
    });
  });

  describe('不可变约束', () => {
    it('Repository 不应暴露 update 方法', () => {
      expect(typeof (repo as any).update).toBe('undefined');
      expect(typeof (repo as any).updateContent).toBe('undefined');
      expect(typeof (repo as any).upsert).toBe('undefined');
    });

    it('Repository 不应暴露 overwrite 方法', () => {
      expect(typeof (repo as any).overwrite).toBe('undefined');
      expect(typeof (repo as any).save).toBe('undefined');
      expect(typeof (repo as any).replace).toBe('undefined');
    });
  });

  describe('安全约束', () => {
    it('Repository 不应暴露直接修改 content 的路径', () => {
      // 不可变核心：写入后 content 不可变更。唯一写入口是 create()。
      const dangerousMethods = ['update', 'updateContent', 'upsert', 'patch', 'save', 'replace', 'overwrite', 'setContent', 'modify'];
      for (const m of dangerousMethods) {
        expect(typeof (repo as any)[m]).toBe('undefined');
      }
    });

    it('create 方法签名要求结构化 content（不可传裸字符串）', () => {
      // content 类型为 Record<string, unknown>，编译期阻止裸字符串。
      // 此处运行时验证方法存在且参数名称暗示结构化输入。
      const createFn = repo.create as Function;
      expect(createFn).toBeDefined();
      expect(createFn.length).toBe(1); // 单参数 data object
    });

    it('仅有的写入口 create() 返回类型不包含 update/upsert 方法', () => {
      // 验证 Repository 的公开 API 只暴露有限操作。
      // 运行时无法阻止访问私有字段（JS 无 # 语法），保护靠 TypeScript 编译 + code review。
      const allowedMethods = ['create', 'findByJobId', 'deleteExpired'];
      const allProtoMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(repo))
        .filter(n => n !== 'constructor');
      const unexpected = allProtoMethods.filter(m => !allowedMethods.includes(m));
      expect(unexpected).toEqual([]);
    });

    it('敏感字段扫描在 SnapshotBuilder 层实现（设计意图文档化）', () => {
      // M-AI-02-05 范围：Repository 基础设施
      // M-AI-02-10 范围：SnapshotBuilder 实现脱敏 + 拒绝
      // 10 类禁止字段：apiKey, jwt, authorization, cookie, databaseUrl,
      //                connectionString, password, secretKey, credential,
      //                以及任何匹配 /Bearer\s+|sk-[a-zA-Z0-9]{32,}/ 模式的值
      expect(repo).toBeDefined();
    });
  });

  describe('删除策略', () => {
    it('应通过 CASCADE 跟随 Job 删除（Prisma schema 定义 onDelete: Cascade）', () => {
      // FK 在 Prisma schema 中定义：
      // job AiAnalysisJob @relation(..., onDelete: Cascade)
      // 此测试确认 Repository 不实现手动级联删除（由 DB 处理）
      expect(typeof (repo as any).deleteByJobId).toBe('undefined');
    });

    it('deleteExpired 应允许独立清理过期快照', async () => {
      mockDeleteMany.mockResolvedValue({ count: 1 });
      const count = await repo.deleteExpired(new Date());
      expect(count).toBe(1);
    });
  });
});
