import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { Subject } from 'rxjs';
import { AttendanceController } from './attendance.controller';
import { AttendanceService } from './attendance.service';
import { AttendanceImportService } from './attendance-import.service';
import { OperationLogsService } from '../operation-logs/operation-logs.service';

describe('AttendanceController — DingTalk import scope', () => {
  const attendanceService = {
    getTeacherClassDingUserIds: jest.fn(),
    getImportableClasses: jest.fn(),
  };
  const importService = {
    importFromDingTalk: jest.fn(),
    progress$: undefined as unknown,
  };
  const logService = {
    log: jest.fn(),
  };
  const authzService = {
    can: jest.fn().mockReturnValue(false),
  };

  let controller: AttendanceController;

  beforeEach(() => {
    jest.clearAllMocks();
    controller = new AttendanceController(
      attendanceService as unknown as AttendanceService,
      importService as unknown as AttendanceImportService,
      logService as unknown as OperationLogsService,
      authzService as never,
    );
    importService.importFromDingTalk.mockResolvedValue({
      success: true,
      imported: 0,
      skipped: 0,
      matched: 0,
      errors: [],
      duration: 1,
    });
  });

  it('defaults teacher DingTalk import to today when no date range is provided', async () => {
    jest.useFakeTimers().setSystemTime(new Date('2026-07-10T08:00:00.000Z'));
    attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-today']);

    await controller.importFromDingTalk({ classId: 8 }, {
      user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
    } as never);

    expect(importService.importFromDingTalk).toHaveBeenCalledWith({
      startDate: '2026-07-10',
      endDate: '2026-07-10',
      userIds: ['ding-today'],
      autoMatch: true,
      userId: 21,
    });
    jest.useRealTimers();
  });

  it('uses only the selected class students mapped to DingTalk for a teacher import', async () => {
    attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-1', 'ding-2']);

    await controller.importFromDingTalk(
      { start: '2026-07-01', end: '2026-07-02', classId: 8 },
      { user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
    );

    expect(importService.importFromDingTalk).toHaveBeenCalledWith({
      startDate: '2026-07-01',
      endDate: '2026-07-02',
      userIds: ['ding-1', 'ding-2'],
      autoMatch: true,
      userId: 21,
    });
  });

  it('does not allow a teacher to supply arbitrary DingTalk user IDs', async () => {
    await expect(
      controller.importFromDingTalk(
        { start: '2026-07-01', end: '2026-07-02', users: 'someone-else' },
        { user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
      ),
    ).rejects.toBeInstanceOf(ForbiddenException);

    expect(importService.importFromDingTalk).not.toHaveBeenCalled();
  });

  it('requires teachers to select one of their classes', async () => {
    await expect(
      controller.importFromDingTalk({ start: '2026-07-01', end: '2026-07-02' }, {
        user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
      } as never),
    ).rejects.toBeInstanceOf(BadRequestException);
  });
  it('lists only classes available to the current user for DingTalk import', async () => {
    attendanceService.getImportableClasses.mockResolvedValue([{ classId: 8, className: '八班' }]);

    await expect(
      controller.getDingTalkImportClasses({
        user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
      } as never),
    ).resolves.toEqual([{ classId: 8, className: '八班' }]);

    expect(attendanceService.getImportableClasses).toHaveBeenCalledWith(21, false);
  });

  it('always auto-matches class-scoped imports', async () => {
    attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-1']);

    await controller.importFromDingTalk(
      { start: '2026-07-01', end: '2026-07-02', classId: 8 },
      { user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false } } as never,
    );

    expect(importService.importFromDingTalk).toHaveBeenCalledWith(
      expect.objectContaining({ autoMatch: true }),
    );
  });

  it('allows class managers to choose any importable class and supply explicit DingTalk users', async () => {
    attendanceService.getImportableClasses.mockResolvedValue([{ classId: 1, className: '一班' }]);

    authzService.can.mockReturnValue(true);
    await expect(
      controller.getDingTalkImportClasses({
        user: {
          id: 7,
          username: 'manager',
          permissions: ['class:edit', 'attendance:create'],
          isSuperAdmin: false,
        },
      } as never),
    ).resolves.toEqual([{ classId: 1, className: '一班' }]);

    expect(attendanceService.getImportableClasses).toHaveBeenCalledWith(7, true);

    await controller.importFromDingTalk(
      { start: '2026-07-01', end: '2026-07-02', users: 'ding-1,ding-2' },
      {
        user: {
          id: 7,
          username: 'manager',
          permissions: ['class:edit', 'attendance:create'],
          isSuperAdmin: false,
        },
      } as never,
    );
    expect(importService.importFromDingTalk).toHaveBeenLastCalledWith({
      startDate: '2026-07-01',
      endDate: '2026-07-02',
      userIds: ['ding-1', 'ding-2'],
      autoMatch: true,
      userId: 7,
    });
  });
});

describe('AttendanceController — write data scope', () => {
  const attendanceService = {
    assertClassAccess: jest.fn(),
    getAccessibleClassIds: jest.fn(),
    getTeacherClassDingUserIds: jest.fn(),
    getLessonAttendanceImportDateRange: jest.fn().mockImplementation(
      (_schedule, lessonDate: string) => ({ startDate: lessonDate, endDate: lessonDate }),
    ),
    batchCreate: jest.fn(),
    generateFromSchedules: jest.fn(),
    findAttendanceRecord: jest.fn(),
    update: jest.fn(),
    remove: jest.fn(),
    getLessonAttendance: jest.fn(),
    createLessonAttendanceFromDingTalk: jest.fn(),
    findAttendanceSession: jest.fn(),
    completeLessonAttendance: jest.fn(),
  };
  const importService = { importFromDingTalk: jest.fn() };
  const logService = { log: jest.fn().mockResolvedValue(undefined) };
  const authzService = { can: jest.fn().mockReturnValue(false) };
  const req = {
    user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false, roles: ['teacher'] },
    headers: {},
  };
  let controller: AttendanceController;

  beforeEach(() => {
    jest.clearAllMocks();
    authzService.can.mockReturnValue(false);
    controller = new AttendanceController(
      attendanceService as unknown as AttendanceService,
      importService as unknown as AttendanceImportService,
      logService as unknown as OperationLogsService,
      authzService as never,
    );
  });

  it('checks every distinct class in a manual attendance batch', async () => {
    attendanceService.batchCreate.mockResolvedValue({ count: 2, records: [] });
    const dto = {
      records: [
        {
          studentId: 1,
          classId: 8,
          attendanceDate: '2026-07-01',
          session: 'morning',
          status: 'present',
        },
        {
          studentId: 2,
          classId: 9,
          attendanceDate: '2026-07-01',
          session: 'morning',
          status: 'present',
        },
      ],
    };

    await controller.batchCreate(dto, req);

    expect(attendanceService.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
    expect(attendanceService.assertClassAccess).toHaveBeenCalledWith(21, 9, false);
  });

  it('rejects classless manual attendance records for a class-scoped teacher', async () => {
    attendanceService.batchCreate.mockResolvedValue({ count: 1, records: [] });
    const dto = {
      records: [
        {
          studentId: 1,
          attendanceDate: '2026-07-01',
          session: 'morning',
          status: 'present',
        },
      ],
    };

    await expect(controller.batchCreate(dto, req)).rejects.toBeInstanceOf(
      ForbiddenException,
    );
    expect(attendanceService.batchCreate).not.toHaveBeenCalled();
  });

  it('checks the schedule class before starting and completing lesson attendance', async () => {
    attendanceService.getTeacherClassDingUserIds.mockResolvedValue(['ding-1']);
    importService.importFromDingTalk.mockResolvedValue({
      success: true,
      imported: 1,
      skipped: 0,
      matched: 1,
      errors: [],
      duration: 10,
    });
    attendanceService.getLessonAttendance.mockResolvedValue({
      schedule: { id: 4, classId: 8 },
      session: null,
      records: [],
    });
    attendanceService.createLessonAttendanceFromDingTalk.mockResolvedValue({
      schedule: { id: 4, classId: 8 },
      session: { id: 90, classId: 8 },
      records: [],
    });
    attendanceService.findAttendanceSession.mockResolvedValue({ id: 90, classId: 8 });
    attendanceService.completeLessonAttendance.mockResolvedValue({
      session: { id: 90, classId: 8, status: 'completed' },
      records: [],
    });

    await controller.pullLessonAttendance('4', { date: '2026-07-11' }, req);
    await controller.completeLessonAttendance('90', req);

    expect(attendanceService.assertClassAccess).toHaveBeenNthCalledWith(1, 21, 8, false);
    expect(attendanceService.assertClassAccess).toHaveBeenNthCalledWith(2, 21, 8, false);
  });

  it('checks class access before generating attendance from schedules', async () => {
    attendanceService.generateFromSchedules.mockResolvedValue({ count: 0, records: [] });

    await controller.generateFromSchedules({ classId: 8 }, req);

    expect(attendanceService.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
  });

  it('checks the attendance record owning class before update and delete', async () => {
    attendanceService.findAttendanceRecord.mockResolvedValue({ id: 4, classId: 8 });
    attendanceService.update.mockResolvedValue({ id: 4, classId: 8, status: 'late' });
    attendanceService.remove.mockResolvedValue({ deleted: true });

    await controller.update('4', { status: 'late' }, req);
    await controller.remove('4', req);

    expect(attendanceService.assertClassAccess).toHaveBeenCalledTimes(2);
    expect(attendanceService.assertClassAccess).toHaveBeenNthCalledWith(1, 21, 8, false);
    expect(attendanceService.assertClassAccess).toHaveBeenNthCalledWith(2, 21, 8, false);
  });
});

describe('AttendanceController — SSE progress scoping', () => {
  let progressSubject: Subject<{ phase: string; userId?: number }>;
  const importService = {
    importFromDingTalk: jest.fn(),
    get progress$() { return progressSubject.asObservable(); },
  };
  const attendanceService = {} as unknown as AttendanceService;
  const logService = {} as unknown as OperationLogsService;
  const authzService = {} as never;

  let controller: AttendanceController;

  beforeEach(() => {
    progressSubject = new Subject<{ phase: string; userId?: number }>();
    controller = new AttendanceController(
      attendanceService,
      importService as unknown as AttendanceImportService,
      logService,
      authzService,
    );
  });

  afterEach(() => {
    progressSubject.complete();
  });

  it('delivers events matching the requesting user id', () => {
    const received: Array<{ phase: string; userId?: number }> = [];
    const sub = controller.importProgressStream({
      user: { id: 42, username: 'alice', permissions: ['attendance:view'], isSuperAdmin: false, roles: [] },
    }).subscribe({
      next: (e) => received.push(JSON.parse(e.data as string)),
    });

    progressSubject.next({ phase: 'fetching', userId: 42 });
    progressSubject.next({ phase: 'complete', userId: 42 });

    sub.unsubscribe();
    expect(received).toHaveLength(2);
  });

  it('excludes events from a different user', () => {
    const received: Array<{ phase: string; userId?: number }> = [];
    const sub = controller.importProgressStream({
      user: { id: 42, username: 'alice', permissions: ['attendance:view'], isSuperAdmin: false, roles: [] },
    }).subscribe({
      next: (e) => received.push(JSON.parse(e.data as string)),
    });

    progressSubject.next({ phase: 'fetching', userId: 99 }); // different user
    progressSubject.next({ phase: 'complete', userId: 42 });

    sub.unsubscribe();
    // Only the matching event should arrive
    expect(received).toHaveLength(1);
    expect(received[0].userId).toBe(42);
  });

  it('excludes events with undefined userId (non-HTTP callers)', () => {
    const received: Array<{ phase: string; userId?: number }> = [];
    const sub = controller.importProgressStream({
      user: { id: 42, username: 'alice', permissions: ['attendance:view'], isSuperAdmin: false, roles: [] },
    }).subscribe({
      next: (e) => received.push(JSON.parse(e.data as string)),
    });
    progressSubject.next({ phase: 'fetching' }); // no userId
    progressSubject.next({ phase: 'complete', userId: 42 });

    sub.unsubscribe();
    // Only the event with matching userId should arrive
    expect(received).toHaveLength(1);
    expect(received[0].userId).toBe(42);
  });
});
