/**
 * AI Usage Dashboard API Integration Tests (9 cases)
 */
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { PrismaClient } from '@prisma/client';
import { createTestApp } from '../../helpers/app.helper';
import {
  setupAiUsageTestContext,
  AiUsageTestContext,
  cleanupAiUsageData,
  disconnect,
  seedEvent,
  createOrgLocal,
} from './_helpers';
import { createTestUser } from '../../helpers/factories/user.factory';

describe('AI Usage Dashboard API', () => {
  let app: INestApplication;
  let prisma: PrismaClient;
  let ctx: AiUsageTestContext;

  beforeAll(async () => {
    app = await createTestApp();
    prisma = new PrismaClient();
  });

  beforeEach(async () => {
    await cleanupAiUsageData();
    ctx = await setupAiUsageTestContext(app);
    // 给 admin user 也建 events
    for (let i = 0; i < 3; i++) {
      await seedEvent({ userId: ctx.adminUser.id, organizationId: ctx.organization.id, projectPath: '/repo/admin' });
    }
    for (let i = 0; i < 5; i++) {
      await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, projectPath: '/repo/member' });
    }
  });

  afterAll(async () => {
    await cleanupAiUsageData();
    await prisma.$disconnect();
    await disconnect();
    await app.close();
  });

  it('1. /me/summary 仅返回当前 user 数据', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/me/summary?period=month')
      .set('Authorization', `Bearer ${ctx.memberToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    // member 5 个 event × 1500 token (1000+500) = 7500
    expect(Number(data.totalTokens)).toBe(7500);
    expect(data.activeUsers).toBe(1);
  });

  it('2. /me/breakdown?by=project 正确聚合', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/me/breakdown?period=month&by=project')
      .set('Authorization', `Bearer ${ctx.memberToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const data = res.body.data ?? res.body;
    expect(data.items.length).toBe(1);
    expect(data.items[0].key).toBe('member');
  });

  it('3. Admin /summary 跨 user 聚合', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/summary?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const data = res.body.data ?? res.body;
    // 5+3=8 event × 1500 = 12000
    expect(Number(data.totalTokens)).toBe(12000);
    expect(data.activeUsers).toBe(2);
  });

  it('4. Admin /breakdown?by=user 分页正确', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/breakdown?period=month&by=user&page=1&pageSize=10')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const data = res.body.data ?? res.body;
    expect(data.items.length).toBe(2);
    expect(data.pagination.total).toBe(2);
  });

  it('5. 普通员工调 /summary → 403', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/summary?period=month')
      .set('Authorization', `Bearer ${ctx.memberToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(403);
  });

  it('6. Trend 按日粒度聚合正确', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/trend?period=month&granularity=day')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const data = res.body.data ?? res.body;
    expect(data.series.length).toBeGreaterThan(0);
    const totalTokens = data.series[0].points.reduce((a: number, p: any) => a + Number(p.tokens), 0);
    expect(totalTokens).toBe(12000);
  });

  it('7. /me/export CSV 含 UTF-8 BOM + 列', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/me/export?period=month')
      .set('Authorization', `Bearer ${ctx.memberToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    expect(res.text.charCodeAt(0)).toBe(0xfeff); // BOM
    expect(res.text).toContain('project,tokens,cost_usd,share');
  });

  it('8. /me/devices 返回当前用户的 device', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/me/devices')
      .set('Authorization', `Bearer ${ctx.memberToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const items = (res.body.data ?? res.body).items;
    expect(items.length).toBeGreaterThanOrEqual(1);
    expect(items.every((d: any) => d.osPlatform === 'LINUX')).toBe(true);
  });

  it('9. 跨 org 隔离：另一个 org 的 admin 看不到本 org 数据', async () => {
    // 建另一个 org + admin
    const otherOrg = await createOrgLocal();
    const otherAdmin = await createTestUser({ password: 'TestPass123!' });
    // 给 otherAdmin 挂 ai-usage:view-all 权限（复用 setup 已建的 role 即可；最简化：直接调 admin endpoint 不带角色将拿 403）
    // 这里：otherAdmin 没挂任何 ai-usage 角色 → 403（验证权限闭环）
    const loginRes = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username: otherAdmin.username, password: 'TestPass123!' });
    const otherToken = loginRes.body?.data?.accessToken;
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/summary?period=month')
      .set('Authorization', `Bearer ${otherToken}`);
    expect(res.status).toBe(403);

    // 二次验证：ctx.adminToken 看 summary 的 totalTokens 应严格等于本 org 的种子量
    const myRes = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/summary?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    const myData = myRes.body.data ?? myRes.body;
    expect(Number(myData.totalTokens)).toBe(12000);
  });

  // ===== v1.1 富 metadata 聚合 endpoint =====

  it('10. /tool-frequency 展开 toolNames JSONB 数组并按 name 聚合', async () => {
    await seedEvent({
      userId: ctx.memberUser.id,
      organizationId: ctx.organization.id,
      toolNames: ['Bash', 'Edit'],
      toolUseCount: 2,
    });
    await seedEvent({
      userId: ctx.memberUser.id,
      organizationId: ctx.organization.id,
      toolNames: ['Bash', 'Write'],
      toolUseCount: 2,
    });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/tool-frequency?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    const map = Object.fromEntries(data.items.map((it: any) => [it.name, it]));
    expect(map['Bash'].eventCount).toBe(2);
    expect(map['Edit'].eventCount).toBe(1);
    expect(map['Write'].eventCount).toBe(1);
  });

  it('11. /session-stats 按 sessionId 分桶 turn / duration', async () => {
    const sid = `sess-${Date.now()}`;
    const t0 = new Date(Date.now() - 60 * 60 * 1000);
    for (let i = 0; i < 4; i++) {
      await seedEvent({
        userId: ctx.memberUser.id,
        organizationId: ctx.organization.id,
        sessionId: sid,
        ts: new Date(t0.getTime() + i * 5 * 60 * 1000), // 0/5/10/15 min
        turnIndex: i + 1,
      });
    }
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/session-stats?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    expect(data.sessionCount).toBeGreaterThanOrEqual(1);
    const target = data.recentSessions.find((s: any) => s.sessionId === sid);
    expect(target).toBeTruthy();
    expect(target.turnCount).toBe(4);
    expect(target.durationSec).toBeGreaterThanOrEqual(15 * 60 - 5);
  });

  it('12. /turn-gap-distribution 用 LAG 计算 session 内 turn 间隔', async () => {
    const sid = `gap-${Date.now()}`;
    const t0 = new Date(Date.now() - 10 * 60 * 1000);
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, sessionId: sid, ts: t0 });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, sessionId: sid, ts: new Date(t0.getTime() + 3000) });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, sessionId: sid, ts: new Date(t0.getTime() + 13000) });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/turn-gap-distribution?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    // 2 个 gap：3s (<5s) + 10s (5-30s)
    expect(data.buckets['<5s']).toBeGreaterThanOrEqual(1);
    expect(data.buckets['5-30s']).toBeGreaterThanOrEqual(1);
  });

  it('13. /service-tier-mix 按 service_tier 聚合', async () => {
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, serviceTier: 'standard' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, serviceTier: 'priority' });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/service-tier-mix?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    const tiers = new Set(data.items.map((it: any) => it.tier));
    expect(tiers.has('standard')).toBe(true);
    expect(tiers.has('priority')).toBe(true);
  });

  it('14. /git-branch-heatmap 按 git_branch 聚合 top N', async () => {
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, gitBranch: 'feature/x' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, gitBranch: 'feature/x' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, gitBranch: 'feature/y' });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/git-branch-heatmap?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    const map = Object.fromEntries(data.items.map((it: any) => [it.branch, it]));
    expect(Number(map['feature/x'].eventCount)).toBe(2);
    expect(Number(map['feature/y'].eventCount)).toBe(1);
  });

  it('15a. /trend?groupBy=project 按 project_basename 分组返回时序', async () => {
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, projectPath: '/x/proj-a' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, projectPath: '/x/proj-b' });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/trend?period=month&granularity=day&groupBy=project')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    const keys = new Set(data.series.map((s: any) => s.key));
    expect(keys.has('proj-a')).toBe(true);
    expect(keys.has('proj-b')).toBe(true);
  });

  it('15b. /trend?groupBy=user 返回 user_id key + label', async () => {
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/trend?period=month&granularity=day&groupBy=user')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    expect(data.series.length).toBeGreaterThan(0);
    // 至少一条 series 带 label（displayName 反查）
    const withLabel = data.series.find((s: any) => s.label);
    expect(withLabel).toBeTruthy();
  });

  it('16. /session-stats 支持 userId / projectBasename / 分页过滤', async () => {
    const sid = `filt-${Date.now()}`;
    await seedEvent({
      userId: ctx.memberUser.id,
      organizationId: ctx.organization.id,
      sessionId: sid,
      projectPath: '/x/filter-target',
    });
    const res = await request(app.getHttpServer())
      .get(`/api/v1/ai-usage/session-stats?period=month&userId=${ctx.memberUser.id}&projectBasename=filter-target&page=1&pageSize=10`)
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    expect(data.pagination).toBeTruthy();
    expect(data.recentSessions.every((s: any) => s.projectBasename === 'filter-target')).toBe(true);
    expect(data.recentSessions.find((s: any) => s.sessionId === sid)).toBeTruthy();
  });

  it('17. /sessions/:id/turns 返回 turn timeline + 工具聚合', async () => {
    const sid = `turns-${Date.now()}`;
    const t0 = new Date(Date.now() - 60 * 60 * 1000);
    for (let i = 0; i < 3; i++) {
      await seedEvent({
        userId: ctx.memberUser.id,
        organizationId: ctx.organization.id,
        sessionId: sid,
        ts: new Date(t0.getTime() + i * 60_000),
        turnIndex: i + 1,
        toolNames: i % 2 === 0 ? ['Bash'] : ['Edit', 'Write'],
        stopReason: i === 2 ? 'end_turn' : 'tool_use',
      });
    }
    const res = await request(app.getHttpServer())
      .get(`/api/v1/ai-usage/sessions/${sid}/turns`)
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    expect(data.summary.turnCount).toBe(3);
    expect(data.turns.length).toBe(3);
    const toolMap = Object.fromEntries(data.summary.toolUseAgg.map((x: any) => [x.name, x.count]));
    expect(toolMap['Bash']).toBeGreaterThanOrEqual(2);
    expect(toolMap['Edit']).toBeGreaterThanOrEqual(1);
  });

  it('18. /daily-user-matrix 返回 users + dates + cells（admin only）', async () => {
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/daily-user-matrix?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    expect(Array.isArray(data.users)).toBe(true);
    expect(Array.isArray(data.dates)).toBe(true);
    expect(Array.isArray(data.cells)).toBe(true);
    expect(data.users.length).toBeGreaterThan(0);
  });

  it('19. /export?withRich=1 返回详细 CSV 含富 metadata 列', async () => {
    await seedEvent({
      userId: ctx.memberUser.id,
      organizationId: ctx.organization.id,
      gitBranch: 'feature/export-test',
      toolNames: ['Bash', 'Edit'],
      serviceTier: 'standard',
      stopReason: 'tool_use',
    });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/export?withRich=1&period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    expect(res.header['content-type']).toMatch(/text\/csv/);
    const body = res.text;
    expect(body).toMatch(/git_branch/);
    expect(body).toMatch(/tool_names/);
    expect(body).toMatch(/service_tier/);
    expect(body).toMatch(/feature\/export-test/);
    expect(body).toMatch(/Bash\|Edit/);
  });

  it('20. /stop-reason-mix 按 stop_reason 聚合', async () => {
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, stopReason: 'end_turn' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, stopReason: 'tool_use' });
    await seedEvent({ userId: ctx.memberUser.id, organizationId: ctx.organization.id, stopReason: 'tool_use' });
    const res = await request(app.getHttpServer())
      .get('/api/v1/ai-usage/stop-reason-mix?period=month')
      .set('Authorization', `Bearer ${ctx.adminToken}`)
      .set('X-Organization-Id', ctx.organizationId);
    expect(res.status).toBe(200);
    const data = res.body.data ?? res.body;
    const map = Object.fromEntries(data.items.map((it: any) => [it.reason, it]));
    expect(Number(map['tool_use'].eventCount)).toBeGreaterThanOrEqual(2);
    expect(Number(map['end_turn'].eventCount)).toBeGreaterThanOrEqual(1);
  });
});
