/**
 * GiteaWebhookService Unit Tests
 *
 * 覆盖 07-api §4.4 + §8.4 webhook 安全分层：
 * - HMAC 签名校验（缺 secret / 缺 header / 不匹配 / timing safe）
 * - 仓库 org 白名单（foreign_org 拒绝）
 * - ref 过滤（非 main 静默 ignored）
 * - 仓库名 → app 反向解析（含连字符歧义场景）
 * - app 不存在 / 已 DESTROYED 拦截
 * - 成功路径：调 containerHost.runDeployScript 异步触发
 */

import { createHmac } from 'node:crypto';
import { GiteaWebhookService, type GiteaPushPayload } from '@/modules/internal-app-platform/services/webhook.service';
import type { PrismaService } from '@/core/database/prisma/prisma.service';
import type { ContainerHostService } from '@/modules/internal-app-platform/services/container-host.service';

describe('GiteaWebhookService', () => {
  const SECRET = 'test-webhook-secret-very-long-string';
  let svc: GiteaWebhookService;
  let prisma: jest.Mocked<PrismaService>;
  let containerHost: jest.Mocked<ContainerHostService>;
  let originalSecret: string | undefined;

  beforeAll(() => {
    originalSecret = process.env.INTERNAL_APP_GITEA_WEBHOOK_SECRET;
    process.env.INTERNAL_APP_GITEA_WEBHOOK_SECRET = SECRET;
  });

  afterAll(() => {
    if (originalSecret === undefined) {
      delete process.env.INTERNAL_APP_GITEA_WEBHOOK_SECRET;
    } else {
      process.env.INTERNAL_APP_GITEA_WEBHOOK_SECRET = originalSecret;
    }
  });

  beforeEach(() => {
    prisma = {
      internalApp: { findUnique: jest.fn() },
    } as unknown as jest.Mocked<PrismaService>;
    containerHost = {
      runDeployScript: jest.fn().mockResolvedValue({
        ok: true,
        containerName: 'ffoa-app-lijian-hello',
        internalHost: 'ffoa-app-lijian-hello:3000',
        externalUrl: 'https://lijian-hello.apps.ffworkspace.faradayfuture.com',
        logsTail: [],
      }),
    } as unknown as jest.Mocked<ContainerHostService>;
    svc = new GiteaWebhookService(prisma, containerHost);
  });

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

  describe('verifySignature', () => {
    it('正确签名通过', () => {
      const body = '{"x":1}';
      expect(svc.verifySignature(body, sign(body))).toBe(true);
    });

    it('签名错误拒绝', () => {
      expect(svc.verifySignature('{"x":1}', sign('{"x":2}'))).toBe(false);
    });

    it('缺 header 拒绝', () => {
      expect(svc.verifySignature('{"x":1}', undefined)).toBe(false);
    });

    it('非 hex 签名拒绝（不抛异常）', () => {
      expect(svc.verifySignature('{"x":1}', 'not-hex-!!!')).toBe(false);
    });

    it('长度不一致拒绝（防 timingSafeEqual 抛错）', () => {
      expect(svc.verifySignature('{"x":1}', 'aa')).toBe(false);
    });
  });

  describe('handlePush', () => {
    const validRepo = (name = 'lijian-hello'): GiteaPushPayload => ({
      ref: 'refs/heads/main',
      after: 'abc123',
      repository: {
        full_name: `FFAIApps/${name}`,
        clone_url: `http://43.130.59.228/FFAIApps/${name}.git`,
      },
    });

    it('foreign org 拒绝（即使签名正确也防御）', 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({
        ...validRepo(),
        ref: 'refs/heads/feature/x',
      });
      expect(result.ok).toBe(true);
      if (result.ok && result.action === 'ignored') {
        expect(result.reason).toContain('非 main');
      }
    });

    it('app 不存在 → app_not_found', async () => {
      (prisma.internalApp.findUnique as jest.Mock).mockResolvedValue(null);
      const result = await svc.handlePush(validRepo());
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.code).toBe('app_not_found');
    });

    it('app DESTROYED → 静默 ignored', async () => {
      (prisma.internalApp.findUnique as jest.Mock).mockResolvedValue({
        id: 'a1', employeeSlug: 'lijian', appSlug: 'hello', runtime: 'node', status: 'DESTROYED',
      });
      const result = await svc.handlePush(validRepo());
      expect(result.ok).toBe(true);
      if (result.ok && result.action === 'ignored') {
        expect(result.reason).toContain('DESTROYED');
      }
    });

    it('happy path 触发 runDeployScript', async () => {
      (prisma.internalApp.findUnique as jest.Mock).mockResolvedValue({
        id: 'a1', employeeSlug: 'lijian', appSlug: 'hello', runtime: 'node', status: 'HEALTHY',
      });
      const result = await svc.handlePush(validRepo());
      expect(result.ok).toBe(true);
      if (result.ok && result.action === 'deploy_queued') {
        expect(result.appId).toBe('a1');
      }
      // 异步触发：等微任务清空
      await new Promise((r) => setImmediate(r));
      expect(containerHost.runDeployScript).toHaveBeenCalledWith(
        expect.objectContaining({
          employeeSlug: 'lijian',
          appSlug: 'hello',
          runtime: 'node',
          repoUrl: 'http://43.130.59.228/FFAIApps/lijian-hello.git',
        }),
      );
    });

    it('仓库名含多连字符时迭代切分能找到正确 app', async () => {
      // 仓库名 hongwei-zhang-birthday-reminder：
      // 切点 0: ('hongwei', 'zhang-birthday-reminder') ← 第 1 次查空
      // 切点 1: ('hongwei-zhang', 'birthday-reminder') ← 第 2 次查命中
      (prisma.internalApp.findUnique as jest.Mock)
        .mockResolvedValueOnce(null)
        .mockResolvedValueOnce({
          id: 'a2',
          employeeSlug: 'hongwei-zhang',
          appSlug: 'birthday-reminder',
          runtime: 'node',
          status: 'HEALTHY',
        });
      const result = await svc.handlePush(validRepo('hongwei-zhang-birthday-reminder'));
      expect(result.ok).toBe(true);
      if (result.ok && result.action === 'deploy_queued') {
        expect(result.appId).toBe('a2');
      }
      expect(prisma.internalApp.findUnique).toHaveBeenCalledTimes(2);
    });

    it('缺 clone_url → missing_clone_url', async () => {
      (prisma.internalApp.findUnique as jest.Mock).mockResolvedValue({
        id: 'a1', employeeSlug: 'lijian', appSlug: 'hello', runtime: 'node', status: 'HEALTHY',
      });
      const result = await svc.handlePush({
        ref: 'refs/heads/main',
        repository: { full_name: 'FFAIApps/lijian-hello' },
      });
      expect(result.ok).toBe(false);
      if (!result.ok) expect(result.code).toBe('missing_clone_url');
    });
  });
});
