/**
 * FFAI Agent — Memory 注入到 system prompt 的集成测试（M1-step1）
 *
 * 覆盖：
 *   - 空 memory：返回空串（messages.service 不拼空段）
 *   - 同 org GLOBAL memory：完整内容写进 system prompt
 *   - 跨 org GLOBAL memory：邮箱 / 金额 / 项目代号 / 长数字按 sanitizeForCrossOrg 替换
 *   - 多条按 updatedAt desc 排序
 *   - PROJECT / PERSONA scope 不被 GLOBAL 路径注入
 *
 * 这里通过直接调 service 测，不走 HTTP/LLM——避免 mock provider 和 token 计费的复杂性。
 */
import { INestApplication } from '@nestjs/common';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { AgentMemoriesService } from '@/modules/agent/services/memories.service';
import { AgentMessagesService } from '@/modules/agent/services/messages.service';
import { ProviderRegistry } from '@/modules/agent/providers/provider-registry.service';
import { sanitizeForCrossOrg } from '@/modules/agent/utils/memory-sanitize.util';
import { cleanupDatabase } from '../../helpers/cleanup.helper';
import { createTestApp } from '../../helpers/app.helper';
import { setupIntegrationTest } from '../../helpers/test-setup.helper';

describe('FFAI Agent - Memory Injection (M1-step1)', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let service: AgentMemoriesService;
  let userId: string;
  let orgId: string;

  beforeAll(async () => {
    app = await createTestApp();
    prisma = app.get<PrismaService>(PrismaService);
    service = app.get<AgentMemoriesService>(AgentMemoriesService);
  });

  beforeEach(async () => {
    const ctx = await setupIntegrationTest(app, prisma);
    userId = ctx.adminUser.id;
    const org = await prisma.organization.findFirst({ orderBy: { createdAt: 'asc' } });
    orgId = org!.id;
  });

  afterEach(async () => {
    await cleanupDatabase(prisma);
  });

  afterAll(async () => {
    await app.close();
  });

  it('[INJ-001] 无 memory → 返回空串', async () => {
    const out = await service.buildSystemPromptSection(orgId, userId);
    expect(out).toBe('');
  });

  it('[INJ-002] 同 org GLOBAL memory → 完整内容 + header 段', async () => {
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: '用户偏好简洁中文回答，避免冗余解释',
        scope: 'GLOBAL',
        source: 'user',
      },
    });
    const out = await service.buildSystemPromptSection(orgId, userId);
    expect(out).toContain('## 关于该用户的长期记忆');
    expect(out).toContain('用户偏好简洁中文回答，避免冗余解释');
    expect(out).not.toContain('已按合规要求脱敏');
  });

  it('[INJ-003] 多条 memory 按 updatedAt desc 注入', async () => {
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: 'OLD-entry-marker',
        scope: 'GLOBAL',
        source: 'user',
      },
    });
    // updated 推后
    await new Promise((r) => setTimeout(r, 20));
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: 'NEW-entry-marker',
        scope: 'GLOBAL',
        source: 'user',
      },
    });
    const out = await service.buildSystemPromptSection(orgId, userId);
    const newIdx = out.indexOf('NEW-entry-marker');
    const oldIdx = out.indexOf('OLD-entry-marker');
    expect(newIdx).toBeGreaterThan(-1);
    expect(oldIdx).toBeGreaterThan(-1);
    expect(newIdx).toBeLessThan(oldIdx);
  });

  it('[INJ-004] 跨 org memory → sanitize 替换敏感片段，header 显示脱敏计数', async () => {
    const otherOrg = await prisma.organization.create({
      data: { code: `T_inj_${Date.now()}`, name: 'inj-other' },
    });
    await prisma.agentMemory.create({
      data: {
        organizationId: otherOrg.id,
        createdById: userId,
        content: '上次在 FFAI-001 上线时联系了 vendor@ext.com，金额 ¥120000，工号 887766551',
        scope: 'GLOBAL',
        source: 'user',
      },
    });
    const out = await service.buildSystemPromptSection(orgId, userId);
    expect(out).toContain('[email-redacted]');
    expect(out).toContain('[amount-redacted]');
    expect(out).toContain('[code-redacted]');
    expect(out).toContain('[number-redacted]');
    expect(out).not.toContain('FFAI-001');
    expect(out).not.toContain('vendor@ext.com');
    expect(out).toContain('已按合规要求脱敏');
  });

  it('[INJ-005] PROJECT / PERSONA scope 不被无 ctx 调用注入（仅 GLOBAL 激活）', async () => {
    const persona = await prisma.agentPersona.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        name: 'p1',
        instructions: 's',
      },
    });
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: 'PERSONA-scoped-marker',
        scope: 'PERSONA',
        personaId: persona.id,
        source: 'user',
      },
    });
    const out = await service.buildSystemPromptSection(orgId, userId);
    expect(out).not.toContain('PERSONA-scoped-marker');
  });

  it('[INJ-006] PROJECT scope memory 仅在 ctx.projectId 匹配时激活', async () => {
    const project = await prisma.agentProject.create({
      data: { organizationId: orgId, createdById: userId, name: 'proj-A' },
    });
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: 'PROJECT-A-marker',
        scope: 'PROJECT',
        projectId: project.id,
        source: 'user',
      },
    });
    // 无 ctx
    expect(await service.buildSystemPromptSection(orgId, userId)).not.toContain('PROJECT-A-marker');
    // ctx.projectId 不匹配
    expect(
      await service.buildSystemPromptSection(orgId, userId, {
        projectId: '00000000-0000-0000-0000-000000000000',
      }),
    ).not.toContain('PROJECT-A-marker');
    // ctx.projectId 匹配
    expect(
      await service.buildSystemPromptSection(orgId, userId, { projectId: project.id }),
    ).toContain('PROJECT-A-marker');
  });

  it('[INJ-007] PERSONA scope memory 仅在 ctx.personaId 匹配时激活', async () => {
    const persona = await prisma.agentPersona.create({
      data: { organizationId: orgId, createdById: userId, name: 'p2', instructions: 'x' },
    });
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: userId,
        content: 'PERSONA-X-marker',
        scope: 'PERSONA',
        personaId: persona.id,
        source: 'user',
      },
    });
    expect(await service.buildSystemPromptSection(orgId, userId)).not.toContain('PERSONA-X-marker');
    expect(
      await service.buildSystemPromptSection(orgId, userId, { personaId: persona.id }),
    ).toContain('PERSONA-X-marker');
  });

  it('[INJ-008] 4 个 category 各自落到独立章节，按固定顺序排列', async () => {
    const seed = async (cat: 'USER' | 'FEEDBACK' | 'PROJECT' | 'REFERENCE', tag: string) => {
      await prisma.agentMemory.create({
        data: {
          organizationId: orgId,
          createdById: userId,
          content: tag,
          scope: 'GLOBAL',
          category: cat,
          source: 'user',
        },
      });
    };
    await seed('USER', 'TAG-USER');
    await seed('FEEDBACK', 'TAG-FEEDBACK');
    await seed('PROJECT', 'TAG-PROJECT');
    await seed('REFERENCE', 'TAG-REFERENCE');
    const out = await service.buildSystemPromptSection(orgId, userId);
    const idxU = out.indexOf('TAG-USER');
    const idxF = out.indexOf('TAG-FEEDBACK');
    const idxP = out.indexOf('TAG-PROJECT');
    const idxR = out.indexOf('TAG-REFERENCE');
    expect(idxU).toBeGreaterThan(-1);
    expect(idxF).toBeGreaterThan(idxU);
    expect(idxP).toBeGreaterThan(idxF);
    expect(idxR).toBeGreaterThan(idxP);
    expect(out).toContain('### 用户偏好与背景');
    expect(out).toContain('### 协作反馈与边界');
    expect(out).toContain('### 项目上下文');
    expect(out).toContain('### 参考资料');
  });

  it('[INJ-009] ownerScope=ORG 共享 memory 在 org 内任意用户都注入（同 org，无 sanitize）', async () => {
    await prisma.agentMemory.create({
      data: {
        organizationId: orgId,
        createdById: null,
        ownerScope: 'ORG',
        content: 'ORG-policy-numbers-must-be-RMB ¥0',
        scope: 'GLOBAL',
        category: 'PROJECT',
        source: 'system',
      },
    });
    // 用一个跟 ORG memory 创建者无关的 userId 也能拉到这条 ORG 记忆
    // （ORG-scope 查询只看 organizationId，不看 createdById）
    const anotherUserId = '11111111-1111-1111-1111-111111111111';
    const out = await service.buildSystemPromptSection(orgId, anotherUserId);
    expect(out).toContain('ORG-policy-numbers-must-be-RMB');
    // 同 org → 不脱敏（¥0 应被 amount-redacted 仅当跨 org，这里不该）
    expect(out).not.toContain('[amount-redacted]');
  });

  it('[INJ-010] ownerScope=ORG 永远不跨 org 注入（即便 session orgId 不同）', async () => {
    const otherOrg = await prisma.organization.create({
      data: { code: `T_org_${Date.now()}`, name: 'other-for-org-memory' },
    });
    await prisma.agentMemory.create({
      data: {
        organizationId: otherOrg.id,
        createdById: null,
        ownerScope: 'ORG',
        content: 'OTHER-ORG-secret-policy',
        scope: 'GLOBAL',
        category: 'PROJECT',
        source: 'system',
      },
    });
    // 站在 orgId（≠ otherOrg.id）视角拿不到那条 ORG memory
    const out = await service.buildSystemPromptSection(orgId, userId);
    expect(out).not.toContain('OTHER-ORG-secret-policy');
    expect(out).not.toContain('OTHER-ORG');
  });

  describe('M2 auto-extract (<remember> tag)', () => {
    it('[EXT-001] 解析单条 <remember> tag → 写入 ai-detected memory + 剥离 tag', async () => {
      const raw =
        '好的我记下了。<remember category="USER">用户偏好使用 SQL 而非 ORM 调试线上问题</remember>另外我还想说...';
      const { cleanedText, created } = await service.extractAndPersist(orgId, userId, raw);
      expect(created).toBe(1);
      expect(cleanedText).not.toContain('<remember');
      expect(cleanedText).toContain('好的我记下了');
      expect(cleanedText).toContain('另外我还想说');
      const rows = await prisma.agentMemory.findMany({ where: { createdById: userId } });
      expect(rows).toHaveLength(1);
      expect(rows[0].content).toBe('用户偏好使用 SQL 而非 ORM 调试线上问题');
      expect(rows[0].category).toBe('USER');
      expect(rows[0].source).toBe('ai-detected');
      expect(rows[0].scope).toBe('GLOBAL');
      expect(rows[0].ownerScope).toBe('USER');
    });

    it('[EXT-002] 多 tag + 不同 category 全部落表', async () => {
      const raw = `结论说完。
<remember category="FEEDBACK">不要把整段代码贴回用户，差异点用 diff 形式</remember>
<remember category="PROJECT">当前在做 FFAI Agent v0.2 记忆注入模块</remember>
<remember category="REFERENCE">PRD 在 docs/modules/agent/01-prd-phase1.md</remember>`;
      const { created } = await service.extractAndPersist(orgId, userId, raw);
      expect(created).toBe(3);
      const cats = (await prisma.agentMemory.findMany({ where: { createdById: userId } }))
        .map((m) => m.category)
        .sort();
      expect(cats).toEqual(['FEEDBACK', 'PROJECT', 'REFERENCE']);
    });

    it('[EXT-003] dedup：重复内容只写一次', async () => {
      const raw = '<remember>用户用 Mac M2 Pro 开发</remember>';
      await service.extractAndPersist(orgId, userId, raw);
      const { created } = await service.extractAndPersist(orgId, userId, raw);
      expect(created).toBe(0);
      const rows = await prisma.agentMemory.findMany({ where: { createdById: userId } });
      expect(rows).toHaveLength(1);
    });

    it('[EXT-004] 单轮最多 3 条 tag（超出忽略）', async () => {
      const raw = Array.from({ length: 5 }, (_, i) => `<remember>fact ${i}</remember>`).join('');
      const { created } = await service.extractAndPersist(orgId, userId, raw);
      expect(created).toBe(3);
    });

    it('[EXT-005] 缺 category 属性 → 默认 USER', async () => {
      await service.extractAndPersist(orgId, userId, '<remember>plain fact</remember>');
      const row = await prisma.agentMemory.findFirstOrThrow({ where: { createdById: userId } });
      expect(row.category).toBe('USER');
    });

    it('[EXT-006] 无 tag 输入 → 不写 + 文本原样', async () => {
      const raw = 'just normal text with no tag';
      const { cleanedText, created } = await service.extractAndPersist(orgId, userId, raw);
      expect(created).toBe(0);
      expect(cleanedText).toBe(raw);
    });
  });

  describe('M3 容量算法（200 行 / 25KB 双约束）', () => {
    it('[CAP-001] 超 200 行截断，提示注入到 system prompt header', async () => {
      // 写入 250 条 GLOBAL，每条 1 行
      await Promise.all(
        Array.from({ length: 250 }, (_, i) =>
          prisma.agentMemory.create({
            data: {
              organizationId: orgId,
              createdById: userId,
              content: `entry-${i}`,
              scope: 'GLOBAL',
              source: 'user',
            },
          }),
        ),
      );
      const out = await service.buildSystemPromptSection(orgId, userId);
      expect(out).toContain('条目过多');
      // 实际行数小于等于 200
      expect(out.split('\n').length).toBeLessThanOrEqual(200);
    });

    it('[CAP-002] 超 25KB 字节截断', async () => {
      // 单条 2KB × 15 条 = 30KB > 25KB
      const big = 'x'.repeat(2000);
      for (let i = 0; i < 15; i++) {
        await prisma.agentMemory.create({
          data: {
            organizationId: orgId,
            createdById: userId,
            content: `${big}-${i}`,
            scope: 'GLOBAL',
            source: 'user',
          },
        });
      }
      const out = await service.buildSystemPromptSection(orgId, userId);
      expect(Buffer.byteLength(out, 'utf8')).toBeLessThanOrEqual(25 * 1024);
      expect(out).toContain('条目过多');
    });

    it('[CAP-003] 在预算内不截断，无 truncated 提示', async () => {
      await prisma.agentMemory.create({
        data: {
          organizationId: orgId,
          createdById: userId,
          content: 'small',
          scope: 'GLOBAL',
          source: 'user',
        },
      });
      const out = await service.buildSystemPromptSection(orgId, userId);
      expect(out).not.toContain('条目过多');
      expect(out).toContain('small');
    });
  });

  describe('P1: persona.instructions 注入 system prompt', () => {
    let messagesService: AgentMessagesService;
    let providerRegistry: ProviderRegistry;

    beforeAll(() => {
      messagesService = app.get<AgentMessagesService>(AgentMessagesService);
      providerRegistry = app.get<ProviderRegistry>(ProviderRegistry);
    });

    afterEach(() => {
      jest.restoreAllMocks();
    });

    it('[PERSONA-001] session 绑 persona → instructions 拼到 system prompt 的 ## 当前 Persona 段', async () => {
      const persona = await prisma.agentPersona.create({
        data: {
          organizationId: orgId,
          createdById: userId,
          name: 'CodeReviewer',
          instructions: '你是严苛的 code reviewer，回答必带文件:行号引用。',
        },
      });
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, personaId: persona.id, title: 'p-test' },
      });
      let captured: { messages: { role: string; content: string }[] } | null = null;
      jest.spyOn(providerRegistry, 'invoke').mockImplementation(async (req) => {
        captured = req as never;
        return {
          id: 'm', model: 'mock', resolvedModel: 'mock', text: 'ok',
          stopReason: 'end_turn', usage: { inputTokens: 1, outputTokens: 1 },
        } as never;
      });

      await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'hi',
      });

      expect(captured).not.toBeNull();
      const system = captured!.messages.find((m) => m.role === 'system')!.content;
      expect(system).toContain('## 当前 Persona：CodeReviewer');
      expect(system).toContain('你是严苛的 code reviewer');
    });

    it('[PERSONA-002] session 无 persona → system prompt 不含 ## 当前 Persona 段', async () => {
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, title: 'no-persona' },
      });
      let captured: { messages: { role: string; content: string }[] } | null = null;
      jest.spyOn(providerRegistry, 'invoke').mockImplementation(async (req) => {
        captured = req as never;
        return {
          id: 'm', model: 'mock', resolvedModel: 'mock', text: 'ok',
          stopReason: 'end_turn', usage: { inputTokens: 1, outputTokens: 1 },
        } as never;
      });

      await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'hi',
      });

      const system = captured!.messages.find((m) => m.role === 'system')!.content;
      expect(system).not.toContain('## 当前 Persona');
    });

    it('[PERSONA-003] persona.instructions 为空 → 仍不注入空段', async () => {
      const persona = await prisma.agentPersona.create({
        data: { organizationId: orgId, createdById: userId, name: 'NoInstr', instructions: '' },
      });
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, personaId: persona.id, title: 'empty-instr' },
      });
      let captured: { messages: { role: string; content: string }[] } | null = null;
      jest.spyOn(providerRegistry, 'invoke').mockImplementation(async (req) => {
        captured = req as never;
        return {
          id: 'm', model: 'mock', resolvedModel: 'mock', text: 'ok',
          stopReason: 'end_turn', usage: { inputTokens: 1, outputTokens: 1 },
        } as never;
      });

      await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'hi',
      });

      const system = captured!.messages.find((m) => m.role === 'system')!.content;
      expect(system).not.toContain('## 当前 Persona');
    });
  });

  describe('M2 end-to-end (runTurn 经 provider 注入 <remember> tag)', () => {
    let messagesService: AgentMessagesService;
    let providerRegistry: ProviderRegistry;

    beforeAll(() => {
      messagesService = app.get<AgentMessagesService>(AgentMessagesService);
      providerRegistry = app.get<ProviderRegistry>(ProviderRegistry);
    });

    afterEach(() => {
      jest.restoreAllMocks();
    });

    it('[E2E-001] runTurn 收到带 <remember> tag 的 provider 响应 → memory 落表 + ASSISTANT_TEXT 已剥离', async () => {
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, title: 'e2e-extract' },
      });
      // Mock provider 返回带 tag 的响应（不走 ModelRouter 真路径，但 messages.service 拼装走全套）
      jest.spyOn(providerRegistry, 'invoke').mockResolvedValue({
        id: 'mock-1',
        model: 'mock-echo',
        resolvedModel: 'mock-echo',
        text:
          '好的我注意到了。<remember category="FEEDBACK">用户偏好用 Pinia 而不是 Vuex</remember>另外建议你看下文档。',
        stopReason: 'end_turn',
        usage: { inputTokens: 10, outputTokens: 20 },
      } as never);

      const result = await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'tell me about state mgmt',
      });

      // assistant_text 已剥离 tag
      const assistantMsg = result.messages.find((m) => m.type === 'ASSISTANT_TEXT');
      expect(assistantMsg).toBeDefined();
      expect(assistantMsg!.content).not.toContain('<remember');
      expect(assistantMsg!.content).toContain('好的我注意到了');
      expect(assistantMsg!.content).toContain('另外建议你看下文档');

      // memory 落表
      const rows = await prisma.agentMemory.findMany({
        where: { createdById: userId, source: 'ai-detected' },
      });
      expect(rows).toHaveLength(1);
      expect(rows[0].content).toBe('用户偏好用 Pinia 而不是 Vuex');
      expect(rows[0].category).toBe('FEEDBACK');
      expect(rows[0].scope).toBe('GLOBAL');
      expect(rows[0].ownerScope).toBe('USER');

      // AgentMessage.payload.memoriesAutoCreated = 1
      const dbMsg = await prisma.agentMessage.findUnique({ where: { id: assistantMsg!.id } });
      const payload = dbMsg!.payload as { memoriesAutoCreated?: number };
      expect(payload.memoriesAutoCreated).toBe(1);
    });

    it('[E2E-002] runTurn 收到无 tag 的响应 → 不写 memory + 文本不变', async () => {
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, title: 'e2e-no-tag' },
      });
      jest.spyOn(providerRegistry, 'invoke').mockResolvedValue({
        id: 'mock-2',
        model: 'mock-echo',
        resolvedModel: 'mock-echo',
        text: '简单回答没有任何 tag。',
        stopReason: 'end_turn',
        usage: { inputTokens: 5, outputTokens: 8 },
      } as never);

      const result = await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'hi',
      });

      const assistantMsg = result.messages.find((m) => m.type === 'ASSISTANT_TEXT');
      expect(assistantMsg!.content).toBe('简单回答没有任何 tag。');
      const rows = await prisma.agentMemory.findMany({
        where: { createdById: userId, source: 'ai-detected' },
      });
      expect(rows).toHaveLength(0);
    });

    it('[E2E-003] 第二轮重复 tag 不重复写 memory（dedup 在 e2e 层生效）', async () => {
      const session = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: userId, title: 'e2e-dedup' },
      });
      const sameResp = {
        id: 'mock-3',
        model: 'mock-echo',
        resolvedModel: 'mock-echo',
        text: '<remember>用户在 Faraday Future 工作</remember>',
        stopReason: 'end_turn',
        usage: { inputTokens: 5, outputTokens: 8 },
      };
      jest
        .spyOn(providerRegistry, 'invoke')
        .mockResolvedValueOnce(sameResp as never)
        .mockResolvedValueOnce(sameResp as never);

      await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'r1',
      });
      await messagesService.runTurn({
        sessionId: session.id,
        organizationId: orgId,
        userId,
        prompt: 'r2',
      });

      const rows = await prisma.agentMemory.findMany({
        where: { createdById: userId, source: 'ai-detected' },
      });
      expect(rows).toHaveLength(1);
    });
  });

  describe('sanitizeForCrossOrg 规则单测', () => {
    it('邮箱被替换', () => {
      expect(sanitizeForCrossOrg('contact a@b.co for detail')).toContain('[email-redacted]');
    });
    it('金额被替换（人民币 / 美元）', () => {
      expect(sanitizeForCrossOrg('cost ¥1,200.50 USD')).toContain('[amount-redacted]');
      expect(sanitizeForCrossOrg('cost $99')).toContain('[amount-redacted]');
    });
    it('项目代号被替换', () => {
      expect(sanitizeForCrossOrg('see PRJ-2024 status')).toContain('[code-redacted]');
      expect(sanitizeForCrossOrg('FFAI001 issue')).toContain('[code-redacted]');
    });
    it('长数字 ≥6 位被替换', () => {
      expect(sanitizeForCrossOrg('user 887766')).toContain('[number-redacted]');
    });
    it('短数字 < 6 位保留（普通版本号 / 序号）', () => {
      expect(sanitizeForCrossOrg('v2.3 step 5')).not.toContain('[number-redacted]');
    });
    it('无敏感片段原样返回', () => {
      const safe = 'user prefers concise replies';
      expect(sanitizeForCrossOrg(safe)).toBe(safe);
    });
  });
});
