/**
 * FFAI Agent — sessions API 集成测试
 *
 * 覆盖：
 *   - happy path CRUD（POST/GET/DELETE）
 *   - 跨 org 访问拒绝（INV-1）
 *   - JWT 缺失返 401
 *   - 默认 mode 字段（planMode=OFF / permissionMode=DEFAULT / surface=WEB / status=ACTIVE）
 */
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { cleanupDatabase } from '../../helpers/cleanup.helper';
import { createTestApp } from '../../helpers/app.helper';
import { setupIntegrationTest } from '../../helpers/test-setup.helper';

describe('FFAI Agent - Sessions API', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let adminToken: string;
  let orgId: string;

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

  beforeEach(async () => {
    const ctx = await setupIntegrationTest(app, prisma);
    adminToken = ctx.adminToken;
    // setupIntegrationTest 应该已经准备好默认 org；用第一条
    const org = await prisma.organization.findFirst({ orderBy: { createdAt: 'asc' } });
    orgId = org!.id;
  });

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

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

  it('[AGENT-SES-001] POST /agent/sessions 创建会话；默认 mode 字段全部正确', async () => {
    const res = await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 'integration-test session' })
      .expect(201);

    const s = res.body.data;
    expect(s.title).toBe('integration-test session');
    expect(s.surface).toBe('WEB');
    expect(s.status).toBe('ACTIVE');
    expect(s.planMode).toBe('OFF');
    expect(s.permissionMode).toBe('DEFAULT');
    expect(s.closedAt).toBeNull();
    expect(s.organizationId).toBe(orgId);
  });

  it('[AGENT-SES-002] GET /agent/sessions 列出本人创建的会话（按 createdAt desc）', async () => {
    // 先创 3 个
    for (let i = 0; i < 3; i += 1) {
      await request(app.getHttpServer())
        .post('/api/v1/agent/sessions')
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: `s-${i}` });
    }
    const res = await request(app.getHttpServer())
      .get('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .expect(200);

    expect(res.body.data.items.length).toBeGreaterThanOrEqual(3);
    const titles = res.body.data.items.slice(0, 3).map((s: { title: string }) => s.title);
    expect(titles).toEqual(['s-2', 's-1', 's-0']); // desc
  });

  it('[AGENT-SES-003] DELETE /agent/sessions/:id 软删（closedAt 标记，status=CLOSED）', async () => {
    const created = await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 'to-delete' });

    await request(app.getHttpServer())
      .delete(`/api/v1/agent/sessions/${created.body.data.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .expect(200);

    const row = await prisma.agentSession.findUnique({ where: { id: created.body.data.id } });
    expect(row?.closedAt).not.toBeNull();
    expect(row?.status).toBe('CLOSED');
  });

  it('[AGENT-SES-004] GET /agent/sessions/:id 跨 org 必拒（INV-1）', async () => {
    // 创建测试 session（org A）
    const otherOrg = await prisma.organization.create({
      data: { code: `T_${Date.now()}`, name: 'other-org-INV1' },
    });
    const sess = await prisma.agentSession.create({
      data: {
        organizationId: otherOrg.id,
        createdById: '00000000-0000-0000-0000-000000000000',
        title: 'cross-org',
      },
    });

    await request(app.getHttpServer())
      .get(`/api/v1/agent/sessions/${sess.id}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId) // 当前用户在 orgId（非 otherOrg）
      .expect(403);
  });

  it('[AGENT-SES-005] 无 JWT 返 401', async () => {
    await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('X-Organization-Id', orgId)
      .send({ title: 'no-auth' })
      .expect(401);
  });

  // ─────────── Phase 2: rewind / projectId filter / parentSessionId ───────────

  it('[AGENT-SES-006] POST /agent/sessions/:id/rewind 截断后续消息 + 返 { ok, deleted }', async () => {
    // 拿当前测试 admin 真实 userId
    const created = await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 'rewind-host' });
    const sessionId = created.body.data.id;
    const turnId = '00000000-0000-0000-0000-000000000001';
    // 塞 5 条消息 sequence 1..5
    for (let i = 1; i <= 5; i += 1) {
      await prisma.agentMessage.create({
        data: {
          sessionId,
          turnId,
          type: 'ASSISTANT_TEXT',
          sequence: i,
          content: `msg-${i}`,
        },
      });
    }
    const res = await request(app.getHttpServer())
      .post(`/api/v1/agent/sessions/${sessionId}/rewind`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ afterSequence: 2 })
      .expect(201);

    expect(res.body.data.ok).toBe(true);
    expect(res.body.data.deleted).toBe(3); // sequence 3,4,5 删
    const remaining = await prisma.agentMessage.findMany({
      where: { sessionId },
      orderBy: { sequence: 'asc' },
    });
    expect(remaining.map((m) => m.sequence)).toEqual([1, 2]);
  });

  it('[AGENT-SES-007] POST /agent/sessions/:id/rewind 跨 org → 403', async () => {
    const otherOrg = await prisma.organization.create({
      data: { code: `T_REWIND_${Date.now()}`, name: 'other-org-rewind' },
    });
    const foreign = await prisma.agentSession.create({
      data: {
        organizationId: otherOrg.id,
        createdById: '00000000-0000-0000-0000-000000000000',
        title: 'cross-org-rewind',
      },
    });
    await request(app.getHttpServer())
      .post(`/api/v1/agent/sessions/${foreign.id}/rewind`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ afterSequence: 0 })
      .expect(403);
  });

  it('[AGENT-SES-007b] rewind afterSequence 校验：缺失 / 非整数 / 负数 → 400（防 data-loss）', async () => {
    const created = await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 'rewind-validate' });
    const sessionId = created.body.data.id;
    // 缺失 body
    await request(app.getHttpServer())
      .post(`/api/v1/agent/sessions/${sessionId}/rewind`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({})
      .expect(400);
    // 负数
    await request(app.getHttpServer())
      .post(`/api/v1/agent/sessions/${sessionId}/rewind`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ afterSequence: -1 })
      .expect(400);
    // 非整数
    await request(app.getHttpServer())
      .post(`/api/v1/agent/sessions/${sessionId}/rewind`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ afterSequence: 1.5 })
      .expect(400);
  });

  it('[AGENT-SES-008] GET /agent/sessions?projectId=<uuid> 过滤指定项目下的会话', async () => {
    // 先建一个 project + 两个 session（一个归属，一个不归属）
    const proj = await request(app.getHttpServer())
      .post('/api/v1/agent/projects')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ name: 'p-filter' });
    const projectId = proj.body.data.id;
    await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 's-in-project', projectId });
    await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 's-orphan' });

    const res = await request(app.getHttpServer())
      .get(`/api/v1/agent/sessions?projectId=${projectId}`)
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .expect(200);
    const titles = res.body.data.items.map((s: { title: string }) => s.title);
    expect(titles).toContain('s-in-project');
    expect(titles).not.toContain('s-orphan');
  });

  it('[AGENT-SES-009] GET /agent/sessions?projectId=null 过滤未归属任何项目的会话', async () => {
    const proj = await request(app.getHttpServer())
      .post('/api/v1/agent/projects')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ name: 'p-orphan-filter' });
    await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 's-bound', projectId: proj.body.data.id });
    await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 's-loose' });

    const res = await request(app.getHttpServer())
      .get('/api/v1/agent/sessions?projectId=null')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .expect(200);
    const titles = res.body.data.items.map((s: { title: string }) => s.title);
    expect(titles).toContain('s-loose');
    expect(titles).not.toContain('s-bound');
  });

  it('[AGENT-SES-010] AgentSession.parentSessionId 创建链路（多 Agent 拓扑）', async () => {
    // 用 prisma 直接创建（controller 不暴露 parentSessionId，是 delegate_task tool 写入）
    const parent = await request(app.getHttpServer())
      .post('/api/v1/agent/sessions')
      .set('Authorization', `Bearer ${adminToken}`)
      .set('X-Organization-Id', orgId)
      .send({ title: 'root-session' });
    const child = await prisma.agentSession.create({
      data: {
        organizationId: orgId,
        createdById: parent.body.data.createdById,
        title: 'child-session',
        parentSessionId: parent.body.data.id,
      },
    });
    expect(child.parentSessionId).toBe(parent.body.data.id);
    // 反向通过 prisma 查询确认 child 能被 parentSessionId 索引到
    const children = await prisma.agentSession.findMany({
      where: { parentSessionId: parent.body.data.id },
    });
    expect(children.map((c) => c.id)).toContain(child.id);
  });

  describe('PATCH /agent/sessions/:id', () => {
    it('[AGENT-SESS-PATCH-001] 改 title 成功', async () => {
      const sess = await request(app.getHttpServer())
        .post('/api/v1/agent/sessions')
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: 'old-title' })
        .expect(201);
      const res = await request(app.getHttpServer())
        .patch(`/api/v1/agent/sessions/${sess.body.data.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: 'new-title' })
        .expect(200);
      expect(res.body.data.title).toBe('new-title');
    });

    it('[AGENT-SESS-PATCH-002] 绑 personaId 后 session.personaId 落库', async () => {
      const sess = await request(app.getHttpServer())
        .post('/api/v1/agent/sessions')
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: 'persona-bind' })
        .expect(201);
      const persona = await prisma.agentPersona.create({
        data: { organizationId: orgId, createdById: sess.body.data.createdById, name: 'p-bind' },
      });
      const res = await request(app.getHttpServer())
        .patch(`/api/v1/agent/sessions/${sess.body.data.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ personaId: persona.id })
        .expect(200);
      expect(res.body.data.personaId).toBe(persona.id);
    });

    it('[AGENT-SESS-PATCH-003] personaId=null 清空', async () => {
      const persona = await prisma.agentPersona.create({
        data: { organizationId: orgId, createdById: '00000000-0000-0000-0000-000000000000', name: 'p-clr' },
      });
      const sess = await prisma.agentSession.create({
        data: { organizationId: orgId, createdById: '00000000-0000-0000-0000-000000000000', personaId: persona.id, title: 'has-persona' },
      });
      const res = await request(app.getHttpServer())
        .patch(`/api/v1/agent/sessions/${sess.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ personaId: null })
        .expect(200);
      expect(res.body.data.personaId).toBeNull();
    });

    it('[AGENT-SESS-PATCH-004] 跨 org personaId → 403', async () => {
      const otherOrg = await prisma.organization.create({
        data: { code: `T_PATCH_${Date.now()}`, name: 'other-org-patch' },
      });
      const foreignPersona = await prisma.agentPersona.create({
        data: { organizationId: otherOrg.id, createdById: null, name: 'foreign-p' },
      });
      const sess = await request(app.getHttpServer())
        .post('/api/v1/agent/sessions')
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: 'reject-cross-org' })
        .expect(201);
      await request(app.getHttpServer())
        .patch(`/api/v1/agent/sessions/${sess.body.data.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ personaId: foreignPersona.id })
        .expect(403);
    });

    it('[AGENT-SESS-PATCH-005] PATCH 跨 org session 本身 → 403', async () => {
      const otherOrg = await prisma.organization.create({
        data: { code: `T_PATCH2_${Date.now()}`, name: 'other-org-patch2' },
      });
      const foreignSess = await prisma.agentSession.create({
        data: { organizationId: otherOrg.id, createdById: '00000000-0000-0000-0000-000000000000', title: 'foreign' },
      });
      await request(app.getHttpServer())
        .patch(`/api/v1/agent/sessions/${foreignSess.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .set('X-Organization-Id', orgId)
        .send({ title: 'pwned' })
        .expect(403);
    });
  });
});
