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 · Series Controller L1 集成测试
 *
 * 覆盖 17 个 endpoint 的 HTTP 行为 + 鉴权 + 核心业务规则：
 *   GET    /series
 *   POST   /series
 *   GET    /series/:id
 *   PUT    /series/:id
 *   DELETE /series/:id
 *   GET    /series/:id/history
 *   POST   /series/:id/update-schedule
 *   GET    /series/:id/attendees
 *   POST   /series/:id/attendees
 *   PATCH  /series/:id/attendees
 *   DELETE /series/:id/attendees
 *   PUT    /series/:id/meetings/:meetingId
 *   DELETE /series/:id/meetings/:meetingId
 *   PATCH  /series/:id/enforce-checkin-mode
 *   GET    /series/:id/attendee-preferences
 *   PUT    /series/:id/attendee-preferences
 *   DELETE /series/:id/attendee-preferences/:userId
 *
 * 鉴权层：
 *   - requireAdminOrManager → 需要 Administrator 或 MeetingManager 角色
 *   - requireMeetingUser    → 需要任意会议角色（含 Employee / Leader）
 *   - canMutateSeries       → 系列变更额外鉴权：admin/manager/leader/series-creator
 *
 * 关联工单 #341 · Batch 100% 覆盖
 */
describe('Meeting-attendance · Series 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（旧测试文件不全清表）
    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 / series 相关数据，避免 cleanupDatabase 只清 user 后留下 orphan
    // → 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 prisma.meetingSeries.deleteMany({}).catch(() => undefined);
    await cleanupDatabase(prisma);
  });

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

  // ============================================================
  // helpers
  // ============================================================

  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' | 'Leader') {
    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: `Series 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: `Series 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 setupManager() {
    const s = suffix();
    const manager = await createTestUser({
      username: `ci_mgr_${s}`,
      email: `ci_mgr_${s}@example.com`,
      password: 'Mgr@123',
      displayName: `Series Mgr ${s}`,
    });
    const role = await ensureRole('MeetingManager');
    await assignRoleToUser(manager.id, role.id);
    const mgrToken = await login(manager.username, 'Mgr@123');
    return { manager, mgrToken };
  }

  /**
   * 创建一个会议系列（直接写 DB），返回 series + 至少 1 个 meeting 实例
   */
  async function createSeries(opts: {
    creatorId: string;
    title?: string;
    pattern?: 'WEEKLY' | 'DAILY' | 'MONTHLY';
    maxOccurrences?: number;
    city?: string | null;
    enforceCheckinMode?: boolean;
  }) {
    const s = suffix();
    const title = opts.title ?? `Test Series ${s}`;
    const pattern = opts.pattern ?? 'WEEKLY';
    const maxOccurrences = opts.maxOccurrences ?? 3;
    const city = opts.city !== undefined ? opts.city : null;
    const enforceCheckinMode = opts.enforceCheckinMode ?? false;

    // startDate 从明天开始，避免实例被判定为过去时间
    const startDate = new Date();
    startDate.setDate(startDate.getDate() + 1);
    startDate.setHours(9, 0, 0, 0);

    const series = await (prisma as any).meetingSeries.create({
      data: {
        title,
        pattern,
        frequency: 1,
        startDate,
        timezone: 'UTC',
        type: 'OFFLINE',
        location: 'HQ',
        creatorId: opts.creatorId,
        city,
        enforceCheckinMode,
        maxOccurrences,
      },
    });

    // 生成 meeting 实例
    const meetings: any[] = [];
    for (let i = 0; i < maxOccurrences; i++) {
      const instanceStart = new Date(startDate);
      if (pattern === 'WEEKLY') instanceStart.setDate(instanceStart.getDate() + 7 * i);
      else if (pattern === 'DAILY') instanceStart.setDate(instanceStart.getDate() + i);
      else instanceStart.setMonth(instanceStart.getMonth() + i);

      const instanceEnd = new Date(instanceStart.getTime() + 60 * 60 * 1000);
      const meeting = await (prisma as any).meeting.create({
        data: {
          title: `${title} - Session ${i + 1}`,
          startTime: instanceStart,
          endTime: instanceEnd,
          timezone: 'UTC',
          location: 'HQ',
          type: 'OFFLINE',
          status: 'SCHEDULED',
          creatorId: opts.creatorId,
          seriesId: series.id,
          instanceNumber: i + 1,
          isSeriesMaster: i === 0,
          city,
          enforceCheckinMode,
        },
      });
      meetings.push(meeting);
    }

    return { series, meetings };
  }

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

  // ============================================================
  // GET /series
  // ============================================================

  describe('GET /series', () => {
    it('[1] 管理员可以拉取系列列表 → 200 + series 数组', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      await createSeries({ creatorId: adminUser.id });

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

      expect(Array.isArray(resp.body.series)).toBe(true);
      expect(resp.body.series.length).toBeGreaterThanOrEqual(1);
    });

    it('[2] 无 token → 401', async () => {
      await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/series')
        .expect(401);
    });

    it('[3] 任意有效 token 均可获取系列列表（GET /series 无角色限制）→ 200 + series 数组', async () => {
      // GET /series 只受全局 JwtAuthGuard 保护，controller 没有 requireAdminOrManager
      // 有效 token（无论角色）均能访问，不返回 403
      const s = suffix();
      const noRoleUser = await createTestUser({
        username: `ci_norole_${s}`,
        email: `ci_norole_${s}@example.com`,
        password: 'NoRole@123',
        displayName: `NoRole ${s}`,
      });
      const tok = await login(noRoleUser.username, 'NoRole@123');

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

      expect(Array.isArray(resp.body.series)).toBe(true);
    });

    it('[4] 返回空列表时不报错 → 200 + series 为数组（无数据时）', async () => {
      // 注意：cleanupDatabase 不保证完全清空系列，只是验证格式正常
      const { adminToken } = await setupAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(200);

      expect(Array.isArray(resp.body.series)).toBe(true);
    });
  });

  // ============================================================
  // POST /series
  // ============================================================

  describe('POST /series', () => {
    it('[5] 管理员创建系列 → 201 + series + meetings', async () => {
      const { adminToken } = await setupAdmin();
      const s = suffix();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `Series Create Test ${s}`,
          pattern: 'WEEKLY',
          startDate: new Date(Date.now() + 86400000).toISOString(),
          duration: 60,
          location: 'Room A',
          type: 'OFFLINE',
          timezone: 'UTC',
          maxOccurrences: 2,
        })
        .expect(201);

      expect(resp.body.message).toMatch(/created successfully/i);
      expect(resp.body.series).toBeDefined();
      expect(Array.isArray(resp.body.meetings)).toBe(true);
      expect(resp.body.meetings.length).toBe(2);
    });

    it('[6] 无 token → 401', async () => {
      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .send({ title: 'X', pattern: 'WEEKLY', startDate: new Date().toISOString(), duration: 60 })
        .expect(401);
    });

    it('[7] 普通员工 token → 403', async () => {
      const { empToken } = await setupEmployee();
      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${empToken}`)
        .send({ title: 'X', pattern: 'WEEKLY', startDate: new Date().toISOString(), duration: 60 })
        .expect(403);
    });

    it('[8] 缺少必填字段 title → 400', async () => {
      const { adminToken } = await setupAdmin();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ pattern: 'WEEKLY', startDate: new Date().toISOString(), duration: 60 })
        .expect(400);

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

    it('[9] enforceCheckinMode=true 但无 city → 400 MEETING_ATTENDANCE_036', async () => {
      const { adminToken } = await setupAdmin();
      const s = suffix();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `EnforceNoCity ${s}`,
          pattern: 'WEEKLY',
          startDate: new Date(Date.now() + 86400000).toISOString(),
          duration: 60,
          enforceCheckinMode: true,
          // 故意不传 city
        })
        .expect(400);

      expect(resp.body.error).toMatch(/签到方式校验/);
    });

    it('[10] enforceCheckinMode=true 且有 city → 201', async () => {
      const { adminToken } = await setupAdmin();
      const s = suffix();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `EnforceWithCity ${s}`,
          pattern: 'WEEKLY',
          startDate: new Date(Date.now() + 86400000).toISOString(),
          duration: 60,
          enforceCheckinMode: true,
          city: 'SZ',
          maxOccurrences: 1,
        })
        .expect(201);

      expect(resp.body.series.enforceCheckinMode).toBe(true);
      expect(resp.body.series.city).toBe('SZ');
    });
  });

  // ============================================================
  // GET /series/:id
  // ============================================================

  describe('GET /series/:id', () => {
    it('[11] 获取存在的系列详情 → 200 + 包含 meetings', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

      expect(resp.body.id).toBe(series.id);
      expect(Array.isArray(resp.body.meetings)).toBe(true);
    });

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

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

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

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

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

  // ============================================================
  // PUT /series/:id
  // ============================================================

  describe('PUT /series/:id', () => {
    it('[14] 管理员更新系列标题 → 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });
      const s = suffix();

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

      expect(resp.body.title).toBe(`Updated Title ${s}`);
    });

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

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

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

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

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

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

    it('[18] 更新时 enforceCheckinMode=true 但无 city → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: null });

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

      expect(resp.body.error).toMatch(/签到方式校验/);
    });
  });

  // ============================================================
  // DELETE /series/:id
  // ============================================================

  describe('DELETE /series/:id', () => {
    it('[19] 删除无出勤记录的系列 → 200 + hard_delete', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

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

    it('[20] 有出勤记录的系列 → 200 + soft_delete（禁用）', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      const { employee } = await setupEmployee();

      // 写入出勤记录
      await (prisma as any).meetingAttendance.create({
        data: {
          meetingId: meetings[0].id,
          userId: employee.id,
          status: 'ON_SITE',
          checkinType: 'MANUAL',
          checkinTime: new Date(),
          isLate: false,
          isEarlyLeave: false,
        },
      });

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

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

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

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

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

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

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

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

  // ============================================================
  // GET /series/:id/history
  // ============================================================

  describe('GET /series/:id/history', () => {
    it('[24] 管理员获取历史（无已完成会议） → 200 + meetings=[]', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

      expect(resp.body.meetings).toBeDefined();
      expect(Array.isArray(resp.body.meetings)).toBe(true);
      expect(resp.body.summary).toBeDefined();
    });

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

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

    it('[26] 不存在的系列 → 200 + meetings=[]（查询 COMPLETED 过滤后自然为空）', async () => {
      const { adminToken } = await setupAdmin();

      // service 直接查 meeting where seriesId=xxx AND status=COMPLETED，
      // 不存在的 seriesId 只会返回空数组而不是 404
      // TODO: service 对不存在的 seriesId 未显式抛 404；此行为待确认
      const resp = await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/series/non-existent-series-id/history')
        .set('Authorization', `Bearer ${adminToken}`);

      // 接受 200 空列表 或 404
      expect([200, 404]).toContain(resp.status);
    });

    it('[27] 有 COMPLETED 会议时 → 200 + summary.totalMeetings > 0', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

      // 将第一个实例标记为 COMPLETED
      await (prisma as any).meeting.update({
        where: { id: meetings[0].id },
        data: { status: 'COMPLETED' },
      });

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

      expect(resp.body.summary.totalMeetings).toBe(1);
    });
  });

  // ============================================================
  // POST /series/:id/update-schedule
  // ============================================================

  describe('POST /series/:id/update-schedule', () => {
    it('[28] 管理员更新调度时间 → 200 + updatedCount >= 1', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/update-schedule`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ startTime: '10:00', duration: '90', timezone: 'UTC' })
        .expect(200);

      expect(resp.body.success).toBe(true);
      expect(resp.body.updatedCount).toBeGreaterThanOrEqual(1);
    });

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

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/update-schedule`)
        .send({ startTime: '10:00', duration: '60', timezone: 'UTC' })
        .expect(401);
    });

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

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/update-schedule`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ startTime: '10:00', duration: '60', timezone: 'UTC' })
        .expect(403);
    });

    it('[31] 缺少必填字段 → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/update-schedule`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ startTime: '10:00' }) // 缺 duration 和 timezone
        .expect(400);

      expect(resp.body.error).toMatch(/Missing required fields/i);
    });

    it('[32] 非法时间格式 → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/update-schedule`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ startTime: 'invalid', duration: '60', timezone: 'UTC' })
        .expect(400);

      expect(resp.body.error).toMatch(/Invalid time format/i);
    });

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

      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series/non-existent-id/update-schedule')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ startTime: '10:00', duration: '60', timezone: 'UTC' })
        .expect(404);
    });
  });

  // ============================================================
  // GET /series/:id/attendees  (requireMeetingUser + canMutateSeries || creatorId)
  // ============================================================

  describe('GET /series/:id/attendees', () => {
    it('[34] 系列创建者（admin）可查看参会人 → 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

      expect(resp.body.series.id).toBe(series.id);
      expect(Array.isArray(resp.body.meetings)).toBe(true);
    });

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

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

    it('[36] 普通员工（非创建者）→ 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${empToken}`)
        .expect(403);
    });

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

      await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/series/non-existent-id/attendees')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });

  // ============================================================
  // POST /series/:id/attendees
  // ============================================================

  describe('POST /series/:id/attendees', () => {
    it('[38] 管理员设置参会人（replace 模式）→ 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

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

    it('[39] action=add 追加参会人 → 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

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

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

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

    it('[41] 普通员工（非创建者）→ 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ attendees: [] })
        .expect(403);
    });

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

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

  // ============================================================
  // PATCH /series/:id/attendees  (updateAttendeeRole)
  // ============================================================

  describe('PATCH /series/:id/attendees', () => {
    it('[43] 管理员更新参会人角色 → 200 + updatedCount', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      const attendee = await addRequiredAttendee(meetings[0].id, employee.id);

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendeeId: attendee.id, newRole: 'OPTIONAL_ATTENDEE' })
        .expect(200);

      expect(resp.body.newRole).toBe('OPTIONAL_ATTENDEE');
    });

    it('[44] 非法 role 值 → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      const attendee = await addRequiredAttendee(meetings[0].id, employee.id);

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendeeId: attendee.id, newRole: 'INVALID_ROLE_XYZ' })
        .expect(400);

      expect(resp.body.error).toMatch(/Invalid role/i);
    });

    it('[45] 缺少 attendeeId → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ newRole: 'OPTIONAL_ATTENDEE' }) // 缺 attendeeId
        .expect(400);

      expect(resp.body.error).toMatch(/Missing attendee ID/i);
    });

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

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .send({ attendeeId: 'x', newRole: 'OPTIONAL_ATTENDEE' })
        .expect(401);
    });

    it('[47] attendeeId 不存在 → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendeeId: 'non-existent-attendee-id', newRole: 'OPTIONAL_ATTENDEE' })
        .expect(404);

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

  // ============================================================
  // DELETE /series/:id/attendees
  // ============================================================

  describe('DELETE /series/:id/attendees', () => {
    it('[48] 管理员移除参会人 → 200 + deletedCount', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      const attendee = await addRequiredAttendee(meetings[0].id, employee.id);

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

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

    it('[49] 缺少 attendeeId → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({}) // 缺 attendeeId
        .expect(400);

      expect(resp.body.error).toMatch(/Missing attendee ID/i);
    });

    it('[50] attendeeId 不存在 → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ attendeeId: 'non-existent-attendee-id' })
        .expect(404);
    });

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

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/attendees`)
        .send({ attendeeId: 'any' })
        .expect(401);
    });
  });

  // ============================================================
  // PUT /series/:id/meetings/:meetingId
  // ============================================================

  describe('PUT /series/:id/meetings/:meetingId', () => {
    it('[52] 管理员更新单个 meeting → 200 + meeting', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      const s = suffix();

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

      expect(resp.body.meeting.title).toBe(`Updated Meeting ${s}`);
      expect(resp.body.message).toMatch(/updated successfully/i);
    });

    it('[53] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/meetings/${meetings[0].id}`)
        .send({ title: 'x' })
        .expect(401);
    });

    it('[54] 普通员工 token → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

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

    it('[55] meetingId 不属于该 series → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });
      // 创建另一个系列的 meeting
      const { meetings: otherMeetings } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/meetings/${otherMeetings[0].id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ title: 'x' })
        .expect(404);
    });
  });

  // ============================================================
  // DELETE /series/:id/meetings/:meetingId
  // ============================================================

  describe('DELETE /series/:id/meetings/:meetingId', () => {
    it('[56] 管理员取消单个 meeting → 200 + message', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

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

      expect(resp.body.message).toMatch(/cancelled/i);

      // DB 验证
      const updated = await (prisma as any).meeting.findUnique({ where: { id: meetings[0].id } });
      expect(updated.status).toBe('CANCELLED');
    });

    it('[57] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

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

    it('[58] 普通员工 token → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/meetings/${meetings[0].id}`)
        .set('Authorization', `Bearer ${empToken}`)
        .expect(403);
    });

    it('[59] meetingId 不属于该 series → 404', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });
      const { meetings: otherMeetings } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/meetings/${otherMeetings[0].id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });

  // ============================================================
  // PATCH /series/:id/enforce-checkin-mode
  // ============================================================

  describe('PATCH /series/:id/enforce-checkin-mode', () => {
    it('[60] 管理员开启 enforce（有 city）→ 200 + enforceCheckinMode=true', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: 'SZ' });

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

      expect(resp.body.enforceCheckinMode).toBe(true);
      expect(resp.body.seriesId).toBe(series.id);
    });

    it('[61] 关闭 enforce → 200 + enforceCheckinMode=false', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: 'SZ', enforceCheckinMode: true });

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

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

    it('[62] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: 'SZ' });

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

    it('[63] 普通员工 token → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id, city: 'SZ' });

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/series/${series.id}/enforce-checkin-mode`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ enforceCheckinMode: true })
        .expect(403);
    });

    it('[64] enforceCheckinMode 不是 boolean → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: 'SZ' });

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

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

    it('[65] 开启但无 city → 400 MEETING_ATTENDANCE_036', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id, city: null });

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

      expect(resp.body.error).toMatch(/签到方式校验/);
    });

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

      await request(app.getHttpServer())
        .patch('/api/v1/meeting-attendance/series/non-existent-id/enforce-checkin-mode')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ enforceCheckinMode: false })
        .expect(404);
    });
  });

  // ============================================================
  // GET /series/:id/attendee-preferences
  // ============================================================

  describe('GET /series/:id/attendee-preferences', () => {
    it('[67] 管理员查询偏好列表 → 200 + items + pagination', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

      expect(Array.isArray(resp.body.items)).toBe(true);
      expect(resp.body.pagination).toBeDefined();
      expect(resp.body.pagination.total).toBeGreaterThanOrEqual(0);
    });

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

      await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .expect(401);
    });

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

      await request(app.getHttpServer())
        .get(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${empToken}`)
        .expect(403);
    });

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

      await request(app.getHttpServer())
        .get('/api/v1/meeting-attendance/series/non-existent-id/attendee-preferences')
        .set('Authorization', `Bearer ${adminToken}`)
        .expect(404);
    });
  });

  // ============================================================
  // PUT /series/:id/attendee-preferences
  // ============================================================

  describe('PUT /series/:id/attendee-preferences', () => {
    it('[71] 管理员批量 upsert 偏好 → 200 + created/updated', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      await addRequiredAttendee(meetings[0].id, employee.id);

      const resp = await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          preferences: [{ userId: employee.id, defaultCheckinMode: 'ON_SITE' }],
        })
        .expect(200);

      expect(resp.body.seriesId).toBe(series.id);
      expect(typeof resp.body.created).toBe('number');
      expect(typeof resp.body.updated).toBe('number');
    });

    it('[72] preferences 不是数组 → 400', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ preferences: 'not-an-array' })
        .expect(400);

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

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

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .send({ preferences: [] })
        .expect(401);
    });

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

      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ preferences: [] })
        .expect(403);
    });

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

      await request(app.getHttpServer())
        .put('/api/v1/meeting-attendance/series/non-existent-id/attendee-preferences')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ preferences: [] })
        .expect(404);
    });

    it('[76] 未知 userId 被跳过（skippedUnknownUserIds）→ 200', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { series } = await createSeries({ creatorId: adminUser.id });

      const resp = await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          preferences: [{ userId: 'totally-unknown-user-id', defaultCheckinMode: 'ONLINE' }],
        })
        .expect(200);

      expect(resp.body.skippedUnknownUserIds).toContain('totally-unknown-user-id');
    });
  });

  // ============================================================
  // DELETE /series/:id/attendee-preferences/:userId
  // ============================================================

  describe('DELETE /series/:id/attendee-preferences/:userId', () => {
    it('[77] 删除存在的偏好 → 200 + removed=true', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series, meetings } = await createSeries({ creatorId: adminUser.id });
      await addRequiredAttendee(meetings[0].id, employee.id);

      // 先 upsert 一条偏好
      await request(app.getHttpServer())
        .put(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ preferences: [{ userId: employee.id, defaultCheckinMode: 'ONLINE' }] });

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

      expect(resp.body.removed).toBe(true);
      expect(resp.body.userId).toBe(employee.id);
    });

    it('[78] 删除不存在的偏好 → 200 + removed=false（幂等）', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

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

    it('[79] 无 token → 401', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

    it('[80] 普通员工 token → 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken, employee } = await setupEmployee();
      const { series } = await createSeries({ creatorId: adminUser.id });

      await request(app.getHttpServer())
        .delete(`/api/v1/meeting-attendance/series/${series.id}/attendee-preferences/${employee.id}`)
        .set('Authorization', `Bearer ${empToken}`)
        .expect(403);
    });
  });

  // ============================================================
  // 鉴权边界：MeetingManager 可以操作（requireAdminOrManager）
  // ============================================================

  describe('MeetingManager 角色跨端点鉴权验证', () => {
    it('[81] MeetingManager 创建系列 → 201', async () => {
      const { manager, mgrToken } = await setupManager();
      const s = suffix();

      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/series')
        .set('Authorization', `Bearer ${mgrToken}`)
        .send({
          title: `Manager Creates ${s}`,
          pattern: 'WEEKLY',
          startDate: new Date(Date.now() + 86400000).toISOString(),
          duration: 60,
          maxOccurrences: 1,
        })
        .expect(201);

      expect(resp.body.series).toBeDefined();
    });

    it('[82] MeetingManager 可以删除系列 → 200', async () => {
      const { adminUser } = await setupAdmin();
      const { manager, mgrToken } = await setupManager();
      const { series } = await createSeries({ creatorId: adminUser.id });

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

      expect(['hard_delete', 'soft_delete']).toContain(resp.body.type);
    });
  });
});
