/**
 * InternalAppTokenService L1 集成测试
 *
 * 单测里 mock 了 prisma；本测试用真实 Postgres，覆盖单测无法触达的关键不变量：
 * - upsert binding 不会因 mailNickname 变化而漂移 slug（终身冻结）
 * - 一员工同时只能有一个 ACTIVE token（DB partial unique index 强制）
 * - issue 第二次会撤销旧 token 并创建新 token，slug 不变
 * - verify 对 ACTIVE / REVOKED / EXPIRED 分别返回正确结果或抛对应错误码
 *
 * 第 3 条不变量正是 2026-05-14-prisma-upsert-empty-update-stale-fields.md 记录的 FK 漂移 bug，
 * 这个测试在本 PR 之前会失败——即"如果单测就够了为何还要 L1"的活样本。
 */

import { INestApplication } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { createTestApp } from '../../helpers/app.helper';
import { InternalAppTokenService } from '@/modules/internal-app-platform/services/token.service';
import { PrismaService } from '@/core/database/prisma/prisma.service';

const T = 't_iap_token_'; // 前缀 cleanup
const ORG_CODE = `${T}org`;

describe('InternalAppTokenService (L1)', () => {
  let app: INestApplication;
  let tokenSvc: InternalAppTokenService;
  let prisma: PrismaService;
  // 多套测试可能并发，每个 it 自己持有 userId/employeeSlug，beforeEach 重建
  let userId: string;
  let organizationId: string;

  beforeAll(async () => {
    app = await createTestApp();
    tokenSvc = app.get(InternalAppTokenService);
    prisma = app.get(PrismaService);

    // 一次性 org（前缀清理）
    const org = await prisma.organization.upsert({
      where: { code: ORG_CODE },
      create: {
        code: ORG_CODE,
        name: `${T}org`,
        status: 'ACTIVE',
        isActive: true,
        settings: {},
        financialConfig: {},
        complianceConfig: {},
        order: 0,
        metadata: {},
      } as any,
      update: {},
    });
    organizationId = org.id;
  });

  afterAll(async () => {
    // 清掉测试 org（FK 级联）+ 用户们
    await prisma.internalAppEmployeeToken.deleteMany({
      where: { employeeSlug: { startsWith: T } },
    });
    await prisma.employeeSlugBinding.deleteMany({
      where: { employeeSlug: { startsWith: T } },
    });
    await prisma.user.deleteMany({
      where: { email: { startsWith: T } },
    });
    await prisma.organization.deleteMany({ where: { code: ORG_CODE } });
    await app.close();
  });

  beforeEach(async () => {
    // 每个 it 单独的用户，避免 unique 冲突
    const ts = Date.now() + Math.floor(Math.random() * 1000);
    const u = await prisma.user.create({
      data: {
        username: `${T}u${ts}`,
        email: `${T}u${ts}@test.local`,
        passwordHash: 'PLACEHOLDER',
        displayName: `${T}u${ts}`,
        status: 'ACTIVE',
        source: 'LOCAL',
        tenantId: 'default',
      },
    });
    userId = u.id;
  });

  it('issue 首次：创建 binding + ACTIVE token，返回完整 mcpAddCommand', async () => {
    const r = await tokenSvc.issue({
      userId,
      mailNickname: `${T}alice`,
      organizationId,
    });

    expect(r.tokenPlaintext).toMatch(/^ffoa_/);
    // normalizeEmployeeSlug 把下划线替换成短横，所以 T='t_iap_token_' → 't-iap-token-'
    expect(r.employeeSlug).toMatch(/^t-iap-token-alice/);
    expect(r.mcpAddCommand).toContain(r.tokenPlaintext);
    // 防回归：必须以 'claude mcp add --transport http ' 开头（详见 .learnings/2026-05-18-mcp-add-command-format-bug.md）
    expect(r.mcpAddCommand).toMatch(/^claude mcp add --transport http /);
    expect(r.mcpEndpoint).toMatch(/internal-apps\/mcp$/);

    const binding = await prisma.employeeSlugBinding.findUnique({
      where: { userId },
    });
    expect(binding?.employeeSlug).toBe(r.employeeSlug);

    const tokens = await prisma.internalAppEmployeeToken.findMany({
      where: { employeeSlug: r.employeeSlug },
    });
    expect(tokens).toHaveLength(1);
    expect(tokens[0].status).toBe('ACTIVE');
  });

  it('issue 第二次（同 user 不同 mailNickname）：slug 保持终身冻结，老 token 转 REVOKED', async () => {
    // 第一次
    const first = await tokenSvc.issue({
      userId,
      mailNickname: `${T}bob`,
      organizationId,
    });
    const firstSlug = first.employeeSlug;

    // 故意换 mailNickname（模拟员工改名等）— 旧 bug：slug 漂移导致 FK 违反
    const second = await tokenSvc.issue({
      userId,
      mailNickname: `${T}bob-renamed`,
      organizationId,
    });

    // 不变量：slug 终身冻结，必须等于第一次
    expect(second.employeeSlug).toBe(firstSlug);
    expect(second.tokenPlaintext).not.toBe(first.tokenPlaintext);

    // 旧 token 应 REVOKED，新 token ACTIVE，partial unique 限制下只 1 个 ACTIVE
    const all = await prisma.internalAppEmployeeToken.findMany({
      where: { employeeSlug: firstSlug },
      orderBy: { issuedAt: 'asc' },
    });
    expect(all).toHaveLength(2);
    expect(all[0].status).toBe('REVOKED');
    expect(all[0].revokedReason).toBe('rotated');
    expect(all[1].status).toBe('ACTIVE');
  });

  it('verify 对 ACTIVE 返回 employeeSlug；对 REVOKED 抛 revoked_token；对未知 token 抛 invalid_token', async () => {
    const r = await tokenSvc.issue({
      userId,
      mailNickname: `${T}carol`,
      organizationId,
    });

    const ok = await tokenSvc.verify(r.tokenPlaintext);
    expect(ok.employeeSlug).toBe(r.employeeSlug);

    await tokenSvc.revokeCurrent(r.employeeSlug);
    await expect(tokenSvc.verify(r.tokenPlaintext)).rejects.toThrow('revoked_token');

    await expect(tokenSvc.verify('ffoa_does_not_exist')).rejects.toThrow('invalid_token');
  });

  it('getMyTokenStatus：未颁发 → hasToken=false；颁发后 → ACTIVE + expiringInDays≈90；撤销后 → REVOKED', async () => {
    const mailNickname = `${T}dave`;
    // 不存在 binding 时 status 应 hasToken=false（slug 还没建）
    // 注意：getMyTokenStatus 接 employeeSlug，所以这里走不到 hasToken=false 分支；
    // 我们改在 issue 前先查"任意 slug"路径
    const beforeIssue = await tokenSvc.getMyTokenStatus(`${T}nobody-xyz`);
    expect(beforeIssue.hasToken).toBe(false);

    const r = await tokenSvc.issue({ userId, mailNickname, organizationId });
    const afterIssue = await tokenSvc.getMyTokenStatus(r.employeeSlug);
    expect(afterIssue.hasToken).toBe(true);
    expect(afterIssue.status).toBe('ACTIVE');
    expect(afterIssue.prefix).toBe(r.tokenPlaintext.slice(0, 8));
    expect(afterIssue.expiringInDays).toBeGreaterThan(85);
    expect(afterIssue.expiringInDays).toBeLessThanOrEqual(90);

    await tokenSvc.revokeCurrent(r.employeeSlug);
    const afterRevoke = await tokenSvc.getMyTokenStatus(r.employeeSlug);
    expect(afterRevoke.status).toBe('REVOKED');
  });

  it('revokeCurrent 幂等：无 ACTIVE token 时静默成功不报错', async () => {
    const r = await tokenSvc.issue({
      userId,
      mailNickname: `${T}eve`,
      organizationId,
    });
    const first = await tokenSvc.revokeCurrent(r.employeeSlug);
    expect(first.revokedAt).toBeInstanceOf(Date);

    // 再撤销一次（已无 ACTIVE）
    const second = await tokenSvc.revokeCurrent(r.employeeSlug);
    expect(second.revokedAt).toBeNull();
  });
});
