import { Test } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { InvocationRetentionService } from '../src/queues/invocation-retention.service';
import { PrismaService } from '../src/prisma/prisma.service';

/**
 * `agent_invocations` 的保留期清理。
 *
 * 🔴 这段直接关系到**磁盘**：助手接进来之后按对话轮数写库，
 *   而测试机盘常年 90%+、PG 卷已占 75 G。不清就是把一个已知的紧张问题变成事故。
 *
 * 这里锁住四件事：清哪些状态 · 失败留更久 · 清哪几列 · 幂等闸在不在。
 */

type Where = Record<string, unknown>;
type Call = { where: Where; data: Record<string, unknown> };

function build(retentionDays: number) {
  const calls: Call[] = [];
  const prisma = {
    agentInvocation: {
      updateMany: jest.fn(async (args: Call) => {
        calls.push(args);
        return { count: 1 };
      }),
    },
  };
  return { calls, prisma };
}

async function make(retentionDays: number) {
  const { calls, prisma } = build(retentionDays);
  const mod = await Test.createTestingModule({
    providers: [
      InvocationRetentionService,
      { provide: PrismaService, useValue: prisma },
      {
        provide: ConfigService,
        useValue: { get: () => ({ invocationRetentionDays: retentionDays }) },
      },
    ],
  }).compile();
  return { svc: mod.get(InvocationRetentionService), calls, prisma };
}

describe('InvocationRetentionService', () => {
  it('分两批清：成功/缓存一批，失败一批', async () => {
    const { svc, calls } = await make(30);
    await svc.prune();
    expect(calls).toHaveLength(2);
    expect(calls[0].where.status).toEqual({ in: ['succeeded', 'cached'] });
    expect(calls[1].where.status).toEqual({ in: ['failed'] });
  });

  it('🔴 失败的留 3 倍时长 —— 排查时唯一能看的就是它的输入', async () => {
    const { svc, calls } = await make(30);
    await svc.prune();
    const okCutoff = (calls[0].where.startedAt as { lt: Date }).lt.getTime();
    const failCutoff = (calls[1].where.startedAt as { lt: Date }).lt.getTime();
    const days = (ms: number) => Math.round((Date.now() - ms) / 86_400_000);
    expect(days(okCutoff)).toBe(30);
    expect(days(failCutoff)).toBe(90);
  });

  it('只清肥字段，⛔ 不碰 token / 成本 / 状态这些元数据列', async () => {
    const { svc, calls } = await make(30);
    await svc.prune();
    expect(calls[0].data).toEqual({
      inputSnapshot: { pruned: true },
      promptTemplate: null,
      systemPrompt: null,
      outputText: null,
    });
    for (const k of ['costYuan', 'totalTokens', 'status', 'callKey', 'promptVersion', 'judgeScore', 'userFeedback']) {
      expect(Object.keys(calls[0].data)).not.toContain(k);
    }
  });

  it('🔴 带幂等闸 —— 已清过的不再进来，否则每天全表重扫', async () => {
    const { svc, calls } = await make(30);
    await svc.prune();
    expect(calls[0].where.NOT).toEqual({ inputSnapshot: { equals: { pruned: true } } });
  });

  it('⚠️ 判据用 startedAt，⛔ 不能用 updatedAt（清理本身会刷新它）', async () => {
    const { svc, calls } = await make(30);
    await svc.prune();
    expect(calls[0].where).toHaveProperty('startedAt');
    expect(calls[0].where).not.toHaveProperty('updatedAt');
  });

  it('配 0 = 关掉清理，一次都不写库', async () => {
    const { svc, prisma } = await make(0);
    await svc.prune();
    expect(prisma.agentInvocation.updateMany).not.toHaveBeenCalled();
  });
});
