/**
 * Audit 模块集成测试 helpers
 *
 * 注意：testing/backend/helpers/app.helper.ts 的 createTestApp() 默认 override
 * 掉 APP_INTERCEPTOR 为空——审计拦截器无法触发。本文件提供 createAuditTestApp()
 * 用于需要拦截器真实运行的场景。
 */

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from '@/app.module';
import { TransformInterceptor } from '@common/interceptors/transform.interceptor';
import { AllExceptionsFilter } from '@common/filters/http-exception.filter';
import { PrismaClient } from '@prisma/client';
import { randomUUID, createHash } from 'crypto';

/**
 * 创建未 override APP_INTERCEPTOR 的 Nest 测试应用，
 * 让 AuditLogInterceptor 真实运行。
 */
export async function createAuditTestApp(): Promise<INestApplication> {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile();

  const app = moduleFixture.createNestApplication();
  const configService = app.get(ConfigService);

  const apiPrefix = configService.get('apiPrefix') || '/api/v1';
  app.setGlobalPrefix(apiPrefix);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: false,
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  );
  app.useGlobalFilters(new AllExceptionsFilter());
  app.useGlobalInterceptors(new TransformInterceptor());

  await app.init();
  return app;
}

/**
 * 创建一个测试角色，赋予指定权限码集合，分配给 userId。
 * 注意：项目权限模型只支持 role -> permission，不支持直接 user -> permission。
 */
export async function createRoleWithPermissions(
  prisma: PrismaClient,
  userId: string,
  roleCode: string,
  permissionCodes: string[],
): Promise<string> {
  const role = await prisma.role.create({
    data: {
      code: roleCode,
      name: roleCode,
      description: `test role ${roleCode}`,
    },
  });
  for (const code of permissionCodes) {
    const [resource, ...actionParts] = code.split(':');
    const action = actionParts.join(':');
    const perm = await prisma.permission.findFirst({
      where: { resource, action },
    });
    if (!perm) {
      throw new Error(`权限码不存在: ${code}（请先确认 seed 已加载）`);
    }
    await prisma.rolePermission.create({
      data: { roleId: role.id, permissionId: perm.id },
    });
  }
  await prisma.userRole.create({
    data: { userId, roleId: role.id },
  });
  return role.id;
}

/**
 * 通过 service 写入一条审计日志（保证哈希链由 service 串好），
 * 用于在测试 sandbox tenantId 下构造可控记录。
 */
export async function insertAuditLogViaPrisma(
  prisma: PrismaClient,
  partial: {
    region?: string;
    tenantId?: string;
    who?: string;
    what?: string;
    where?: string;
    how?: string;
    module?: string;
    action?: any;
    entityType?: string;
    entityId?: string;
    userId?: string | null;
    sessionId?: string;
    traceId?: string;
    requestId?: string;
    ipAddress?: string;
    userAgent?: string;
    isFinancial?: boolean;
    isSensitive?: boolean;
    riskLevel?: any;
    complianceLevel?: any;
    retentionYears?: number;
    newValue?: any;
    oldValue?: any;
    when?: Date;
  },
) {
  const region = (partial.region || 'cn').toLowerCase();
  const tenantId = partial.tenantId || 'default';

  // 取上一条同 (region, tenantId) 的 currentHash 作为 previousHash
  const prev = await prisma.auditLog.findFirst({
    where: { region, tenantId },
    orderBy: { when: 'desc' },
    select: { currentHash: true },
  });

  // 注意：必须包含所有 schema 中的 nullable 字段（即便是 null），否则
  // verify 时 findUnique 取回的完整 row 字段集合与此不一致 → hash 对不上。
  // 同样：id 必须显式设置，DB 默认生成会破坏 hash 自检。
  const record: any = {
    id: randomUUID(),
    region,
    tenantId,
    who: partial.who || 'test',
    what: partial.what || 'test op',
    when: partial.when || new Date(),
    where: partial.where || '/test',
    why: null,
    how: partial.how || 'TEST',
    module: partial.module || 'test',
    action: partial.action || 'CREATE',
    entityType: partial.entityType || 'TestEntity',
    entityId: partial.entityId || randomUUID(),
    oldValue: partial.oldValue ?? null,
    newValue: partial.newValue ?? null,
    changes: null,
    userId: partial.userId === undefined ? null : partial.userId,
    sessionId: partial.sessionId || randomUUID(),
    traceId: partial.traceId || randomUUID(),
    requestId: partial.requestId || randomUUID(),
    ipAddress: partial.ipAddress || '127.0.0.1',
    userAgent: partial.userAgent || 'jest',
    deviceId: null,
    geoLocation: null,
    businessType: null,
    businessKey: null,
    status: 'SUCCESS',
    errorMessage: null,
    duration: null,
    isFinancial: partial.isFinancial ?? false,
    isSensitive: partial.isSensitive ?? false,
    riskLevel: partial.riskLevel || 'MEDIUM',
    complianceLevel: partial.complianceLevel || 'MEDIUM',
    retentionYears: partial.retentionYears ?? 5,
    previousHash: prev?.currentHash ?? null,
    signature: null,
  };
  record.currentHash = computeHash(record);

  return prisma.auditLog.create({ data: record });
}

/**
 * 镜像后端 hash-chain.service.ts 的算法：
 * sha256(JSON.stringify(sortKeys(record except {currentHash, signature, createdAt, archivedAt})))
 */
export function computeHash(record: any): string {
  const { currentHash, signature, createdAt, archivedAt, ...rest } = record;
  return createHash('sha256')
    .update(JSON.stringify(sortKeys(rest)), 'utf8')
    .digest('hex');
}

function sortKeys(value: any): any {
  if (value === null || value === undefined) return value;
  if (value instanceof Date) return value.toISOString();
  if (Array.isArray(value)) return value.map(sortKeys);
  if (typeof value !== 'object') return value;
  const sorted: any = {};
  for (const k of Object.keys(value).sort()) sorted[k] = sortKeys(value[k]);
  return sorted;
}

/**
 * 清理测试 sandbox tenant 的所有审计记录
 */
export async function cleanupAuditSandbox(
  prisma: PrismaClient,
  tenantId: string,
): Promise<void> {
  await prisma.auditLog.deleteMany({ where: { tenantId } });
  await prisma.auditIntegrityCheckLog.deleteMany({ where: { tenantId } });
}

export function uniqueTenantId(prefix = 't_audit'): string {
  return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}