import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { createTestApp } from '../../helpers/app.helper';
import { cleanupDatabase } from '../../helpers/cleanup.helper';
import {
  assignRoleToUser,
  createAdminUser,
  createTestUser,
} from '../../helpers/factories';

/**
 * Meeting-attendance · Meetings Controller L1 集成测试
 *
 * 覆盖 meetings.controller.ts 全部 14 个 endpoint 的基础行为：
 *   GET    /meetings
 *   POST   /meetings
 *   GET    /meetings/:id
 *   PUT    /meetings/:id
 *   DELETE /meetings/:id
 *   GET    /meetings/:id/public          (@Public)
 *   GET    /meetings/:id/required-attendees
 *   POST   /meetings/:id/required-attendees
 *   DELETE /meetings/:id/required-attendees/:userId
 *   POST   /meetings/:id/import-attendees
 *   PATCH  /meetings/:id/enforce-checkin-mode
 *   PATCH  /meetings/:id/required-attendees/:userId/checkin-mode
 *   POST   /meetings/:id/mark-absent
 *   GET    /meetings/:id/attendees/search (@Public)
 *
 * 与已有测试互补（不重复覆盖深度业务规则）：
 *   - enforce-checkin-mode-toggle.api.test.ts：已覆盖 ON→OFF/OFF→ON 切换效果
 *   - v12-checkin-mode.api.test.ts：已覆盖 city 空校验、required-attendee enrichment
 *   - meetings-required-attendees.api.test.ts：已覆盖 GET/POST/DELETE required-attendees
 *     的深度业务规则（带 attendance 限制/applyUpdatedEvent/reconcileManagedBindings）
 *
 * 本文件补充：每个 endpoint 的 happy path + 401/403 鉴权 + 关键业务错误 + 边界值
 *
 * 关联工单 #341 · Batch 100% 覆盖
 */
describe('Meeting-attendance · Meetings API', () => {
  let app: INestApplication;
  let prisma: PrismaService;

  beforeAll(async () => {
    process.env.NODE_ENV = 'test';
    app = await createTestApp();
    prisma = app.get<PrismaService>(PrismaService);
    // 清前一个 suite 残留的 orphan meeting/series（旧测试文件不全清 meeting 表）
    await prisma.meetingAttendance.deleteMany({}).catch(() => undefined);
    await prisma.meetingRequiredAttendee.deleteMany({}).catch(() => undefined);
    await prisma.meeting.deleteMany({}).catch(() => undefined);
    await prisma.meetingSeries.deleteMany({}).catch(() => undefined);
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    // 先清 meeting 相关数据，避免 cleanupDatabase 只清 user 后留下 orphan meeting
    // → GET 列表 include creator (required relation) 撞孤儿 FK 报 500
    await prisma.meetingAttendance.deleteMany({}).catch(() => undefined);
    await prisma.meetingRequiredAttendee.deleteMany({}).catch(() => undefined);
    await prisma.meeting.deleteMany({}).catch(() => undefined);
    await cleanupDatabase(prisma);
  });

  afterAll(async () => {
    await app.close();
  });

  // ============================================================
  // helpers（直接复用 checkin.api.test.ts 风格）
  // ============================================================

  function suffix() {
    return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  }

  async function login(username: string, password: string): Promise<string> {
    const resp = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username, password })
      .expect(200);
    return resp.body.data.accessToken as string;
  }

  async function ensureRole(code: 'Administrator' | 'Employee' | 'MeetingManager') {
    return prisma.role.upsert({
      where: { code },
      create: { code, name: code, enabled: true, isBuiltIn: true },
      update: { enabled: true },
    });
  }

  async function setupAdmin() {
    const s = suffix();
    const adminUser = await createAdminUser({
      username: `ci_adm_${s}`,
      email: `ci_adm_${s}@example.com`,
      password: 'Admin@123',
      displayName: `Meetings Admin ${s}`,
    });
    const adminToken = await login(adminUser.username, 'Admin@123');
    return { adminUser, adminToken };
  }

  async function setupEmployee() {
    const s = suffix();
    const employee = await createTestUser({
      username: `ci_emp_${s}`,
      email: `ci_emp_${s}@example.com`,
      password: 'Emp@123',
      displayName: `Meetings Emp ${s}`,
    });
    const role = await ensureRole('Employee');
    await assignRoleToUser(employee.id, role.id);
    const empToken = await login(employee.username, 'Emp@123');
    return { employee, empToken };
  }

  async function createMeeting(opts: {
    creatorId: string;
    startOffsetMin?: number;
    durationMin?: number;
    status?: 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
    city?: string | null;
    enforceCheckinMode?: boolean;
  }) {
    const startOffsetMin = opts.startOffsetMin ?? 60;
    const duration = opts.durationMin ?? 60;
    const now = new Date();
    const startTime = new Date(now.getTime() + startOffsetMin * 60 * 1000);
    const endTime = new Date(startTime.getTime() + duration * 60 * 1000);
    return prisma.meeting.create({
      data: {
        title: `Meetings Test ${suffix()}`,
        startTime,
        endTime,
        timezone: 'UTC',
        location: 'HQ',
        type: 'HYBRID',
        status: opts.status ?? 'SCHEDULED',
        creatorId: opts.creatorId,
        city: opts.city ?? null,
        enforceCheckinMode: opts.enforceCheckinMode ?? false,
      } as any,
    });
  }

  async function addRequiredAttendee(meetingId: string, userId: string) {
    return (prisma as any).meetingRequiredAttendee.create({
      data: {
        meetingId,
        userId,
        role: 'REGULAR_ATTENDEE',
      },
    });
  }

  // ============================================================
  // GET /meetings
  // ============================================================

  describe('GET /meetings', () => {
    it('[1] 有 token 可获取列表（全局 JwtAuthGuard 保护）→ 200 + meetings[]', async () => {
      const { adminToken } = await setupAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body).toHaveProperty('meetings');
      expect(Array.isArray(resp.body.meetings)).toBe(true);
      expect(resp.body).toHaveProperty('pagination');
    });

    it('[2] 带 status=SCHEDULED 查询参数 → 200 + 分页信息', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      await createMeeting({ creatorId: adminUser.id, startOffsetMin: 120 });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings?status=SCHEDULED')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.meetings).toBeDefined();
      expect(resp.body.pagination).toHaveProperty('page');
      expect(resp.body.pagination).toHaveProperty('total');
    });

    it('[3] 分页参数 page/limit 生效 → 200 + totalPages 正确', async () => {
      const { adminToken } = await setupAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings?page=1&limit=5')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.pagination.limit).toBe(5);
    });
  });

  // ============================================================
  // POST /meetings
  // ============================================================

  describe('POST /meetings', () => {
    it('[4] admin 创建会议 → 201 + 返回 meeting 对象含 id', async () => {
      const { adminToken } = await setupAdmin();
      const future = new Date(Date.now() + 2 * 3600 * 1000).toISOString();
      const futureEnd = new Date(Date.now() + 3 * 3600 * 1000).toISOString();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `Test Meeting ${suffix()}`,
          startTime: future,
          endTime: futureEnd,
          location: 'Room A',
          type: 'OFFLINE',
          timezone: 'UTC',
        })
        .expect(201);

      expect(resp.body).toHaveProperty('id');
      expect(resp.body).toHaveProperty('title');
      expect(resp.body).toHaveProperty('qrCodeOnline');
    });

    it('[5] 无 token → 401', async () => {
      const future = new Date(Date.now() + 2 * 3600 * 1000).toISOString();
      const futureEnd = new Date(Date.now() + 3 * 3600 * 1000).toISOString();

      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .send({ title: 'Test', startTime: future, endTime: futureEnd })
        .expect(401);
    });

    it('[6] 普通员工 token → 403', async () => {
      const { empToken } = await setupEmployee();
      const future = new Date(Date.now() + 2 * 3600 * 1000).toISOString();
      const futureEnd = new Date(Date.now() + 3 * 3600 * 1000).toISOString();

      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${empToken}`)
        .send({ title: 'Test', startTime: future, endTime: futureEnd })
        .expect(403);
    });

    it('[7] enforceCheckinMode=true 但未设 city → 400 MEETING_ATTENDANCE_036', async () => {
      const { adminToken } = await setupAdmin();
      const future = new Date(Date.now() + 2 * 3600 * 1000).toISOString();
      const futureEnd = new Date(Date.now() + 3 * 3600 * 1000).toISOString();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `Test ${suffix()}`,
          startTime: future,
          endTime: futureEnd,
          enforceCheckinMode: true,
          // city 故意不传
        })
        .expect(400);

      expect(resp.body.error).toMatch(/签到方式校验前请先配置会议地点/);
      expect(resp.body.code).toBe('MEETING_ATTENDANCE_036');
    });
  });

  // ============================================================
  // GET /meetings/:id
  // ============================================================

  describe('GET /meetings/:id', () => {
    it('[8] 有 token 可获取会议详情（全局 JwtAuthGuard 保护）→ 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.id).toBe(meeting.id);
      expect(resp.body).toHaveProperty('requiredAttendees');
      expect(resp.body).toHaveProperty('attendances');
    });

    it('[9] 不存在的 id → 404', async () => {
      const { adminToken } = await setupAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings/non-existent-meeting-id')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // PUT /meetings/:id
  // ============================================================

  describe('PUT /meetings/:id', () => {
    it('[10] admin 更新会议标题 → 200 + 新标题体现在返回', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      const newTitle = `Updated Title ${suffix()}`;

      const resp = await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ title: newTitle })
        .expect(200);

      expect(resp.body.title).toBe(newTitle);
    });

    it('[11] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .send({ title: 'New Title' })
        .expect(401);
    });

    it('[12] 普通员工 → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ title: 'New Title' })
        .expect(403);
    });

    it('[13] 不存在的 id → 404', async () => {
      const { adminToken } = await setupAdmin();

      const resp = await request(app.getHttpServer())
        .put('/api/v1/meeting-attendance/meetings/non-existent-meeting-id')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ title: 'X' })
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });

    it('[14] enforceCheckinMode=true + city 为空 → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id, city: null });

      const resp = await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ enforceCheckinMode: true })
        .expect(400);

      expect(resp.body.code).toBe('MEETING_ATTENDANCE_036');
    });
  });

  // ============================================================
  // DELETE /meetings/:id
  // ============================================================

  describe('DELETE /meetings/:id', () => {
    it('[15] admin 删除无签到记录的会议 → 200 + type=hard_delete', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.type).toBe('hard_delete');
    });

    it('[16] 有签到记录的会议 → 200 + type=soft_delete（改为 CANCELLED）', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      // 插入一条签到记录
      await prisma.meetingAttendance.create({
        data: {
          meetingId: meeting.id,
          userId: employee.id,
          status: 'ON_SITE',
          checkinTime: new Date(),
          isLate: false,
          isEarlyLeave: false,
        } as any,
      });

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.type).toBe('soft_delete');
      expect(resp.body.message).toMatch(/cancelled/i);
    });

    it('[17] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}`)
        .expect(401);
    });

    it('[18] 不存在的 id → 404', async () => {
      const { adminToken } = await setupAdmin();

      const resp = await request(app.getHttpServer())
        .delete('/api/v1/meeting-attendance/meetings/non-existent-meeting-id')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // GET /meetings/:id/public  (@Public)
  // ============================================================

  describe('GET /meetings/:id/public', () => {
    it('[19] 无 token 获取公开会议信息 → 200 + canCheckin 字段', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/public`)
        .expect(200);

      expect(resp.body.id).toBe(meeting.id);
      expect(resp.body).toHaveProperty('canCheckin');
      expect(resp.body).toHaveProperty('checkinStartTime');
      expect(resp.body).toHaveProperty('checkinEndTime');
    });

    it('[20] 不存在的 id → 404', async () => {
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/public')
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });

    it('[21] 会议在 15 分钟签到窗内 → canCheckin=true', async () => {
      const { adminUser } = await setupAdmin();
      // startOffsetMin=5：会议在 5 分钟后开始，在 15 分钟签到窗内
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 5 });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/public`)
        .expect(200);

      expect(resp.body.canCheckin).toBe(true);
    });

    it('[22] 会议在 60 分钟后开始（窗口未开）→ canCheckin=false', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 60 });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/public`)
        .expect(200);

      expect(resp.body.canCheckin).toBe(false);
    });
  });

  // ============================================================
  // GET /meetings/:id/required-attendees
  // 注意：深度业务规则（enrichment/applyUpdatedEvent）已被 meetings-required-attendees.api.test.ts 覆盖
  // 本处只测 happy path + 鉴权
  // ============================================================

  describe('GET /meetings/:id/required-attendees', () => {
    it('[23] 有 token 可获取参会人列表（全局 JwtAuthGuard 保护）→ 200 + requiredAttendees[]', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body).toHaveProperty('requiredAttendees');
      expect(Array.isArray(resp.body.requiredAttendees)).toBe(true);
      expect(resp.body.requiredAttendees.length).toBe(1);
    });

    it('[24] 不存在的 meeting id → 返回空列表（service 层不 throw 404，直接返回空）', async () => {
      // NOTE: listRequiredAttendees 不 throw 404，meeting=null 时 requiredAttendees 查询仍返回空数组
      const { adminToken } = await setupAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings/non-existent-id/required-attendees')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.requiredAttendees).toEqual([]);
    });
  });

  // ============================================================
  // POST /meetings/:id/required-attendees
  // 注意：深度规则（applyUpdatedEvent/reconcileManagedBindings）已被 meetings-required-attendees 覆盖
  // ============================================================

  describe('POST /meetings/:id/required-attendees', () => {
    it('[25] admin 添加参会人 → 201 + "Attendees added successfully"', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userIds: [employee.id] })
        .expect(201);

      expect(resp.body.message).toMatch(/added successfully/i);
    });

    it('[26] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees`)
        .send({ userIds: ['some-user-id'] })
        .expect(401);
    });

    it('[27] 空 attendees 列表 → 400 Attendee list cannot be empty', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userIds: [] })
        .expect(400);

      expect(resp.body.error).toMatch(/cannot be empty/i);
    });

    it('[28] 不存在的 meeting id → 404', async () => {
      const { adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/required-attendees')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ userIds: [employee.id] })
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // DELETE /meetings/:id/required-attendees/:userId
  // 注意：带 attendance 记录的删除限制已被 meetings-required-attendees 深度覆盖
  // ============================================================

  describe('DELETE /meetings/:id/required-attendees/:userId', () => {
    it('[29] admin 删除无签到记录的参会人 → 200 + remainingCount', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.message).toMatch(/deleted successfully/i);
      expect(resp.body).toHaveProperty('remainingCount');
    });

    it('[30] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}`)
        .expect(401);
    });

    it('[31] 参会人不在列表中 → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      // 故意不添加 employee 到 required-attendees

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // POST /meetings/:id/import-attendees
  // ============================================================

  describe('POST /meetings/:id/import-attendees', () => {
    it('[32] admin 通过 email 导入参会人 → 200 + results.created >= 1', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/import-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendees: [{ email: employee.email, role: 'OPTIONAL_ATTENDEE' }] })
        .expect(200);

      expect(resp.body.message).toMatch(/import completed/i);
      expect(resp.body.results.created).toBeGreaterThanOrEqual(1);
    });

    it('[33] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/import-attendees`)
        .send({ attendees: [] })
        .expect(401);
    });

    it('[34] attendees 为空数组 → 400 Attendees data cannot be empty', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/import-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendees: [] })
        .expect(400);

      expect(resp.body.error).toMatch(/cannot be empty/i);
    });

    it('[35] 不存在的 meeting id → 404', async () => {
      const { adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/import-attendees')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendees: [{ email: employee.email }] })
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // PATCH /meetings/:id/enforce-checkin-mode
  // 注意：ON→OFF/OFF→ON 切换效果已被 enforce-checkin-mode-toggle.api.test.ts 覆盖
  // 本处补充：基础 happy path + 鉴权 + enforceCheckinMode 非布尔值 400
  // ============================================================

  describe('PATCH /meetings/:id/enforce-checkin-mode', () => {
    it('[36] admin 开启 enforce-checkin-mode（已有 city）→ 200 + enforceCheckinMode=true', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        city: 'Shanghai',
        enforceCheckinMode: false,
      });

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/enforce-checkin-mode`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ enforceCheckinMode: true })
        .expect(200);

      expect(resp.body.enforceCheckinMode).toBe(true);
    });

    it('[37] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/enforce-checkin-mode`)
        .send({ enforceCheckinMode: true })
        .expect(401);
    });

    it('[38] enforceCheckinMode 为字符串（非布尔）→ 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/enforce-checkin-mode`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ enforceCheckinMode: 'yes' })
        .expect(400);

      expect(resp.body.error).toMatch(/enforceCheckinMode must be boolean/i);
    });
  });

  // ============================================================
  // PATCH /meetings/:id/required-attendees/:userId/checkin-mode
  // 注意：checkin-mode enrichment 已被 v12-checkin-mode.api.test.ts 覆盖
  // ============================================================

  describe('PATCH /meetings/:id/required-attendees/:userId/checkin-mode', () => {
    it('[39] admin 设置参会人 checkinMode=ON_SITE → 200 + checkinMode=ON_SITE', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ checkinMode: 'ON_SITE' })
        .expect(200);

      expect(resp.body.checkinMode).toBe('ON_SITE');
      expect(resp.body.userId).toBe(employee.id);
    });

    it('[40] 传 checkinMode=null 清除覆盖 → 200 + checkinMode=null', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      // 先设置一个 override
      await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ checkinMode: 'ONLINE' })
        .expect(200);

      // 再清除
      const resp = await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ checkinMode: null })
        .expect(200);

      expect(resp.body.checkinMode).toBeNull();
    });

    it('[41] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .send({ checkinMode: 'ON_SITE' })
        .expect(401);
    });

    it('[42] 非法 checkinMode 值（非 ON_SITE/ONLINE/null）→ 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ checkinMode: 'INVALID_MODE' })
        .expect(400);

      expect(resp.body.error).toMatch(/ON_SITE, ONLINE or null/i);
    });

    it('[43] 参会人不在 required-attendees 中 → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      // 故意不添加 employee 到 required-attendees

      const resp = await request(app.getHttpServer())
        .patch(
          `/api/v1/meeting-attendance/meetings/${meeting.id}/required-attendees/${employee.id}/checkin-mode`,
        )
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ checkinMode: 'ON_SITE' })
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // POST /meetings/:id/mark-absent
  // ============================================================

  describe('POST /meetings/:id/mark-absent', () => {
    it('[44] admin 对 COMPLETED 会议执行 mark-absent → 200 + count', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      // 创建一个已结束的会议（过去 2 小时前开始，持续 1 小时 → 已结束）
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        startOffsetMin: -120,
        durationMin: 60,
        status: 'COMPLETED',
      });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/mark-absent`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(resp.body.count).toBeGreaterThanOrEqual(1);
      expect(resp.body.message).toMatch(/absent/i);
    });

    it('[45] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        status: 'COMPLETED',
      });

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/mark-absent`)
        .expect(401);
    });

    it('[46] 普通员工 → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        status: 'COMPLETED',
      });

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/mark-absent`)
        .set('Authorization', `Bearer ${empToken}`)
        .expect(403);
    });

    it('[47] 会议状态不是 COMPLETED → 400 must be completed', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        status: 'SCHEDULED',
      });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/mark-absent`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(400);

      expect(resp.body.error).toMatch(/must be completed/i);
    });

    it('[48] 不存在的 meeting id → 404', async () => {
      const { adminToken } = await setupAdmin();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/mark-absent')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });
  });

  // ============================================================
  // GET /meetings/:id/attendees/search  (@Public)
  // ============================================================

  describe('GET /meetings/:id/attendees/search', () => {
    it('[49] 无 token 搜索参会人 → 200 + attendees[]', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendees/search?q=${employee.email.slice(0, 6)}`)
        .expect(200);

      expect(resp.body).toHaveProperty('attendees');
      expect(Array.isArray(resp.body.attendees)).toBe(true);
    });

    it('[50] query 少于 2 个字符 → 200 + attendees=[]', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendees/search?q=a`)
        .expect(200);

      expect(resp.body.attendees).toEqual([]);
    });

    it('[51] 不存在的 meeting id → 404', async () => {
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/meetings/non-existent-id/attendees/search?q=test')
        .expect(404);

      expect(resp.body.error).toMatch(/not found/i);
    });

    it('[52] 搜索不存在的关键词 → 200 + attendees=[]', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendees/search?q=xxxxxxnotexist`)
        .expect(200);

      expect(resp.body.attendees).toEqual([]);
    });
  });
});
