import { BadRequestException } from '@nestjs/common';
import { ExamScore } from '../entities';
import { ExamsService } from './exams.service';

function createService(transaction: (run: (manager: any) => Promise<unknown>) => Promise<unknown>) {
  return new ExamsService(
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    { findOne: jest.fn().mockResolvedValue({ id: 1 }) } as never,
    { transaction } as never,
  );
}

describe('ExamsService', () => {
  it('creates score rows from the active class roster snapshot', async () => {
    const members = [
      { studentId: 11, status: 'active' },
      { studentId: 12, status: 'active' },
    ];
    const manager = {
      findOne: jest.fn().mockResolvedValue({ id: 3, isArchived: false }),
      find: jest.fn().mockResolvedValue(members),
      create: jest.fn((_entity, value) => value),
      save: jest.fn(async (entity, value) =>
        entity.name === 'Exam' ? { ...value, id: 9 } : value,
      ),
    };
    const service = createService(async (run) => run(manager));

    await service.create(
      {
        examType: '月考',
        examName: '七月月考',
        subject: '数学',
        examDate: '2026-07-21',
        classId: 3,
      },
      1,
      true,
    );

    expect(manager.find).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ where: { classId: 3, status: 'active' } }),
    );
    expect(manager.save).toHaveBeenCalledWith(
      ExamScore,
      expect.arrayContaining([
        expect.objectContaining({ examId: 9, studentId: 11, score: null }),
        expect.objectContaining({ examId: 9, studentId: 12, score: null }),
      ]),
    );
  });

  it('rejects creating an exam for an empty class', async () => {
    const manager = {
      findOne: jest.fn().mockResolvedValue({ id: 3, isArchived: false }),
      find: jest.fn().mockResolvedValue([]),
    };
    const service = createService(async (run) => run(manager));

    await expect(
      service.create(
        {
          examType: '月考',
          examName: '七月月考',
          subject: '数学',
          examDate: '2026-07-21',
          classId: 3,
        },
        1,
        true,
      ),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('counts zero, ignores empty scores, and uses competition ranking', async () => {
    const rows = [
      { id: 1, score: 90, classAvg: null, rank: null },
      { id: 2, score: 90, classAvg: null, rank: null },
      { id: 3, score: 60, classAvg: null, rank: null },
      { id: 4, score: 0, classAvg: null, rank: null },
      { id: 5, score: null, classAvg: null, rank: null },
    ];
    const manager = {
      findOne: jest
        .fn()
        .mockResolvedValueOnce({ id: 8, classId: 3, status: 'active' })
        .mockResolvedValueOnce(rows[0])
        .mockResolvedValueOnce(rows[0]),
      find: jest.fn().mockResolvedValue(rows),
      save: jest.fn(async (_entity, value) => value),
    };
    const service = createService(async (run) => run(manager));

    await service.updateScore(8, 1, 90, 1, true);

    expect(rows.map((row) => row.rank)).toEqual([1, 1, 3, 4, null]);
    expect(rows.map((row) => row.classAvg)).toEqual([60, 60, 60, 60, 60]);
  });

  it('clears a score and recalculates the remaining rows', async () => {
    const rows = [
      { id: 1, score: 90, classAvg: 80, rank: 1 },
      { id: 2, score: 70, classAvg: 80, rank: 2 },
    ];
    const manager = {
      findOne: jest
        .fn()
        .mockResolvedValueOnce({ id: 8, classId: 3, status: 'active' })
        .mockResolvedValueOnce(rows[0])
        .mockResolvedValueOnce(rows[0]),
      find: jest.fn().mockResolvedValue(rows),
      save: jest.fn(async (_entity, value) => value),
    };
    const service = createService(async (run) => run(manager));

    await service.updateScore(8, 1, null, 1, true);

    expect(rows).toEqual([
      expect.objectContaining({ score: null, classAvg: 70, rank: null }),
      expect.objectContaining({ score: 70, classAvg: 70, rank: 1 }),
    ]);
  });
});
