/**
 * GiteaWebhookService L1 集成测试
 *
 * 单测覆盖了签名 / org 白名单 / branch 过滤 / 多连字符仓库名解析，但 mock 了 prisma。
 * 本测试用真实 DB 验证：
 * - 仓库名 → app 反查命中 DB 实存行
 * - app 状态机：HEALTHY → 触发 deploy；DESTROYED → 静默 ignored
 * - HMAC 签名跟实际请求体严格一致（包括 ContentType 边界）
 *
 * runDeployScript 被 mock（不真 docker），但前 4 个分支与 DB lookup 都走真 prisma。
 */

import { createHmac } from 'node:crypto';
import { INestApplication } from '@nestjs/common';
import { createTestApp } from '../../helpers/app.helper';
import { GiteaWebhookService } from '@/modules/internal-app-platform/services/webhook.service';
import { ContainerHostService } from '@/modules/internal-app-platform/services/container-host.service';
import { PrismaService } from '@/core/database/prisma/prisma.service';

// employee_slug schema check: ^[a-z0-9]([a-z0-9-]{1,18}[a-z0-9])?$ — 禁下划线，必须用 -
const T = 't-iap-wh-';
const ORG_CODE = `${T}org`;
const SECRET_OVERRIDE = 't_secret_only_for_test_56789abcdef';

describe('GiteaWebhookService (L1)', () => {
  let app: INestApplication;
  let svc: GiteaWebhookService;
  let prisma: PrismaService;
  let runSpy: jest.SpyInstance;
  let userId: string;
  let orgId: string;
  let employeeSlug: string;
  let appId: string;

  beforeAll(async () => {
    process.env.INTERNAL_APP_GITEA_WEBHOOK_SECRET = SECRET_OVERRIDE;
    app = await createTestApp();
    svc = app.get(GiteaWebhookService);
    prisma = app.get(PrismaService);

    // service 在 constructor 把 secret 读到 this.secret，需要重新构造或反射改值
    // 简单方案：直接覆写私有字段
    (svc as unknown as { secret: string }).secret = SECRET_OVERRIDE;

    // 一次性 fixture
    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: {},
    });
    orgId = org.id;

    const user = await prisma.user.create({
      data: {
        username: `${T}user`,
        email: `${T}user@test.local`,
        passwordHash: 'PLACEHOLDER',
        displayName: `${T}user`,
        status: 'ACTIVE',
        source: 'LOCAL',
        tenantId: 'default',
      },
    });
    userId = user.id;

    employeeSlug = `${T}emp`;
    await prisma.employeeSlugBinding.create({
      data: {
        userId,
        employeeSlug,
        sourceMailNickname: `${T}emp`,
        organizationId: orgId,
        createdById: userId,
      },
    });

    const internalApp = await prisma.internalApp.create({
      data: {
        employeeSlug,
        appSlug: 'hello',
        displayName: `${T}hello`,
        runtime: 'node',
        status: 'HEALTHY',
        url: `http://${employeeSlug}-hello.test.local`,
        giteaRepoFullName: `FFAIApps/${employeeSlug}-hello`,
        organizationId: orgId,
        createdById: userId,
        lastDeployedAt: new Date(),
      },
    });
    appId = internalApp.id;
  });

  afterAll(async () => {
    await prisma.internalAppEnvVar.deleteMany({ where: { app: { employeeSlug } } });
    await prisma.internalAppDeployment.deleteMany({ where: { app: { employeeSlug } } });
    await prisma.internalApp.deleteMany({ where: { employeeSlug } });
    await prisma.internalAppEmployeeToken.deleteMany({ where: { employeeSlug } });
    await prisma.employeeSlugBinding.deleteMany({ where: { employeeSlug } });
    await prisma.user.deleteMany({ where: { id: userId } });
    await prisma.organization.deleteMany({ where: { code: ORG_CODE } });
    await app.close();
  });

  beforeEach(() => {
    const host = app.get(ContainerHostService);
    runSpy = jest.spyOn(host, 'runDeployScript').mockResolvedValue({
      ok: true,
      containerName: 'mock',
      internalHost: 'mock:3000',
      externalUrl: 'http://mock',
      logsTail: [],
    });
  });

  afterEach(() => {
    runSpy.mockRestore();
  });

  const sign = (body: string) =>
    createHmac('sha256', SECRET_OVERRIDE).update(body).digest('hex');

  describe('verifySignature 真实 secret 链路', () => {
    it('rawBody 计算的 sig 匹配通过', () => {
      const body = JSON.stringify({ ref: 'refs/heads/main' });
      expect(svc.verifySignature(body, sign(body))).toBe(true);
    });
    it('rawBody 改了一个字节签名失效', () => {
      const body = JSON.stringify({ ref: 'refs/heads/main' });
      expect(svc.verifySignature(body + ' ', sign(body))).toBe(false);
    });
  });

  describe('handlePush 真 DB lookup', () => {
    const validRepoName = `${T}emp-hello`;

    it('foreign org 拒绝（不查 DB，最早层）', async () => {
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: { full_name: 'EvilOrg/x', clone_url: 'http://x' },
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.code).toBe('foreign_org');
    });

    it('非 main 分支：静默 ignored', async () => {
      const result = await svc.handlePush({
        ref: 'refs/heads/feature/x',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: 'http://x',
        },
      });
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.action).toBe('ignored');
    });

    it('HEALTHY app + main + 有效 sig: 触发 runDeployScript 异步 fire-and-forget', async () => {
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: `http://gitea/FFAIApps/${validRepoName}.git`,
        },
      });
      expect(result.ok).toBe(true);
      if (result.ok && result.action === 'deploy_queued') {
        expect(result.appId).toBe(appId);
      }
      // 异步触发：等 microtask
      await new Promise((r) => setImmediate(r));
      expect(runSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          employeeSlug,
          appSlug: 'hello',
          runtime: 'node',
          repoUrl: `http://gitea/FFAIApps/${validRepoName}.git`,
        }),
      );
    });

    it('DESTROYED app: 静默 ignored，不调用 deploy', async () => {
      await prisma.internalApp.update({
        where: { id: appId },
        data: { status: 'DESTROYED', destroyedAt: new Date() },
      });
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: 'http://x',
        },
      });
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.action).toBe('ignored');
      await new Promise((r) => setImmediate(r));
      expect(runSpy).not.toHaveBeenCalled();

      // 还原以免影响后面的 it
      await prisma.internalApp.update({
        where: { id: appId },
        data: { status: 'HEALTHY', destroyedAt: null },
      });
    });

    it('仓库名映射不到任何 app: app_not_found', async () => {
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: 'FFAIApps/nobody-here-yet',
          clone_url: 'http://x',
        },
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.code).toBe('app_not_found');
    });
  });

  // 状态机推进（PR #447 修复："app 永远卡 PENDING" bug）
  // 详见 .learnings/2026-05-19-webhook-deploy-event-without-db-status-update.md
  describe('handlePush 状态机推进', () => {
    const validRepoName = `${T}emp-hello`;

    beforeEach(async () => {
      // 每次重置 app 状态到 PENDING（模拟 deploy_prepare 刚跑完）
      await prisma.internalApp.update({
        where: { id: appId },
        data: { status: 'PENDING', lastDeployedAt: null },
      });
    });

    it('成功 → status=HEALTHY + lastDeployedAt 刷新', async () => {
      // runSpy 默认 mockResolvedValue ok:true（见上面 beforeEach）
      const before = new Date();
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: `http://gitea/FFAIApps/${validRepoName}.git`,
        },
      });
      expect(result.ok).toBe(true);
      // 中间态 BUILDING 在异步前同步写
      const buildingRow = await prisma.internalApp.findUnique({ where: { id: appId } });
      expect(buildingRow?.status).toBe('BUILDING');
      // 等异步链 .then 跑完
      await new Promise((r) => setTimeout(r, 200));
      const row = await prisma.internalApp.findUnique({ where: { id: appId } });
      expect(row?.status).toBe('HEALTHY');
      expect(row?.lastDeployedAt).not.toBeNull();
      expect(row!.lastDeployedAt!.getTime()).toBeGreaterThanOrEqual(before.getTime());
    });

    it('失败（runDeployScript ok=false）→ status=FAILED，lastDeployedAt 不动', async () => {
      runSpy.mockResolvedValue({
        ok: false,
        error: { code: 'npm_install_failed', message: 'npm ERR! ...' },
      } as any);
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: `http://gitea/FFAIApps/${validRepoName}.git`,
        },
      });
      expect(result.ok).toBe(true);
      await new Promise((r) => setTimeout(r, 200));
      const row = await prisma.internalApp.findUnique({ where: { id: appId } });
      expect(row?.status).toBe('FAILED');
      expect(row?.lastDeployedAt).toBeNull();
    });

    it('runDeployScript throw → status=FAILED（不卡 BUILDING）', async () => {
      runSpy.mockRejectedValue(new Error('container host unreachable'));
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: `http://gitea/FFAIApps/${validRepoName}.git`,
        },
      });
      expect(result.ok).toBe(true);
      await new Promise((r) => setTimeout(r, 200));
      const row = await prisma.internalApp.findUnique({ where: { id: appId } });
      expect(row?.status).toBe('FAILED');
    });

    it('竞态：build 期间 destroy 把 status 写成 DESTROYED → success 路径不覆盖回 HEALTHY', async () => {
      // 把 runDeployScript 改成可控延迟，模拟 build 期间另一个请求写终态
      let release: () => void = () => undefined;
      const gate = new Promise<void>((r) => { release = r; });
      runSpy.mockImplementation(async () => {
        await gate;
        return {
          ok: true,
          containerName: 'mock',
          internalHost: 'mock:3000',
          externalUrl: 'http://mock',
          logsTail: [],
        } as any;
      });
      // 启动 webhook 处理（不 await，让它进 BUILDING 状态后挂起）
      const handlePromise = svc.handlePush({
        ref: 'refs/heads/main',
        repository: {
          full_name: `FFAIApps/${validRepoName}`,
          clone_url: `http://gitea/FFAIApps/${validRepoName}.git`,
        },
      });
      await handlePromise; // handlePush 同步部分（含 BUILDING 写入）完成
      // 模拟 destroy：把 status 直接写 DESTROYED
      await prisma.internalApp.update({
        where: { id: appId },
        data: { status: 'DESTROYED', destroyedAt: new Date() },
      });
      // 现在让 deploy 完成
      release();
      await new Promise((r) => setTimeout(r, 200));
      const row = await prisma.internalApp.findUnique({ where: { id: appId } });
      expect(row?.status).toBe('DESTROYED'); // 不被 HEALTHY 覆盖

      // 还原以免污染后面的 it
      await prisma.internalApp.update({
        where: { id: appId },
        data: { status: 'HEALTHY', destroyedAt: null },
      });
    });
  });
});
