import { ForbiddenException } from '@nestjs/common';
import { SchedulesController } from './schedules.controller';
import { SchedulesService } from './schedules.service';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { NotificationsService } from '../notifications/notifications.service';

const teacherRequest = {
  user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
  headers: {},
};

const scheduleDto = {
  classId: 8,
  classroomId: 3,
  weekDay: 1,
  startTime: '09:00',
  endTime: '10:00',
  startDate: '2026-07-01',
  endDate: '2026-07-31',
  subject: '数学',
};

describe('SchedulesController — class data scope', () => {
  const service = {
    getAccessibleClassIds: jest.fn(),
    assertClassAccess: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    remove: jest.fn(),
    getClassroomOccupancy: jest.fn(),
    maskScheduleOccupancy: jest.fn((schedule) => ({
      ...schedule,
      id: null,
      classId: null,
      subject: '已占用',
      teacherId: null,
      notes: null,
      canViewDetails: false,
    })),
    checkConflict: jest.fn(),
  };
  const logService = { log: jest.fn().mockResolvedValue(undefined) };
  const notificationsService = { create: jest.fn() };
  const ability = { can: jest.fn().mockReturnValue(false) };
  const authzService = { abilityForRequest: jest.fn().mockReturnValue(ability) };
  let controller: SchedulesController;

  beforeEach(() => {
    jest.clearAllMocks();
    ability.can.mockReturnValue(false);
    service.getAccessibleClassIds.mockResolvedValue([8]);
    controller = new SchedulesController(
      service as unknown as SchedulesService,
      logService as unknown as OperationLogsService,
      notificationsService as unknown as NotificationsService,
      authzService as never,
    );
  });

  it('checks the requested class before creating a schedule', async () => {
    service.create.mockResolvedValue({ id: 1, ...scheduleDto });

    await controller.create(scheduleDto, teacherRequest);

    expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
    expect(service.create).toHaveBeenCalledWith(scheduleDto);
  });

  it('checks both the current and destination class before moving a schedule', async () => {
    service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });
    service.update.mockResolvedValue({ id: 4, ...scheduleDto, classId: 9 });

    await controller.update('4', { classId: 9 }, teacherRequest);

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

  it('checks the owning class before returning full schedule details', async () => {
    service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });

    await controller.findOne('4', teacherRequest);

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

  it('checks the owning class before deleting a schedule', async () => {
    service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });
    service.remove.mockResolvedValue({ success: true });

    await controller.remove('4', teacherRequest);

    expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
    expect(service.remove).toHaveBeenCalledWith(4);
  });

  it('returns only masked occupancy blocks from the classroom occupancy endpoint', async () => {
    service.getClassroomOccupancy.mockResolvedValue([
      { id: 2, classId: 99, subject: '英语', teacherId: 7, notes: '隐私', classroomId: 3 },
    ]);

    const result = await controller.getClassroomOccupancy('3', undefined, teacherRequest);

    expect(result).toEqual([
      expect.objectContaining({
        id: null,
        classId: null,
        subject: '已占用',
        teacherId: null,
        notes: null,
        canViewDetails: false,
      }),
    ]);
    expect(JSON.stringify(result)).not.toContain('英语');
    expect(JSON.stringify(result)).not.toContain('隐私');
  });

  it('rejects records without a class instead of exposing full details to a scoped teacher', async () => {
    service.findOne.mockResolvedValue({ id: 4, ...scheduleDto, classId: null });

    await expect(controller.findOne('4', teacherRequest)).rejects.toBeInstanceOf(
      ForbiddenException,
    );
  });
});
