import 'reflect-metadata';
import { PERMISSION_KEY } from '../auth/decorators/permission.decorator';
import { ValidationPipe } from '@nestjs/common';
import { ClassesController } from './classes.controller';
import { ClassesService } from './classes.service';
import { QueryClassDto } from './dto/class.dto';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { NotificationsService } from '../notifications/notifications.service';
import { CaslAction, SubjectName } from '../authorization';

describe('ClassesController - class data scope', () => {
  const service = {
    getAccessibleClassIds: jest.fn(),
    assertClassAccess: jest.fn(),
    findAll: jest.fn(),
    findOne: jest.fn(),
    getSchedule: jest.fn(),
    getAttendanceSummary: jest.fn(),
    getStudents: jest.fn(),
    getTeachers: jest.fn(),
  };
  const authzService = { can: jest.fn() };
  const logService = {};
  const notificationsService = {};
  let controller: ClassesController;

  const request = (permissions: string[] = [], isSuperAdmin = false) => ({
    user: {
      id: 21,
      username: 'user',
      permissions,
      isSuperAdmin,
      roles: [],
    },
  });

  beforeEach(() => {
    jest.clearAllMocks();
    authzService.can.mockImplementation((req, action, subject) => {
      if (subject !== SubjectName.Class) return false;
      if (req.user.isSuperAdmin && action === CaslAction.Manage) return true;
      if (action === CaslAction.Create) return req.user.permissions.includes('class:create');
      if (action === CaslAction.Update) return req.user.permissions.includes('class:edit');
      return false;
    });
    service.getAccessibleClassIds.mockResolvedValue(undefined);
    service.assertClassAccess.mockResolvedValue(undefined);
    service.findAll.mockResolvedValue([]);
    service.findOne.mockResolvedValue({ id: 8 });
    service.getSchedule.mockResolvedValue([]);
    service.getAttendanceSummary.mockResolvedValue({});
    service.getStudents.mockResolvedValue([]);
    service.getTeachers.mockResolvedValue([]);
    controller = new ClassesController(
      service as unknown as ClassesService,
      logService as unknown as OperationLogsService,
      notificationsService as unknown as NotificationsService,
      authzService as never,
    );
  });

  it.each([
    ['class creator', request(['class:view', 'class:create'])],
    ['class editor', request(['class:view', 'class:edit'])],
    ['super admin', request([], true)],
  ])('allows %s to list all classes', async (_label, req) => {
    await controller.findAll({}, req);

    expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, true);
    expect(service.findAll).toHaveBeenCalledWith({}, undefined);
  });

  it('keeps view-only users scoped to their assigned classes', async () => {
    service.getAccessibleClassIds.mockResolvedValue([8]);

    await controller.findAll({}, request(['class:view']));

    expect(service.getAccessibleClassIds).toHaveBeenCalledWith(21, false);
    expect(service.findAll).toHaveBeenCalledWith({}, [8]);
  });

  it('allows a class creator to read an unassigned class through every detail endpoint', async () => {
    const req = request(['class:view', 'class:create']);

    await controller.findOne('8', req);
    await controller.getSchedule('8', {}, req);
    await controller.getAttendanceSummary('8', {}, req);
    await controller.getStudents('8', req);
    await controller.getTeachers('8', req);

    expect(service.assertClassAccess).toHaveBeenCalledTimes(5);
    expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, true);
  });

  it('requires a view-only user to be assigned before reading class details', async () => {
    await controller.findOne('8', request(['class:view']));

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

describe('QueryClassDto - query transformation', () => {
  const pipe = new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true });

  it.each([
    ['false', false],
    ['0', false],
    ['true', true],
    ['1', true],
  ])('transforms isArchived=%s to %s', async (input, expected) => {
    await expect(
      pipe.transform(
        { isArchived: input },
        { type: 'query', metatype: QueryClassDto, data: undefined },
      ),
    ).resolves.toEqual({ isArchived: expected });
  });
});

describe('ClassesController purge route', () => {
  it('requires class:purge on permanent delete route', () => {
    expect(Reflect.getMetadata(PERMISSION_KEY, ClassesController.prototype.purge)).toEqual([
      'class:purge',
    ]);
  });

  it('writes permanent delete audit logs', async () => {
    const service = {
      purge: jest.fn().mockResolvedValue({ message: '已永久删除班级（不可恢复）' }),
    };
    const log = jest.fn().mockResolvedValue(undefined);
    const controller = new ClassesController(service as never, { log } as never, {} as never, {} as never);
    const req = { user: { id: 1, username: 'admin' }, ip: '127.0.0.1', headers: {} };
    await controller.purge('1', req);
    expect(service.purge).toHaveBeenCalledWith(1);
    expect(log).toHaveBeenCalledWith(
      expect.objectContaining({ module: '班级管理', action: '永久删除班级', targetId: 1 }),
    );
  });
});
