import { ConflictException, ServiceUnavailableException } from '@nestjs/common';
import { SyncLog } from '../entities';
import { SyncService } from './sync.service';

function queryBuilder(affected = 1) {
  const builder = {
    insert: jest.fn(),
    update: jest.fn(),
    values: jest.fn(),
    orIgnore: jest.fn(),
    set: jest.fn(),
    where: jest.fn(),
    andWhere: jest.fn(),
    execute: jest.fn().mockResolvedValue({ affected }),
  };
  for (const method of ['insert', 'update', 'values', 'orIgnore', 'set', 'where', 'andWhere'] as const) {
    builder[method].mockReturnValue(builder);
  }
  return builder;
}

function createService(options?: {
  affected?: number;
  attendanceResult?: { success: boolean; imported: number; errors: string[] };
}) {
  const builders = [queryBuilder(), queryBuilder(options?.affected), queryBuilder()];
  const syncStateRepo = {
    createQueryBuilder: jest.fn().mockImplementation(() => builders.shift()),
    findOne: jest.fn().mockResolvedValue({ lastSyncAt: null }),
    update: jest.fn().mockResolvedValue({ affected: 1 }),
  };
  const syncLogRepo = {
    create: jest.fn().mockImplementation((value: Partial<SyncLog>) => value),
    save: jest.fn().mockImplementation(async (value: SyncLog) => value),
    findOne: jest.fn(),
    find: jest.fn(),
  };
  const dingTalkService = {
    syncAll: jest.fn().mockResolvedValue({ created: 1, updated: 2, conflicts: [] }),
  };
  const attendanceImportService = {
    importFromDingTalk: jest.fn().mockResolvedValue(options?.attendanceResult ?? {
      success: true,
      imported: 3,
      errors: [],
    }),
  };
  const service = new SyncService(
    syncLogRepo as never,
    syncStateRepo as never,
    { find: jest.fn().mockResolvedValue([{ dingUserId: 'u1' }]) } as never,
    dingTalkService as never,
    { syncAll: jest.fn().mockResolvedValue({ userCount: 0 }) } as never,
    attendanceImportService as never,
    {} as never,
  );
  return { service, syncStateRepo, syncLogRepo, dingTalkService, attendanceImportService };
}

describe('SyncService — safe DingTalk orchestration', () => {
  it('uses a dedicated student cursor and records a successful student sync', async () => {
    const { service, syncStateRepo, syncLogRepo } = createService();

    const log = await service.syncDingTalkStudents(9);

    expect(log).toMatchObject({
      platform: 'dingtalk_students',
      status: 'success',
      recordsCount: 3,
    });
    expect(syncStateRepo.update).toHaveBeenCalledWith(
      { platform: 'dingtalk_students' },
      expect.objectContaining({ lastSyncAt: expect.any(Date) }),
    );
    expect(syncLogRepo.save).toHaveBeenCalled();
  });

  it('rejects a second run when the database lease is held', async () => {
    const { service } = createService({ affected: 0 });

    await expect(service.syncDingTalkStudents()).rejects.toBeInstanceOf(ConflictException);
  });

  it('does not advance attendance cursor when import reports failure', async () => {
    const { service, syncStateRepo, syncLogRepo } = createService({
      attendanceResult: { success: false, imported: 0, errors: ['upstream failed'] },
    });

    await expect(service.syncDingTalkAttendance()).rejects.toBeInstanceOf(
      ServiceUnavailableException,
    );
    expect(syncStateRepo.update).not.toHaveBeenCalled();
    expect(syncLogRepo.save).toHaveBeenLastCalledWith(
      expect.objectContaining({ status: 'failed', errorMessage: expect.stringContaining('upstream failed') }),
    );
  });
});
