/**
 * AI Tool Grants API Integration Tests
 *
 * 覆盖：
 * - 角色级授权 CRUD (POST/GET/DELETE/batch)
 * - 用户级授权 CRUD (含 reason 必填校验)
 * - 唯一约束 / 未知工具 / 不存在的 role/user
 * - getUserEffectiveTools 合并逻辑（仅角色 / 仅直接 / 二者叠加 / 多角色）
 * - getToolSubjects 反查
 * - getAvailableTools
 * - /sync 占位返回 501
 *
 * 基于文档: docs/modules/organization/07-api.md - 11. AI 工具授权 API
 *
 * @version v2.2 (权限 MVP)
 */

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';
import { createTestRole, createTestUser } from '../../helpers/factories';

describe('AI Tools API Integration Tests', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let adminToken: string;

  // 测试期间创建的资源，afterEach 显式清理
  const createdRoleIds: string[] = [];
  const createdUserIds: string[] = [];

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

  beforeEach(async () => {
    const ctx = await setupIntegrationTest(app, prisma);
    adminToken = ctx.adminToken;
    createdRoleIds.length = 0;
    createdUserIds.length = 0;
  });

  afterEach(async () => {
    // 删 grants（FK cascade 也会自动跟着 user/role 走，这里显式删一道双保险）
    if (createdRoleIds.length > 0) {
      await prisma.aIToolGrant.deleteMany({
        where: { roleId: { in: createdRoleIds } },
      });
    }
    if (createdUserIds.length > 0) {
      await prisma.aIToolGrantUser.deleteMany({
        where: { userId: { in: createdUserIds } },
      });
    }
    await cleanupDatabase(prisma);
  });

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

  /**
   * 辅助：创建一个测试角色并记录 id 用于清理
   */
  async function makeRole(): Promise<string> {
    const role = await createTestRole();
    createdRoleIds.push(role.id);
    return role.id;
  }

  /**
   * 辅助：创建一个测试用户，可选地分配角色
   */
  async function makeUser(roleIds: string[] = []): Promise<string> {
    const user = await createTestUser();
    createdUserIds.push(user.id);
    for (const roleId of roleIds) {
      await prisma.userRole.create({
        data: { userId: user.id, roleId, organizationId: null },
      });
    }
    return user.id;
  }

  // ==================== 角色级授权 CRUD ====================

  describe('POST /api/v1/ai-tools/grants', () => {
    it('[API-AITOOL-001] 应该成功创建角色级授权', async () => {
      const roleId = await makeRole();

      const res = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId, toolName: 'm365_mail' })
        .expect(201);

      expect(res.body.data.roleId).toBe(roleId);
      expect(res.body.data.toolName).toBe('m365_mail');
      expect(res.body.data.role).toBeDefined();
    });

    it('[API-AITOOL-002] 重复 (roleId, toolName) 应返回 409', async () => {
      const roleId = await makeRole();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId, toolName: 'm365_mail' })
        .expect(201);

      const dup = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId, toolName: 'm365_mail' })
        .expect(409);

      // 错误码可能在 body 不同字段，做宽松匹配
      const bodyText = JSON.stringify(dup.body);
      expect(bodyText).toContain('IAM_AI_TOOL_GRANT_ROLE_EXISTS');
    });

    it('[API-AITOOL-003] 未知工具应返回 400', async () => {
      const roleId = await makeRole();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId, toolName: 'nonexistent_tool_xyz' })
        .expect(400);
    });

    it('[API-AITOOL-004] 非法 roleId 应返回 400', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId: 'not-a-uuid', toolName: 'm365_mail' })
        .expect(400);
    });

    it('[API-AITOOL-005] 未授权请求应返回 401', async () => {
      const roleId = await makeRole();
      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants')
        .send({ roleId, toolName: 'm365_mail' })
        .expect(401);
    });
  });

  describe('POST /api/v1/ai-tools/grants/batch', () => {
    it('[API-AITOOL-010] 批量创建：全部新增', async () => {
      const roleId = await makeRole();

      const res = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants/batch')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          roleId,
          toolNames: ['m365_mail', 'm365_calendar', 'browser'],
        })
        .expect(201);

      expect(res.body.data.createdCount).toBe(3);
      expect(res.body.data.skippedCount).toBe(0);
      expect(res.body.data.results).toHaveLength(3);

      const grants = await prisma.aIToolGrant.findMany({ where: { roleId } });
      expect(grants).toHaveLength(3);
    });

    it('[API-AITOOL-011] 批量创建：部分已存在时跳过不报错', async () => {
      const roleId = await makeRole();
      await prisma.aIToolGrant.create({
        data: { roleId, toolName: 'm365_mail' },
      });

      const res = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants/batch')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          roleId,
          toolNames: ['m365_mail', 'm365_calendar'],
        })
        .expect(201);

      expect(res.body.data.createdCount).toBe(1);
      expect(res.body.data.skippedCount).toBe(1);
    });

    it('[API-AITOOL-012] 批量创建：任一工具未知整体失败', async () => {
      const roleId = await makeRole();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants/batch')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          roleId,
          toolNames: ['m365_mail', 'fake_tool_xyz'],
        })
        .expect(400);

      const grants = await prisma.aIToolGrant.findMany({ where: { roleId } });
      expect(grants).toHaveLength(0);
    });

    it('[API-AITOOL-013] 批量创建：空数组应返回 400', async () => {
      const roleId = await makeRole();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/grants/batch')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ roleId, toolNames: [] })
        .expect(400);
    });
  });

  describe('GET /api/v1/ai-tools/grants', () => {
    it('[API-AITOOL-020] 列表 + 按 roleId 筛选', async () => {
      const roleA = await makeRole();
      const roleB = await makeRole();
      await prisma.aIToolGrant.createMany({
        data: [
          { roleId: roleA, toolName: 'm365_mail' },
          { roleId: roleA, toolName: 'm365_calendar' },
          { roleId: roleB, toolName: 'browser' },
        ],
      });

      const all = await request(app.getHttpServer())
        .get('/api/v1/ai-tools/grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);
      expect(all.body.data.length).toBeGreaterThanOrEqual(3);

      const filtered = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/grants?roleId=${roleA}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);
      expect(filtered.body.data).toHaveLength(2);
      expect(filtered.body.data.every((g: any) => g.roleId === roleA)).toBe(true);
    });
  });

  describe('GET /api/v1/ai-tools/grants/aggregated (v2.3)', () => {
    it('[API-AITOOL-025] 即使无 grant 记录的角色也应返回（toolCount=0），SyncBot 排除', async () => {
      const orphan = await makeRole(); // 不写 AIToolGrant
      const granted = await makeRole();
      await prisma.aIToolGrant.create({
        data: { roleId: granted, toolName: 'm365_mail' },
      });

      // SyncBot 角色（如果尚未存在则创建一个 code='SyncBot' 的）
      const syncBot = await prisma.role.upsert({
        where: { code: 'SyncBot' },
        create: { code: 'SyncBot', name: 'Sync Bot Test' },
        update: {},
      });
      createdRoleIds.push(syncBot.id);
      await prisma.aIToolGrant.create({
        data: { roleId: syncBot.id, toolName: 'm365_mail' },
      });

      const res = await request(app.getHttpServer())
        .get('/api/v1/ai-tools/grants/aggregated')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const ids = res.body.data.map((r: any) => r.roleId);
      expect(ids).toContain(orphan);
      expect(ids).toContain(granted);
      expect(ids).not.toContain(syncBot.id);

      const orphanRow = res.body.data.find((r: any) => r.roleId === orphan);
      expect(orphanRow.toolCount).toBe(0);
      expect(orphanRow.tools).toEqual([]);

      const grantedRow = res.body.data.find((r: any) => r.roleId === granted);
      expect(grantedRow.toolCount).toBe(1);
      expect(grantedRow.tools).toEqual(['m365_mail']);
    });

    it('[API-AITOOL-026] 排序按 createdAt desc（与角色与权限页一致）', async () => {
      const older = await makeRole();
      // 强制写一个更早的 createdAt 让排序可验证
      await prisma.role.update({
        where: { id: older },
        data: { createdAt: new Date('2020-01-01T00:00:00Z') },
      });
      const newer = await makeRole();

      const res = await request(app.getHttpServer())
        .get('/api/v1/ai-tools/grants/aggregated')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const ids = res.body.data.map((r: any) => r.roleId);
      const idxOlder = ids.indexOf(older);
      const idxNewer = ids.indexOf(newer);
      expect(idxNewer).toBeGreaterThanOrEqual(0);
      expect(idxOlder).toBeGreaterThan(idxNewer); // newer 在前
    });
  });

  describe('POST /api/v1/roles → AI 工具 hook (v2.3)', () => {
    it('[API-AITOOL-027] 创建非 SyncBot 角色应自动 seed EMPLOYEE_BASELINE_TOOLS', async () => {
      const code = `R_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
      const res = await request(app.getHttpServer())
        .post('/api/v1/roles')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ code, name: code })
        .expect(201);
      const roleId = res.body.data.id;
      createdRoleIds.push(roleId);

      const grants = await prisma.aIToolGrant.findMany({
        where: { roleId },
      });
      expect(grants.length).toBe(24);
      const names = grants.map((g) => g.toolName).sort();
      expect(names).toContain('m365_mail');
      expect(names).toContain('session_status');
      expect(names).toContain('browser');
    });

    it('[API-AITOOL-028] 创建 code=SyncBot 的角色应跳过 seed', async () => {
      // 先确保不存在（清掉种子里的 SyncBot）
      await prisma.aIToolGrant.deleteMany({
        where: { role: { code: 'SyncBot' } },
      });
      await prisma.userRole.deleteMany({ where: { role: { code: 'SyncBot' } } });
      await prisma.role.deleteMany({ where: { code: 'SyncBot' } });

      const res = await request(app.getHttpServer())
        .post('/api/v1/roles')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ code: 'SyncBot', name: `SyncBot_${Date.now()}` })
        .expect(201);
      const roleId = res.body.data.id;
      createdRoleIds.push(roleId);

      const grants = await prisma.aIToolGrant.findMany({ where: { roleId } });
      expect(grants.length).toBe(0);
    });
  });

  describe('DELETE /api/v1/ai-tools/grants/:id', () => {
    it('[API-AITOOL-030] 删除存在的规则', async () => {
      const roleId = await makeRole();
      const grant = await prisma.aIToolGrant.create({
        data: { roleId, toolName: 'm365_mail' },
      });

      await request(app.getHttpServer())
        .delete(`/api/v1/ai-tools/grants/${grant.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const after = await prisma.aIToolGrant.findUnique({ where: { id: grant.id } });
      expect(after).toBeNull();
    });

    it('[API-AITOOL-031] 不存在的 id 返回 404', async () => {
      await request(app.getHttpServer())
        .delete('/api/v1/ai-tools/grants/00000000-0000-0000-0000-000000000000')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });

  // ==================== 用户级授权 CRUD ====================

  describe('POST /api/v1/ai-tools/user-grants', () => {
    it('[API-AITOOL-040] 含 reason 创建成功', async () => {
      const userId = await makeUser();

      const res = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/user-grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          userId,
          toolName: 'browser',
          reason: '项目 X 临时需要',
        })
        .expect(201);

      expect(res.body.data.userId).toBe(userId);
      expect(res.body.data.reason).toBe('项目 X 临时需要');
    });

    it('[API-AITOOL-041] reason 缺失应 400', async () => {
      const userId = await makeUser();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/user-grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userId, toolName: 'browser' })
        .expect(400);
    });

    it('[API-AITOOL-042] reason 空字符串应 400', async () => {
      const userId = await makeUser();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/user-grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userId, toolName: 'browser', reason: '' })
        .expect(400);
    });

    it('[API-AITOOL-043] 重复 (userId, toolName) 应 409', async () => {
      const userId = await makeUser();

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/user-grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userId, toolName: 'browser', reason: 'first' })
        .expect(201);

      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/user-grants')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userId, toolName: 'browser', reason: 'second' })
        .expect(409);
    });
  });

  describe('GET / DELETE /api/v1/ai-tools/user-grants', () => {
    it('[API-AITOOL-050] 列表 + 按 userId 筛选', async () => {
      const userA = await makeUser();
      const userB = await makeUser();
      await prisma.aIToolGrantUser.createMany({
        data: [
          { userId: userA, toolName: 'browser', reason: 'r1' },
          { userId: userA, toolName: 'm365_mail', reason: 'r2' },
          { userId: userB, toolName: 'm365_calendar', reason: 'r3' },
        ],
      });

      const filtered = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-grants?userId=${userA}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);
      expect(filtered.body.data).toHaveLength(2);
    });

    it('[API-AITOOL-051] 删除规则', async () => {
      const userId = await makeUser();
      const grant = await prisma.aIToolGrantUser.create({
        data: { userId, toolName: 'browser', reason: 'r' },
      });

      await request(app.getHttpServer())
        .delete(`/api/v1/ai-tools/user-grants/${grant.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const after = await prisma.aIToolGrantUser.findUnique({ where: { id: grant.id } });
      expect(after).toBeNull();
    });
  });

  // ==================== 可用工具清单 ====================

  describe('GET /api/v1/ai-tools/available-tools', () => {
    it('[API-AITOOL-060] 返回非空清单', async () => {
      const res = await request(app.getHttpServer())
        .get('/api/v1/ai-tools/available-tools')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(Array.isArray(res.body.data)).toBe(true);
      expect(res.body.data.length).toBeGreaterThan(0);
      expect(res.body.data[0]).toHaveProperty('name');
      expect(res.body.data[0]).toHaveProperty('label');
      expect(res.body.data[0]).toHaveProperty('category');
    });
  });

  // ==================== 生效计算 ====================

  describe('GET /api/v1/ai-tools/user-effective/:userId', () => {
    it('[API-AITOOL-070] 仅角色授权', async () => {
      const roleId = await makeRole();
      const userId = await makeUser([roleId]);
      await prisma.aIToolGrant.createMany({
        data: [
          { roleId, toolName: 'm365_mail' },
          { roleId, toolName: 'm365_calendar' },
        ],
      });

      const res = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-effective/${userId}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(res.body.data).toHaveLength(2);
      const mail = res.body.data.find((t: any) => t.toolName === 'm365_mail');
      expect(mail.sources).toHaveLength(1);
      expect(mail.sources[0].type).toBe('role');
      expect(mail.sources[0].roleId).toBe(roleId);
    });

    it('[API-AITOOL-071] 仅用户直接授权', async () => {
      // 绑一个空 role 避开 service 里的 "无角色 → Employee fallback" 分支，
      // 才能纯粹验证用户直接授权的行为（v2.3.1 起 Employee 默认含 24 项工具）。
      const emptyRole = await makeRole();
      const userId = await makeUser([emptyRole]);
      await prisma.aIToolGrantUser.create({
        data: { userId, toolName: 'browser', reason: '项目 X' },
      });

      const res = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-effective/${userId}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(res.body.data).toHaveLength(1);
      expect(res.body.data[0].toolName).toBe('browser');
      expect(res.body.data[0].sources[0].type).toBe('user');
      expect(res.body.data[0].sources[0].reason).toBe('项目 X');
    });

    it('[API-AITOOL-072] 角色 + 用户授权叠加（同一工具有多个来源）', async () => {
      const roleId = await makeRole();
      const userId = await makeUser([roleId]);
      // 同一工具同时被角色授权 + 直接授权 → 应有 2 个 sources
      await prisma.aIToolGrant.create({
        data: { roleId, toolName: 'm365_mail' },
      });
      await prisma.aIToolGrantUser.create({
        data: { userId, toolName: 'm365_mail', reason: 'override' },
      });

      const res = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-effective/${userId}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(res.body.data).toHaveLength(1);
      expect(res.body.data[0].sources).toHaveLength(2);
      const types = res.body.data[0].sources.map((s: any) => s.type).sort();
      expect(types).toEqual(['role', 'user']);
    });

    it('[API-AITOOL-073] 多角色合并', async () => {
      const roleA = await makeRole();
      const roleB = await makeRole();
      const userId = await makeUser([roleA, roleB]);
      await prisma.aIToolGrant.createMany({
        data: [
          { roleId: roleA, toolName: 'm365_mail' },
          { roleId: roleB, toolName: 'browser' },
        ],
      });

      const res = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-effective/${userId}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(res.body.data).toHaveLength(2);
    });

    it('[API-AITOOL-074] 无任何授权返回空数组', async () => {
      // 绑一个空 role 避开 Employee fallback；user + role 都无授权时才返回空。
      const emptyRole = await makeRole();
      const userId = await makeUser([emptyRole]);

      const res = await request(app.getHttpServer())
        .get(`/api/v1/ai-tools/user-effective/${userId}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(res.body.data).toEqual([]);
    });
  });

  // ==================== 反查 ====================

  describe('GET /api/v1/ai-tools/tool-subjects/:toolName', () => {
    it('[API-AITOOL-080] 反查同时来自角色和用户的来源', async () => {
      const roleId = await makeRole();
      const userA = await makeUser([roleId]);
      const userB = await makeUser();
      await prisma.aIToolGrant.create({
        data: { roleId, toolName: 'm365_mail' },
      });
      await prisma.aIToolGrantUser.create({
        data: { userId: userB, toolName: 'm365_mail', reason: '直接授权' },
      });

      const res = await request(app.getHttpServer())
        .get('/api/v1/ai-tools/tool-subjects/m365_mail')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      const userIds = res.body.data.map((s: any) => s.userId);
      expect(userIds).toContain(userA);
      expect(userIds).toContain(userB);

      const a = res.body.data.find((s: any) => s.userId === userA);
      expect(a.sources[0].type).toBe('role');

      const b = res.body.data.find((s: any) => s.userId === userB);
      expect(b.sources[0].type).toBe('user');
    });
  });

  // ==================== /sync 信息性路由 ====================

  describe('POST /api/v1/ai-tools/sync', () => {
    it('[API-AITOOL-090] 返回 scheduled=true 和 5 分钟等待提示', async () => {
      const res = await request(app.getHttpServer())
        .post('/api/v1/ai-tools/sync')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(201);

      expect(res.body.data.scheduled).toBe(true);
      expect(res.body.data.intervalMinutes).toBe(5);
      expect(typeof res.body.data.message).toBe('string');
      expect(res.body.data.message.length).toBeGreaterThan(0);
    });

    it('[API-AITOOL-091] 无 ai_tool:manage 权限（此处用无 token 模拟）应 401', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/ai-tools/sync')
        .expect(401);
    });
  });
});
