import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import { OutlookSyncService } from '@/modules/meeting-attendance/services/outlook-sync.service';
import { createTestApp } from '../../helpers/app.helper';
import { cleanupDatabase } from '../../helpers/cleanup.helper';
import { createAdminUser, createTestUser } from '../../helpers/factories/user.factory';

describe('Meeting Attendance Required Attendees API Integration Tests', () => {
  let app: INestApplication;
  let prisma: PrismaService;
  let outlookSyncService: OutlookSyncService;

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

  afterEach(async () => {
    jest.restoreAllMocks();
    await cleanupDatabase(prisma);
  });

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

  async function createAdminToken() {
    const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    const adminUser = await createAdminUser({
      username: `meeting_admin_${suffix}`,
      email: `meeting_admin_${suffix}@example.com`,
      password: 'Admin@123',
      displayName: 'Meeting Admin',
    });

    const loginResponse = await request(app.getHttpServer())
      .post('/api/v1/auth/login')
      .send({
        username: adminUser.username,
        password: 'Admin@123',
      })
      .expect(200);

    return {
      adminUser,
      adminToken: loginResponse.body.data.accessToken as string,
    };
  }

  async function createLockedOccurrenceFixture() {
    const suffix = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
    const adminUser = await createAdminUser({
      username: `outlook_admin_${suffix}`,
      email: `outlook_admin_${suffix}@example.com`,
      password: 'Admin@123',
      displayName: 'Outlook Admin',
    });
    const keptAttendee = await createTestUser({
      username: `occurrence_keep_${suffix}`,
      email: `occurrence_keep_${suffix}@example.com`,
      displayName: 'Occurrence Keep',
      password: 'Test@123',
    });
    const removedAttendee = await createTestUser({
      username: `occurrence_removed_${suffix}`,
      email: `occurrence_removed_${suffix}@example.com`,
      displayName: 'Occurrence Removed',
      password: 'Test@123',
    });

    const mailbox = await prisma.outlookSyncMailbox.create({
      data: {
        mailboxEmail: `mailbox_${suffix}@example.com`,
        mailboxType: 'PERSONAL',
        isEnabled: true,
        isPrimaryDefault: true,
      },
    });

    const seriesStart = new Date('2026-04-02T01:30:00.000Z');
    const seriesEnd = new Date('2026-04-02T02:00:00.000Z');
    const occurrenceStart = new Date('2026-04-03T01:30:00.000Z');
    const occurrenceEnd = new Date('2026-04-03T02:00:00.000Z');

    const series = await prisma.meetingSeries.create({
      data: {
        title: `Outlook Series ${suffix}`,
        description: 'Locked occurrence regression fixture',
        pattern: 'WEEKLY',
        frequency: 1,
        startDate: seriesStart,
        endDate: new Date('2026-05-01T00:00:00.000Z'),
        timezone: 'UTC',
        location: 'Teams',
        type: 'HYBRID',
        creatorId: adminUser.id,
        isActive: true,
      },
    });

    const occurrenceMeeting = await prisma.meeting.create({
      data: {
        title: `Locked occurrence ${suffix}`,
        description: 'Should stay locally maintained',
        startTime: occurrenceStart,
        endTime: occurrenceEnd,
        timezone: 'UTC',
        location: 'Teams',
        type: 'HYBRID',
        status: 'SCHEDULED',
        creatorId: adminUser.id,
        seriesId: series.id,
      },
    });

    await prisma.meetingRequiredAttendee.create({
      data: {
        meetingId: occurrenceMeeting.id,
        userId: keptAttendee.id,
        role: 'REGULAR_ATTENDEE',
      },
    });
    await prisma.meetingAttendance.create({
      data: {
        meetingId: occurrenceMeeting.id,
        userId: keptAttendee.id,
        status: 'NOT_CHECKED_IN',
        checkinTime: null,
        isLate: false,
      },
    });

    const seriesMasterId = `series-master-${suffix}`;
    const occurrenceGraphEventId = `series-occurrence-${suffix}`;

    const seriesBinding = await prisma.outlookMeetingBinding.create({
      data: {
        meetingSeriesId: series.id,
        graphEventId: seriesMasterId,
        iCalUId: `ical-series-${suffix}`,
        graphSeriesMasterId: seriesMasterId,
        graphEventType: 'seriesMaster',
        manageStatus: 'MANAGED',
        ownerUserId: adminUser.id,
        ownerEmail: adminUser.email,
        primaryMailboxId: mailbox.id,
        syncFrom: new Date('2026-03-01T00:00:00.000Z'),
        syncMode: 'AUTO',
      },
    });

    const occurrenceBinding = await prisma.outlookMeetingBinding.create({
      data: {
        meetingId: occurrenceMeeting.id,
        meetingSeriesId: series.id,
        graphEventId: occurrenceGraphEventId,
        iCalUId: `ical-occurrence-${suffix}`,
        graphSeriesMasterId: seriesMasterId,
        graphEventType: 'occurrence',
        manageStatus: 'MANAGED',
        ownerUserId: adminUser.id,
        ownerEmail: adminUser.email,
        primaryMailboxId: mailbox.id,
        syncFrom: new Date('2026-03-01T00:00:00.000Z'),
        syncMode: 'LOCKED_BY_LOCAL_EDIT',
        localOverrideAt: new Date('2026-03-31T00:00:00.000Z'),
        localOverrideByUserId: adminUser.id,
        localOverrideByEmail: adminUser.email,
        localOverrideReason: 'MANUAL_ATTENDEE_DELETE',
        localOverrideFields: ['requiredAttendees'],
      },
    });

    const seriesMasterEvent = {
      id: seriesMasterId,
      iCalUId: `ical-series-${suffix}`,
      subject: `Outlook Series ${suffix}`,
      bodyPreview: 'Series master event',
      type: 'seriesMaster',
      start: { dateTime: '2026-04-02T01:30:00.0000000', timeZone: 'UTC' },
      end: { dateTime: '2026-04-02T02:00:00.0000000', timeZone: 'UTC' },
      location: { displayName: 'Teams' },
      recurrence: {
        pattern: {
          type: 'weekly',
          interval: 1,
        },
        range: {
          type: 'endDate',
          startDate: '2026-04-02',
          endDate: '2026-05-01',
          recurrenceTimeZone: 'UTC',
        },
      },
      organizer: {
        emailAddress: {
          address: adminUser.email,
          name: adminUser.displayName,
        },
      },
      attendees: [
        {
          type: 'required',
          status: { response: 'accepted' },
          emailAddress: {
            address: keptAttendee.email,
            name: keptAttendee.displayName,
          },
        },
        {
          type: 'required',
          status: { response: 'none' },
          emailAddress: {
            address: removedAttendee.email,
            name: removedAttendee.displayName,
          },
        },
      ],
      lastModifiedDateTime: '2026-03-31T00:00:00.000Z',
    };

    const occurrenceEvent = {
      id: occurrenceGraphEventId,
      iCalUId: `ical-occurrence-${suffix}`,
      subject: `Outlook occurrence ${suffix}`,
      bodyPreview: 'Occurrence event',
      type: 'occurrence',
      seriesMasterId,
      start: { dateTime: '2026-04-03T01:30:00.0000000', timeZone: 'UTC' },
      end: { dateTime: '2026-04-03T02:00:00.0000000', timeZone: 'UTC' },
      location: { displayName: 'Teams' },
      organizer: {
        emailAddress: {
          address: adminUser.email,
          name: adminUser.displayName,
        },
      },
      attendees: [
        {
          type: 'required',
          status: { response: 'accepted' },
          emailAddress: {
            address: keptAttendee.email,
            name: keptAttendee.displayName,
          },
        },
        {
          type: 'required',
          status: { response: 'none' },
          emailAddress: {
            address: removedAttendee.email,
            name: removedAttendee.displayName,
          },
        },
      ],
      lastModifiedDateTime: '2026-03-31T00:00:00.000Z',
    };

    return {
      adminUser,
      keptAttendee,
      removedAttendee,
      mailbox,
      series,
      occurrenceMeeting,
      seriesBinding,
      occurrenceBinding,
      seriesMasterEvent,
      occurrenceEvent,
    };
  }

  describe('DELETE /api/v1/meeting-attendance/meetings/:id/required-attendees/:userId', () => {
    it('[API-MA-RA-001] 删除未签到参会人时应同时移除名单和占位 attendance，避免详情页残留', async () => {
      const { adminToken, adminUser } = await createAdminToken();
      const attendee = await createTestUser({
        username: `meeting_attendee_${Date.now()}`,
        email: `meeting_attendee_${Date.now()}@example.com`,
        displayName: 'Meeting Attendee',
        password: 'Test@123',
      });

      const startTime = new Date(Date.now() + 60 * 60 * 1000).toISOString();
      const endTime = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString();

      const meetingResponse = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `删除参会人回归测试-${Date.now()}`,
          description: '验证删除未签到参会人时不会在详情页残留',
          startTime,
          endTime,
          location: 'Test Room',
          type: 'OFFLINE',
          timezone: 'America/Los_Angeles',
        })
        .expect(201);

      const meetingId = meetingResponse.body.id as string;

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meetingId}/required-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          attendees: [
            {
              userId: attendee.id,
              role: 'REGULAR_ATTENDEE',
            },
          ],
          markAsCustom: false,
        })
        .expect(201);

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

      expect(beforeDeleteMeeting.body.requiredAttendees).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            user: expect.objectContaining({ id: attendee.id }),
          }),
        ]),
      );
      expect(beforeDeleteMeeting.body.attendances).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            user: expect.objectContaining({ id: attendee.id }),
            status: 'NOT_CHECKED_IN',
          }),
        ]),
      );

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

      expect(deleteResponse.body).toMatchObject({
        message: 'Attendee deleted successfully',
        remainingCount: 0,
        syncMode: 'LOCKED_BY_LOCAL_EDIT',
      });

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

      expect(
        afterDeleteMeeting.body.requiredAttendees.some(
          (item: { user?: { id?: string } }) => item.user?.id === attendee.id,
        ),
      ).toBe(false);
      expect(
        afterDeleteMeeting.body.attendances.some(
          (item: { user?: { id?: string } }) => item.user?.id === attendee.id,
        ),
      ).toBe(false);

      const attendeeRecord = await prisma.meetingRequiredAttendee.findUnique({
        where: {
          meetingId_userId: {
            meetingId,
            userId: attendee.id,
          },
        },
      });
      const attendanceRecord = await prisma.meetingAttendance.findUnique({
        where: {
          userId_meetingId: {
            userId: attendee.id,
            meetingId,
          },
        },
      });

      expect(attendeeRecord).toBeNull();
      expect(attendanceRecord).toBeNull();

      const remainingMeeting = await prisma.meeting.findUnique({
        where: { id: meetingId },
        select: {
          creatorId: true,
        },
      });
      expect(remainingMeeting?.creatorId).toBe(adminUser.id);
    });

    it('[API-MA-RA-002] 删除已有真实出勤记录的参会人应返回 409，且名单与 attendance 保持不变', async () => {
      const { adminToken } = await createAdminToken();
      const attendee = await createTestUser({
        username: `meeting_checkedin_${Date.now()}`,
        email: `meeting_checkedin_${Date.now()}@example.com`,
        displayName: 'Checked In Attendee',
        password: 'Test@123',
      });

      const startTime = new Date(Date.now() + 60 * 60 * 1000).toISOString();
      const endTime = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString();

      const meetingResponse = await request(app.getHttpServer())
        .post('/api/v1/meeting-attendance/meetings')
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          title: `删除已签到参会人回归测试-${Date.now()}`,
          description: '验证删除已签到参会人时返回 409 且数据不变',
          startTime,
          endTime,
          location: 'Test Room',
          type: 'OFFLINE',
          timezone: 'America/Los_Angeles',
        })
        .expect(201);

      const meetingId = meetingResponse.body.id as string;

      await request(app.getHttpServer())
        .post(`/api/v1/meeting-attendance/meetings/${meetingId}/required-attendees`)
        .set('Authorization', `Bearer ${adminToken}`)
        .send({
          attendees: [
            {
              userId: attendee.id,
              role: 'REGULAR_ATTENDEE',
            },
          ],
          markAsCustom: false,
        })
        .expect(201);

      const checkinTime = new Date();
      await prisma.meetingAttendance.update({
        where: {
          userId_meetingId: {
            userId: attendee.id,
            meetingId,
          },
        },
        data: {
          status: 'ON_SITE',
          checkinTime,
          checkinType: 'MANUAL',
        },
      });

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

      expect(deleteResponse.body).toMatchObject({
        error: 'Attendee already has attendance record and cannot be removed',
      });

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

      expect(
        afterDeleteMeeting.body.requiredAttendees.some(
          (item: { user?: { id?: string } }) => item.user?.id === attendee.id,
        ),
      ).toBe(true);
      expect(
        afterDeleteMeeting.body.attendances.some(
          (item: { user?: { id?: string }; status?: string }) =>
            item.user?.id === attendee.id && item.status === 'ON_SITE',
        ),
      ).toBe(true);

      const attendeeRecord = await prisma.meetingRequiredAttendee.findUnique({
        where: {
          meetingId_userId: {
            meetingId,
            userId: attendee.id,
          },
        },
      });
      const attendanceRecord = await prisma.meetingAttendance.findUnique({
        where: {
          userId_meetingId: {
            userId: attendee.id,
            meetingId,
          },
        },
      });

      expect(attendeeRecord).not.toBeNull();
      expect(attendanceRecord).toMatchObject({
        status: 'ON_SITE',
      });
      expect(attendanceRecord?.checkinTime?.toISOString()).toBe(checkinTime.toISOString());
    });
  });

  describe('Outlook locked occurrence regression', () => {
    it('[API-MA-RA-003] applyUpdatedEvent 处理系列根更新时，不应把已锁定 occurrence 的 syncMode 改回 AUTO', async () => {
      const fixture = await createLockedOccurrenceFixture();

      const fetchEventByIdSpy = jest
        .spyOn(outlookSyncService as any, 'fetchEventById')
        .mockImplementation(async (_mailboxEmail: string, graphEventId: string) => {
          if (graphEventId === fixture.seriesMasterEvent.id) return fixture.seriesMasterEvent;
          if (graphEventId === fixture.occurrenceEvent.id) return fixture.occurrenceEvent;
          return null;
        });
      const fetchSeriesInstanceEventsSpy = jest
        .spyOn(outlookSyncService as any, 'fetchSeriesInstanceEvents')
        .mockResolvedValue([fixture.occurrenceEvent]);
      const createOrUpdateMeetingSpy = jest
        .spyOn(outlookSyncService as any, 'createOrUpdateMeetingFromEvent')
        .mockResolvedValue({ id: fixture.occurrenceMeeting.id });
      const syncAttendeesSpy = jest
        .spyOn(outlookSyncService as any, 'syncAttendees')
        .mockResolvedValue(undefined);

      await (outlookSyncService as any).applyUpdatedEvent(
        fixture.seriesMasterEvent,
        fixture.mailbox.id,
        fixture.mailbox.mailboxEmail,
        { alreadyHydrated: true },
      );

      expect(fetchEventByIdSpy).not.toHaveBeenCalledWith(
        fixture.mailbox.mailboxEmail,
        fixture.occurrenceEvent.id,
      );
      expect(fetchSeriesInstanceEventsSpy).toHaveBeenCalled();
      expect(createOrUpdateMeetingSpy).toHaveBeenCalled();
      expect(syncAttendeesSpy).not.toHaveBeenCalled();

      const binding = await prisma.outlookMeetingBinding.findUnique({
        where: { id: fixture.occurrenceBinding.id },
      });
      expect(binding).toMatchObject({
        syncMode: 'LOCKED_BY_LOCAL_EDIT',
        localOverrideReason: 'MANUAL_ATTENDEE_DELETE',
      });

      const requiredAttendees = await prisma.meetingRequiredAttendee.findMany({
        where: { meetingId: fixture.occurrenceMeeting.id },
      });
      expect(requiredAttendees).toHaveLength(1);
      expect(requiredAttendees[0].userId).toBe(fixture.keptAttendee.id);

      const skipLogs = await prisma.outlookSyncEventLog.findMany({
        where: {
          bindingId: fixture.occurrenceBinding.id,
          eventType: 'SYNC_SKIPPED_LOCAL_OVERRIDE',
        },
      });
      expect(skipLogs).toHaveLength(1);
      expect(skipLogs[0].payload).toMatchObject({
        reason: 'MANUAL_ATTENDEE_DELETE',
        changedFields: ['requiredAttendees'],
      });
    });

    it('[API-MA-RA-004] reconcileManagedBindings 不应恢复已本地锁定的 occurrence 参会人和提示状态', async () => {
      const fixture = await createLockedOccurrenceFixture();

      jest
        .spyOn(outlookSyncService as any, 'fetchEventById')
        .mockImplementation(async (_mailboxEmail: string, graphEventId: string) => {
          if (graphEventId === fixture.seriesMasterEvent.id) return fixture.seriesMasterEvent;
          if (graphEventId === fixture.occurrenceEvent.id) return fixture.occurrenceEvent;
          return null;
        });
      jest
        .spyOn(outlookSyncService as any, 'fetchSeriesInstanceEvents')
        .mockResolvedValue([fixture.occurrenceEvent]);
      jest
        .spyOn(outlookSyncService as any, 'createOrUpdateMeetingFromEvent')
        .mockResolvedValue({ id: fixture.occurrenceMeeting.id });
      const syncAttendeesSpy = jest
        .spyOn(outlookSyncService as any, 'syncAttendees')
        .mockResolvedValue(undefined);

      await (outlookSyncService as any).reconcileManagedBindings(
        fixture.mailbox.id,
        fixture.mailbox.mailboxEmail,
      );

      expect(syncAttendeesSpy).not.toHaveBeenCalled();

      const meetingDetail = await prisma.meeting.findUnique({
        where: { id: fixture.occurrenceMeeting.id },
        include: {
          requiredAttendees: true,
          attendances: true,
          outlookBindings: true,
        },
      });

      expect(meetingDetail?.requiredAttendees).toHaveLength(1);
      expect(meetingDetail?.requiredAttendees[0].userId).toBe(fixture.keptAttendee.id);
      expect(meetingDetail?.attendances).toHaveLength(1);
      expect(meetingDetail?.attendances[0].userId).toBe(fixture.keptAttendee.id);
      expect(meetingDetail?.outlookBindings[0]).toMatchObject({
        syncMode: 'LOCKED_BY_LOCAL_EDIT',
        localOverrideReason: 'MANUAL_ATTENDEE_DELETE',
      });

      const skipLogs = await prisma.outlookSyncEventLog.findMany({
        where: {
          bindingId: fixture.occurrenceBinding.id,
          eventType: 'SYNC_SKIPPED_LOCAL_OVERRIDE',
        },
        orderBy: { createdAt: 'asc' },
      });
      expect(skipLogs.length).toBeGreaterThanOrEqual(1);
      expect(skipLogs.at(-1)?.payload).toMatchObject({
        reason: 'MANUAL_ATTENDEE_DELETE',
        changedFields: ['requiredAttendees'],
      });
    });
  });
});
