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 { createAdminUser, createTestUser } from '../../helpers/factories/user.factory';
import { assignRoleToUser } from '../../helpers/factories/role.factory';

/**
 * Meeting-attendance · PTO Controller L1 集成测试
 *
 * 覆盖 1 个 endpoint × HTTP 行为 + 鉴权 + 核心业务规则：
 *   POST /meeting-attendance/meetings/:meetingId/apply-pto
 *
 * 仅会议管理员（Administrator / MeetingManager）可调用；
 * 扫描必参会人 ADP PTO 时段与会议时段重叠，写 MeetingAttendance.status = PTO；
 * 人工已签（ON_SITE / ONLINE）不覆盖；已 PTO 跳过。
 *
 * 关联工单 #341 · Batch 1-A
 */
describe('Meeting-attendance · PTO 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();
    // 先清理 meeting attendance 数据（cleanupDatabase 不清 platform_meeting_attendance schema）
    await prisma.meetingAttendanceAuditLog.deleteMany({}).catch(() => undefined);
    await prisma.meetingAttendance.deleteMany({}).catch(() => undefined);
    await prisma.meetingRequiredAttendee.deleteMany({}).catch(() => undefined);
    await prisma.meeting.deleteMany({}).catch(() => undefined);
    // 清理 AdpPtoSchedule（platform_automation schema）
    await (prisma as any).adpPtoSchedule.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' | 'MeetingManager' | 'Employee') {
    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_pto_adm_${s}`,
      email: `ci_pto_adm_${s}@example.com`,
      password: 'Admin@123',
      displayName: `PTO 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_pto_emp_${s}`,
      email: `ci_pto_emp_${s}@example.com`,
      password: 'Emp@123',
      displayName: `PTO 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;
    startTime?: Date;
    endTime?: Date;
  }) {
    const now = new Date();
    const startTime = opts.startTime ?? new Date(now.getTime() + 60 * 60 * 1000);
    const endTime = opts.endTime ?? new Date(startTime.getTime() + 60 * 60 * 1000);
    return (prisma as any).meeting.create({
      data: {
        title: `PTO Test Meeting ${suffix()}`,
        startTime,
        endTime,
        timezone: 'UTC',
        location: 'HQ',
        type: 'HYBRID',
        status: 'SCHEDULED',
        creatorId: opts.creatorId,
      },
    });
  }

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

  async function createAdpPtoSchedule(opts: {
    userId: string;
    startTime: Date;
    endTime: Date;
  }) {
    const s = suffix();
    return (prisma as any).adpPtoSchedule.create({
      data: {
        userId: opts.userId,
        startTime: opts.startTime,
        endTime: opts.endTime,
        // AdpPtoSchedule 必填字段（schema: platform_automation.adp_pto_schedules）
        adpAoid: `t_aoid_${s}`,
        leaveDate: opts.startTime,        // 取 PTO 开始日期作为 leaveDate
        adpEntryId: `t_entry_${s}`,       // 幂等锚点，每条唯一
      },
    });
  }

  // ============================================================
  // POST /meetings/:meetingId/apply-pto
  // ============================================================

  describe('POST /meetings/:meetingId/apply-pto', () => {
    it('[1] happy path：管理员调用，PTO 时段与会议重叠 → 200 + marked=1 + DB写入PTO', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();

      // 会议：明天 10:00-11:00
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const meetingStart = new Date(tomorrow.setHours(10, 0, 0, 0));
      const meetingEnd = new Date(meetingStart.getTime() + 60 * 60 * 1000);

      const meeting = await createMeeting({
        creatorId: adminUser.id,
        startTime: meetingStart,
        endTime: meetingEnd,
      });
      await addRequiredAttendee(meeting.id, employee.id);

      // ADP PTO: 09:00-11:30（与会议重叠）
      await createAdpPtoSchedule({
        userId: employee.id,
        startTime: new Date(meetingStart.getTime() - 60 * 60 * 1000),
        endTime: new Date(meetingEnd.getTime() + 30 * 60 * 1000),
      });

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

      expect(resp.body.meetingId).toBe(meeting.id);
      expect(resp.body.marked).toBe(1);
      expect(resp.body.details).toHaveLength(1);
      expect(resp.body.details[0].userId).toBe(employee.id);

      const att = await prisma.meetingAttendance.findFirst({
        where: { meetingId: meeting.id, userId: employee.id },
      });
      expect(att?.status).toBe('PTO');
    });

    it('[2] 鉴权：无 Authorization 头 → 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}/apply-pto`)
        .expect(401);
    });

    it('[3] 权限：普通员工 token（非 admin/MeetingManager）→ 403', async () => {
      const { adminUser } = await setupAdmin();
      const { empToken } = await setupEmployee();
      const meeting = await createMeeting({ creatorId: adminUser.id });

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

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

    it('[4] 会议不存在 → 404', async () => {
      const { adminToken } = await setupAdmin();

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

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

    it('[5] 业务规则：用户已签到 ON_SITE，apply-pto 不覆盖 → marked=0', async () => {
      const { adminUser, adminToken } = await setupAdmin();
      const { employee } = await setupEmployee();

      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const meetingStart = new Date(tomorrow.setHours(14, 0, 0, 0));
      const meetingEnd = new Date(meetingStart.getTime() + 60 * 60 * 1000);

      const meeting = await createMeeting({
        creatorId: adminUser.id,
        startTime: meetingStart,
        endTime: meetingEnd,
      });
      await addRequiredAttendee(meeting.id, employee.id);

      // 预先写入 ON_SITE 签到记录
      await prisma.meetingAttendance.create({
        data: {
          userId: employee.id,
          meetingId: meeting.id,
          status: 'ON_SITE',
        } as any,
      });

      // PTO 时段重叠
      await createAdpPtoSchedule({
        userId: employee.id,
        startTime: new Date(meetingStart.getTime() - 30 * 60 * 1000),
        endTime: meetingEnd,
      });

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

      expect(resp.body.marked).toBe(0);

      // 签到状态不变
      const att = await prisma.meetingAttendance.findFirst({
        where: { meetingId: meeting.id, userId: employee.id },
      });
      expect(att?.status).toBe('ON_SITE');
    });
  });
});
