/**
 * ADP PTO 同步 - L1 集成测试
 *
 * 覆盖：
 *  L1a 契约: linker / pto-sync / apply-pto / admin pto-schedules 路径与权限
 *  L1b 业务规则:
 *    1. linker 按 email local-part 匹配，@ff.com 与 @faradayfuture.com 视为同一身份
 *    2. linker unmatched 计入 result + logs，不写入 adpAoid
 *    3. PTO sync upsert + 删除"窗口内未见"记录（取消/移动场景）
 *    4. applyPtoMarking 对时段重叠的参会人写 PTO + 审计
 *    5. 已签到（ON_SITE/ONLINE）的参会人不会被覆盖为 PTO
 *    6. 普通用户调用 admin 接口 → 403
 *    7. admin pto-schedules 时间范围超限 → 400
 *    8. admin pto-schedules 默认不返回 adpAoid / adpEntryId（隐私收口）
 */
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 { AdpLinkerService } from '@/modules/organization/adp/sync/adp-linker.service';
import { AdpPtoSyncService } from '@/modules/organization/adp/sync/adp-pto-sync.service';
import { AdpApiService } from '@/modules/organization/adp/sdk/adp-api.service';
import { MeetingPtoMarkingService } from '@/modules/meeting-attendance/services/meeting-pto-marking.service';

describe('ADP PTO 同步 集成测试', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let linker: AdpLinkerService;
  let ptoSync: AdpPtoSyncService;
  let adpApi: AdpApiService;
  let ptoMarking: MeetingPtoMarkingService;

  beforeAll(async () => {
    process.env.NODE_ENV = 'test';
    app = await createTestApp();
    prisma = app.get<PrismaService>(PrismaService);
    linker = app.get<AdpLinkerService>(AdpLinkerService);
    ptoSync = app.get<AdpPtoSyncService>(AdpPtoSyncService);
    adpApi = app.get<AdpApiService>(AdpApiService);
    ptoMarking = app.get<MeetingPtoMarkingService>(MeetingPtoMarkingService);
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    // 先清 ADP PTO 时段（cleanup helper 暂未感知该表，且 FK 在 cleanup 期间被 replica 禁用，
    // 不能依赖 cascade）
    await prisma.adpPtoSchedule.deleteMany({}).catch((e: any) => {
      // eslint-disable-next-line no-console
      console.warn(`adp_pto_schedules cleanup failed: ${e?.message ?? e}`);
    });
    await cleanupDatabase(prisma);
  });

  beforeEach(async () => {
    // 起步先把上次遗留的 adp_pto_schedules 清空，避免引用已删 user 触发 inconsistent query
    await prisma.adpPtoSchedule.deleteMany({}).catch(() => undefined);
  });

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

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

  async function loginAsAdmin() {
    const sfx = suffix();
    const adminUser = await createAdminUser({
      username: `adp_admin_${sfx}`,
      email: `adp_admin_${sfx}@example.com`,
      password: 'Admin@123',
      displayName: 'ADP Admin',
    });
    const resp = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username: adminUser.username, password: 'Admin@123' })
      .expect(200);
    // 给 admin user 加 MeetingManager 角色（复用既有 isMeetingAdminRole 判定）
    const meetingManagerRole = await prisma.role.upsert({
      where: { code: 'MeetingManager' },
      update: {},
      create: {
        code: 'MeetingManager',
        name: 'MeetingManager',
        description: 'Meeting attendance administrator',
      },
    });
    await prisma.userRole.create({
      data: { userId: adminUser.id, roleId: meetingManagerRole.id },
    });
    return { user: adminUser, token: resp.body.data.accessToken as string };
  }

  async function loginAsRegularUser() {
    const sfx = suffix();
    const u = await createTestUser({
      username: `adp_user_${sfx}`,
      email: `adp_user_${sfx}@example.com`,
      password: 'User@123',
      displayName: 'Regular User',
    });
    const resp = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({ username: u.username, password: 'User@123' })
      .expect(200);
    return { user: u, token: resp.body.data.accessToken as string };
  }

  // --------------- L1b: Linker 双域名匹配 ---------------

  describe('AdpLinkerService.run', () => {
    it('应当能用 @ff.com 匹配 ADP 中 @faradayfuture.com 的员工', async () => {
      const sfx = suffix();
      const userA = await createTestUser({
        username: `link_a_${sfx}`,
        email: `alice.${sfx}@ff.com`,
        displayName: 'Alice (ff.com)',
      });
      const userB = await createTestUser({
        username: `link_b_${sfx}`,
        email: `bob.${sfx}@faradayfuture.com`,
        displayName: 'Bob (faradayfuture.com)',
      });

      jest.spyOn(adpApi, 'fetchAllActiveWorkers').mockResolvedValue([
        {
          associateOID: 'AOID_ALICE',
          workerID: { idValue: 'W1' },
          workerStatus: { statusCode: { codeValue: 'Active' } },
          businessCommunication: {
            emails: [{ itemID: 'Business', emailUri: `alice.${sfx}@faradayfuture.com` }],
          },
        },
        {
          associateOID: 'AOID_BOB',
          workerID: { idValue: 'W2' },
          workerStatus: { statusCode: { codeValue: 'Active' } },
          businessCommunication: {
            emails: [{ itemID: 'Business', emailUri: `bob.${sfx}@ff.com` }],
          },
        },
      ]);

      const result = await linker.run();
      expect(result.success).toBe(true);
      expect(result.recordsUpserted).toBe(2);

      const aliceFresh = await prisma.user.findUnique({ where: { id: userA.id } });
      const bobFresh = await prisma.user.findUnique({ where: { id: userB.id } });
      expect(aliceFresh?.adpAoid).toBe('AOID_ALICE');
      expect(bobFresh?.adpAoid).toBe('AOID_BOB');
      expect(aliceFresh?.adpLinkedAt).toBeTruthy();
    });

    it('ADP 中有但 FFAI 没对应 User 的员工算 unmatched（含其 aoid+email 进 logs）', async () => {
      const sfx = suffix();
      // FFAI 一侧没有对应的 nobody.* 用户
      const ghostEmail = `ghost.${sfx}@ff.com`;
      const ghostAoid = `AOID_GHOST_${sfx}`;
      jest.spyOn(adpApi, 'fetchAllActiveWorkers').mockResolvedValue([
        {
          associateOID: ghostAoid,
          workerStatus: { statusCode: { codeValue: 'Active' } },
          businessCommunication: { emails: [{ itemID: 'Business', emailUri: ghostEmail }] },
        } as any,
      ]);

      const result = await linker.run();
      expect(result.success).toBe(true);
      expect(result.recordsUpserted).toBe(0);
      expect(result.recordsUnmatched).toBeGreaterThanOrEqual(1);
      // ADP 一侧统计：logs 应含 ADP 员工的 aoid 和 email
      expect(result.logs).toContain(ghostAoid);
      expect(result.logs).toContain(ghostEmail);
    });

    it('FFAI 多出的用户不算 unmatched（FFAI 比 ADP 多是正常情况）', async () => {
      const sfx = suffix();
      const u = await createTestUser({
        username: `extra_${sfx}`,
        email: `extra.${sfx}@ff.com`,
      });

      // ADP 完全空 → 没有"ADP 中有 FFAI 找不到"的情况
      jest.spyOn(adpApi, 'fetchAllActiveWorkers').mockResolvedValue([]);

      const result = await linker.run();
      expect(result.success).toBe(true);
      expect(result.recordsUpserted).toBe(0);
      expect(result.recordsUnmatched).toBe(0);
      expect(result.logs).not.toContain(u.email);

      const fresh = await prisma.user.findUnique({ where: { id: u.id } });
      expect(fresh?.adpAoid).toBeNull();
    });

    it('非 @ff.com / @faradayfuture.com 域名的 email 不参与匹配', async () => {
      const sfx = suffix();
      const u = await createTestUser({
        username: `extdomain_${sfx}`,
        email: `external.${sfx}@example.com`,
      });

      jest.spyOn(adpApi, 'fetchAllActiveWorkers').mockResolvedValue([
        {
          associateOID: 'AOID_X',
          workerStatus: { statusCode: { codeValue: 'Active' } },
          businessCommunication: {
            emails: [{ itemID: 'Business', emailUri: `external.${sfx}@example.com` }],
          },
        } as any,
      ]);

      const result = await linker.run();
      // external.com 不在 FF_DOMAINS，linker 直接跳过该 user
      const fresh = await prisma.user.findUnique({ where: { id: u.id } });
      expect(fresh?.adpAoid).toBeNull();
      // result 里他被算到 unmatched 还是不在 target？取决于 normalize 是否返 null。
      // 实现里 normalizeLocalPart 返 null → continue，算 unmatched=0（user 直接跳过）
      // 仅做不爆错的存在性 assertion
      expect(result.success).toBe(true);
    });
  });

  // --------------- L1b: PTO Sync upsert + delete-not-seen ---------------

  describe('AdpPtoSyncService.run', () => {
    it('对已 link 的 User upsert 时段；窗口内消失的记录硬删', async () => {
      const sfx = suffix();
      const u = await createTestUser({
        username: `pto_${sfx}`,
        email: `pto.${sfx}@ff.com`,
      });
      await prisma.user.update({
        where: { id: u.id },
        data: { adpAoid: 'AOID_PTO_TEST', adpLinkedAt: new Date() },
      });

      // 第一次 sync：返回 entry-1
      const today = new Date();
      today.setUTCHours(0, 0, 0, 0);
      const future = new Date(today);
      future.setUTCDate(future.getUTCDate() + 5);

      const mockReq1 = {
        timeOffRequestID: 'REQ1',
        associateOID: 'AOID_PTO_TEST',
        timeOffEntries: [
          {
            dateTimePeriod: {
              startDateTime: new Date(future.getTime() + 9 * 3600 * 1000).toISOString(),
              endDateTime: new Date(future.getTime() + 13 * 3600 * 1000).toISOString(),
            },
          },
        ],
      };

      jest
        .spyOn(adpApi, 'fetchTimeOffRequests')
        .mockResolvedValueOnce([mockReq1 as any]);

      const r1 = await ptoSync.run();
      expect(r1.success).toBe(true);
      expect(r1.recordsUpserted).toBe(1);

      const after1 = await prisma.adpPtoSchedule.findMany({ where: { userId: u.id } });
      expect(after1).toHaveLength(1);
      expect(after1[0].adpEntryId).toBe('REQ1_0');

      // 第二次 sync：ADP 端取消，返回空 → 本地应硬删
      jest.spyOn(adpApi, 'fetchTimeOffRequests').mockResolvedValueOnce([]);
      const r2 = await ptoSync.run();
      expect(r2.success).toBe(true);
      expect(r2.recordsDeleted).toBe(1);

      const after2 = await prisma.adpPtoSchedule.findMany({ where: { userId: u.id } });
      expect(after2).toHaveLength(0);
    });

    it('未 link aoid 的 User 不参与 PTO 同步', async () => {
      const sfx = suffix();
      await createTestUser({
        username: `nolink_${sfx}`,
        email: `nolink.${sfx}@ff.com`,
      });
      const spy = jest.spyOn(adpApi, 'fetchTimeOffRequests').mockResolvedValue([]);
      const r = await ptoSync.run();
      expect(r.success).toBe(true);
      // 只对已 link 的 user 调 fetchTimeOffRequests；新建的 user 没 aoid，不应被调到
      expect(spy).not.toHaveBeenCalled();
    });

    it('窗口超过 42 天应失败', async () => {
      const r = await ptoSync.run(-30, 30);
      expect(r.success).toBe(false);
      expect(r.errors[0]).toContain('窗口');
    });
  });

  // --------------- L1b: applyForMeeting ---------------

  describe('MeetingPtoMarkingService.applyForMeeting', () => {
    it('时段重叠的参会人会被标记为 PTO', async () => {
      const sfx = suffix();
      const adminCreator = await createAdminUser({
        username: `mtg_creator_${sfx}`,
        email: `creator.${sfx}@ff.com`,
        password: 'Admin@123',
      });

      const user = await createTestUser({
        username: `att_${sfx}`,
        email: `att.${sfx}@ff.com`,
      });
      await prisma.user.update({
        where: { id: user.id },
        data: { adpAoid: 'AOID_ATT_TEST' },
      });

      const start = new Date('2026-12-15T10:00:00Z');
      const end = new Date('2026-12-15T11:00:00Z');

      const meeting = await prisma.meeting.create({
        data: {
          title: `mtg_${sfx}`,
          startTime: start,
          endTime: end,
          timezone: 'America/Los_Angeles',
          type: 'OFFLINE',
          status: 'SCHEDULED',
          creator: { connect: { id: adminCreator.id } },
        },
      });
      await prisma.meetingRequiredAttendee.create({
        data: {
          meeting: { connect: { id: meeting.id } },
          user: { connect: { id: user.id } },
          role: 'REGULAR_ATTENDEE',
        },
      });
      // 准备一条与会议重叠的 PTO（早于会议开始结束于会议中间）
      await prisma.adpPtoSchedule.create({
        data: {
          userId: user.id,
          adpAoid: 'AOID_ATT_TEST',
          leaveDate: new Date('2026-12-15'),
          startTime: new Date('2026-12-15T09:00:00Z'),
          endTime: new Date('2026-12-15T13:00:00Z'),
          adpEntryId: 'TEST_ENTRY_1',
        },
      });

      const result = await ptoMarking.applyForMeeting(meeting.id);
      expect(result.marked).toBe(1);
      expect(result.details[0].userId).toBe(user.id);

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

    it('已签到（ON_SITE）的参会人不被覆盖', async () => {
      const sfx = suffix();
      const adminCreator = await createAdminUser({
        username: `mtg_no_overwrite_${sfx}`,
        email: `noov.${sfx}@ff.com`,
        password: 'Admin@123',
      });
      const user = await createTestUser({
        username: `att2_${sfx}`,
        email: `att2.${sfx}@ff.com`,
      });
      await prisma.user.update({ where: { id: user.id }, data: { adpAoid: 'AOID_NOV' } });

      const start = new Date('2026-12-16T10:00:00Z');
      const end = new Date('2026-12-16T11:00:00Z');
      const meeting = await prisma.meeting.create({
        data: {
          title: `mtg2_${sfx}`,
          startTime: start,
          endTime: end,
          timezone: 'America/Los_Angeles',
          type: 'OFFLINE',
          status: 'SCHEDULED',
          creator: { connect: { id: adminCreator.id } },
        },
      });
      await prisma.meetingRequiredAttendee.create({
        data: {
          meeting: { connect: { id: meeting.id } },
          user: { connect: { id: user.id } },
          role: 'REGULAR_ATTENDEE',
        },
      });
      // 已签到 ON_SITE
      await prisma.meetingAttendance.create({
        data: { userId: user.id, meetingId: meeting.id, status: 'ON_SITE' },
      });
      await prisma.adpPtoSchedule.create({
        data: {
          userId: user.id,
          adpAoid: 'AOID_NOV',
          leaveDate: new Date('2026-12-16'),
          startTime: new Date('2026-12-16T09:00:00Z'),
          endTime: new Date('2026-12-16T13:00:00Z'),
          adpEntryId: 'TEST_ENTRY_2',
        },
      });

      const r = await ptoMarking.applyForMeeting(meeting.id);
      expect(r.marked).toBe(0);

      const att = await prisma.meetingAttendance.findUnique({
        where: { userId_meetingId: { userId: user.id, meetingId: meeting.id } },
      });
      expect(att?.status).toBe('ON_SITE'); // 未覆盖
    });
  });

  // --------------- L1a: 控制器契约 + 权限 ---------------

  describe('Admin controllers', () => {
    it('普通用户调用 ADP sync status → 403', async () => {
      const { token } = await loginAsRegularUser();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/adp-sync/status')
        .set('Authorization', `Bearer ${token}`);
      expect(resp.status).toBe(403);
    });

    it('admin 调用 status → 200 + 含 tasks 字段', async () => {
      const { token } = await loginAsAdmin();
      const resp = await request(app.getHttpServer())
        .get('/api/v1/adp-sync/status')
        .set('Authorization', `Bearer ${token}`);
      expect(resp.status).toBe(200);
      // 全局 TransformInterceptor 包成 { success, data: {...} }
      expect(resp.body.data).toHaveProperty('tasks');
      expect(Array.isArray(resp.body.data.tasks)).toBe(true);
    });

    it('admin pto-schedules 默认返回不含 adpAoid/adpEntryId', async () => {
      const { token, user } = await loginAsAdmin();
      // 给 admin 自己加 aoid + 创建一条记录便于检索
      await prisma.user.update({
        where: { id: user.id },
        data: { adpAoid: 'AOID_ADMIN_VIEW' },
      });
      const today = new Date();
      today.setUTCHours(0, 0, 0, 0);
      await prisma.adpPtoSchedule.create({
        data: {
          userId: user.id,
          adpAoid: 'AOID_ADMIN_VIEW',
          leaveDate: today,
          startTime: new Date(today.getTime() + 9 * 3600 * 1000),
          endTime: new Date(today.getTime() + 13 * 3600 * 1000),
          adpEntryId: 'ADMIN_VIEW_E1',
        },
      });

      const resp = await request(app.getHttpServer())
        .get('/api/v1/adp-sync/admin/pto-schedules')
        .set('Authorization', `Bearer ${token}`);
      expect(resp.status).toBe(200);
      expect(resp.body.data.items.length).toBeGreaterThanOrEqual(1);
      const item = resp.body.data.items[0];
      expect(item).not.toHaveProperty('adpAoid');
      expect(item).not.toHaveProperty('adpEntryId');
      expect(item).toHaveProperty('user');
      expect(item).toHaveProperty('leaveDate');
    });

    it('admin pto-schedules 时间范围超限 → 400', async () => {
      const { token } = await loginAsAdmin();
      const today = new Date();
      const tooEarly = new Date(today);
      tooEarly.setUTCDate(tooEarly.getUTCDate() - 60); // -60d 超过 -30d 限制
      const resp = await request(app.getHttpServer())
        .get('/api/v1/adp-sync/admin/pto-schedules')
        .query({ startDate: tooEarly.toISOString(), endDate: today.toISOString() })
        .set('Authorization', `Bearer ${token}`);
      expect(resp.status).toBe(400);
      // BadRequestException 抛 {error, message}，被全局 exception filter 包成
      // { success:false, error: { code: 'INVALID_DATE_RANGE', ... } }
      expect(resp.body.error.code).toBe('INVALID_DATE_RANGE');
    });

    it('普通用户调用 apply-pto → 拒绝（401/403）', async () => {
      const { token } = await loginAsRegularUser();
      // 假定一个不存在的 meeting id；权限校验在 service 之前
      const resp = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings/some-fake-id/apply-pto')
        .set('Authorization', `Bearer ${token}`);
      // JwtAuthGuard 可能在角色判定前就因 user 加载策略返回 401；
      // 角色判定层返回 403。两者都意味着"非管理员被拒绝"。
      expect([401, 403]).toContain(resp.status);
    });
  });
});
