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 · Checkin Controller L1 集成测试
 *
 * 覆盖 3 个 endpoint × HTTP 行为 + 鉴权 + 核心业务规则：
 *   POST  /meetings/:id/checkin             （员工签到，需 token）
 *   POST  /meetings/:id/guest-checkin       （嘉宾签到，@Public）
 *   PATCH /meetings/:id/attendance/:userId  （admin/manager 改 attendance）
 *
 * 与 v12-checkin-mode.api.test.ts 互补：v12 专测 enforce-checkin-mode 三层
 * fallback（MEETING_OVERRIDE / SERIES_PREFERENCE / CITY_DERIVED）；本文件
 * 专测 checkin 三个 endpoint 的"基础 HTTP 行为 + 鉴权 + 核心业务规则"。
 *
 * 关联工单 #341 · Batch 1-A
 */
describe('Meeting-attendance · Checkin API', () => {
  let app: INestApplication;
  let prisma: PrismaService;

  beforeAll(async () => {
    process.env.NODE_ENV = 'test';
    app = await createTestApp();
    prisma = app.get<PrismaService>(PrismaService);
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    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') {
    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: `Checkin 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: `Checkin 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;     // 默认 5 min 后开始（在签到窗内）
    durationMin?: number;        // 默认 60 min
    status?: 'SCHEDULED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
    city?: string | null;
    enforceCheckinMode?: boolean;
  }) {
    const startOffsetMin = opts.startOffsetMin ?? 5;
    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: `Checkin 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',
      },
    });
  }

  // ============================================================
  // POST /meetings/:id/checkin  (employee, auth required)
  // ============================================================

  describe('POST /meetings/:id/checkin', () => {
    it('[1] MANUAL 签到成功：普通员工 → 200 + 写入 MeetingAttendance', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 5 });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(200);

      expect(resp.body.message).toBe('Check-in successful');
      expect(resp.body.attendance.userId).toBe(employee.id);
      expect(resp.body.attendance.status).toBe('ONLINE'); // MANUAL 默认 ONLINE

      const att = await prisma.meetingAttendance.findFirst({
        where: { meetingId: meeting.id, userId: employee.id },
      });
      expect(att).toBeTruthy();
      expect(att!.checkinType).toBe('MANUAL');
    });

    it('[2] QR_CODE 签到成功：员工扫线下码 → 200 + status=ON_SITE', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({
          checkinType: 'QR_CODE',
          qrData: JSON.stringify({ meetingId: meeting.id, type: 'on_site' }),
          qrType: 'on_site',
        })
        .expect(200);

      expect(resp.body.attendance.status).toBe('ON_SITE');
    });

    it('[3] 迟到签到：开会 9 分钟后才签到 → status=LATE + isLate=true', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      // startTime = now - 9min（已迟到 isLate 阈值 8min）
      const meeting = await createMeeting({
        creatorId: adminUser.id,
        startOffsetMin: -9,
        status: 'IN_PROGRESS',
      });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(200);

      expect(resp.body.attendance.status).toBe('LATE');
      expect(resp.body.isLate).toBe(true);
    });

    it('[4] 会议不存在 → 404', async () => {
      const { empToken } = await setupEmployee();
      await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/checkin')
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(404);
    });

    it('[5] 会议 COMPLETED → 400 has not started or has ended', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      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}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(400);

      expect(resp.body.error).toMatch(/has not started or has ended/i);
    });

    it('[6] 会前超过 15 分钟（窗口未到）→ 400 提示时间窗', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      // 60 分钟之后才开会，远超 15 分钟签到窗
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 60 });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(400);

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

    it('[7] 重复签到：已 CHECKED_IN 再签 → 400 already checked in', async () => {
      const { adminUser } = await setupAdmin();
      const { employee, empToken } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

      // 第一次成功
      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(200);

      // 第二次被拒
      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ checkinType: 'MANUAL' })
        .expect(400);

      expect(resp.body.error).toMatch(/already checked in/i);
    });

    it('[8] deviceId 已被其他用户用过 → 400 device already used', async () => {
      const { adminUser } = await setupAdmin();
      const { employee: emp1, empToken: tok1 } = await setupEmployee();
      const { employee: emp2, empToken: tok2 } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, emp1.id);
      await addRequiredAttendee(meeting.id, emp2.id);

      // emp1 拿 deviceId=A 成功
      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${tok1}`)
        .send({ checkinType: 'MANUAL', deviceId: 'device-shared-001' })
        .expect(200);

      // emp2 同 deviceId 被拒
      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/checkin`)
        .set('Authorization', `Bearer ${tok2}`)
        .send({ checkinType: 'MANUAL', deviceId: 'device-shared-001' })
        .expect(400);

      expect(resp.body.error).toMatch(/device has already been used/i);
    });
  });

  // ============================================================
  // POST /meetings/:id/guest-checkin  (@Public, no token)
  // ============================================================

  describe('POST /meetings/:id/guest-checkin', () => {
    it('[9] 嘉宾匹配 required-attendee (email) → 200', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/guest-checkin`)
        .send({
          name: employee.displayName,
          email: employee.email,
          qrType: 'on_site',
        })
        .expect(200);

      expect(resp.body.message).toMatch(/Check-in successful/i);
      expect(resp.body.attendance.userId).toBe(employee.id);
    });

    it('[10] @Public：无 Authorization 头也成功（不返回 401）', async () => {
      const { adminUser } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/guest-checkin`)
        // 故意不带 Authorization
        .send({
          name: employee.displayName,
          email: employee.email,
          qrType: 'on_site',
        });

      expect(resp.status).not.toBe(401);
      expect(resp.status).toBe(200);
    });

    it('[11] 会议不存在 → 404', async () => {
      const { employee } = await setupEmployee();
      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/non-existent-meeting-id/guest-checkin')
        .send({
          name: employee.displayName,
          email: employee.email,
          qrType: 'on_site',
        })
        .expect(404);

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

    it('[12] payload.name 空白 → 400 Name cannot be empty', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/guest-checkin`)
        .send({ name: '   ', qrType: 'on_site' })
        .expect(400);

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

    it('[13] 嘉宾不在 required-attendee 名单 → 403', async () => {
      const { adminUser } = await setupAdmin();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });

      const resp = await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meeting.id}/guest-checkin`)
        .send({
          name: 'Unknown Guest',
          email: 'unknown@nowhere.example',
          qrType: 'on_site',
        })
        .expect(403);

      expect(resp.body.error).toMatch(/not on the attendee list/i);
    });
  });

  // ============================================================
  // PATCH /meetings/:id/attendance/:userId  (admin/manager only)
  // ============================================================

  describe('PATCH /meetings/:id/attendance/:userId', () => {
    it('[14] admin 改 attendance status → 200 + DB 同步', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendance/${employee.id}`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({ status: 'ON_SITE', notes: 'admin override note' })
        .expect(200);

      const att = await prisma.meetingAttendance.findFirst({
        where: { meetingId: meeting.id, userId: employee.id },
      });
      expect(att).toBeTruthy();
      expect(att!.status).toBe('ON_SITE');
      expect(att!.notes).toBe('admin override note');
    });

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

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendance/${employee.id}`)
        // 故意不带 Authorization
        .send({ status: 'ON_SITE' })
        .expect(401);
    });

    it('[16] 普通员工 token（非 admin/manager）→ 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const { employee: target } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });

      await request(app.getHttpServer())
        .patch(`/api/v1/meeting-attendance/meetings/${meeting.id}/attendance/${target.id}`)
        .set('Authorization', `Bearer ${empToken}`)
        .send({ status: 'ON_SITE' })
        .expect(403);
    });

    it('[17] 非法 status 值 → 400 Invalid attendance status', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id, startOffsetMin: 0 });
      await addRequiredAttendee(meeting.id, employee.id);

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

      expect(resp.body.error).toMatch(/invalid attendance status/i);
    });
  });
});
