import { ConflictException, ServiceUnavailableException } from '@nestjs/common';
import { Student, 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 matchRuleRepo = {
    find: jest.fn(),
    findOne: jest.fn(),
    save: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  };
  const jinshujuService = { fetchAllEntries: jest.fn().mockResolvedValue([]) };
  const manager = {
    query: jest.fn().mockResolvedValue([{ id: 1 }]),
    update: jest.fn(),
    save: jest.fn(),
    create: jest.fn().mockImplementation((_entity, value) => value),
  };
  const dataSource = { transaction: jest.fn((callback) => callback(manager)) };
  const service = new SyncService(
    syncLogRepo as never,
    syncStateRepo as never,
    { find: jest.fn().mockResolvedValue([{ dingUserId: 'u1' }]) } as never,
    matchRuleRepo as never,
    dingTalkService as never,
    { syncAll: jest.fn().mockResolvedValue({ userCount: 0 }) } as never,
    jinshujuService as never,
    attendanceImportService as never,
    {} as never,
    dataSource as never,
  );
  return {
    service,
    syncStateRepo,
    syncLogRepo,
    dingTalkService,
    attendanceImportService,
    matchRuleRepo,
    jinshujuService,
    manager,
  };
}

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') }),
    );
  });

  it('applies the selected field mappings and decisions', async () => {
    const { service, matchRuleRepo, jinshujuService, manager } = createService();
    matchRuleRepo.findOne.mockResolvedValue({
      id: 7,
      formToken: 'form-a',
      mappings: { name: 'field_3', phone: 'field_4', idNumber: 'field_5' },
    });
    jinshujuService.fetchAllEntries.mockResolvedValue([
      {
        serial_number: 1,
        field_3: '张三',
        field_4: '13800000000',
        field_5: '123456',
        created_at: '',
        updated_at: '',
      },
    ]);

    const log = await service.applyJinshuju(
      'key',
      'secret',
      'form-a',
      [{ serialNumber: 1, action: 'match', matchStudentId: 99 }],
      7,
    );

    expect(manager.update).toHaveBeenCalledWith(Student, 99, {
      name: '张三',
      phone: '13800000000',
      idNumber: '123456',
    });
    expect(log.recordsCount).toBe(1);
  });
});
