import { EntityManager } from 'typeorm';
import { Student, StudentDingMapping } from '../entities';
import { syncDingTalkStudents } from './dingtalk-student-sync';

function managerFixture(options?: {
  mappings?: StudentDingMapping[];
  students?: Student[];
  occupiedPhones?: Student[];
  failMappingSave?: boolean;
}) {
  const saves: Array<{ entity: unknown; values: unknown }> = [];
  let nextId = 100;
  const manager = {
    find: jest.fn().mockImplementation(async (entity: unknown, findOptions?: unknown) => {
      if (entity === StudentDingMapping) return options?.mappings ?? [];
      if (entity === Student) {
        const where = findOptions && typeof findOptions === 'object' && 'where' in findOptions
          ? findOptions.where
          : undefined;
        if (where && typeof where === 'object' && 'phone' in where) {
          return options?.occupiedPhones ?? [];
        }
        return options?.students ?? [];
      }
      return [];
    }),
    findOne: jest.fn().mockResolvedValue({ id: 7, isHost: true, status: 'active' }),
    create: jest.fn().mockImplementation((_entity: unknown, value: object) => ({ ...value })),
    save: jest.fn().mockImplementation(async (entity: unknown, values: unknown) => {
      saves.push({ entity, values });
      if (entity === StudentDingMapping && options?.failMappingSave) throw new Error('mapping failed');
      if (entity === Student && Array.isArray(values)) {
        return values.map((value) => ({ ...value, id: 'id' in value ? value.id : nextId++ }));
      }
      return values;
    }),
  };
  return { manager: manager as unknown as EntityManager, saves };
}

describe('syncDingTalkStudents', () => {
  it('deduplicates DingTalk users and creates a host-owned student and mapping once', async () => {
    const { manager, saves } = managerFixture();

    const result = await syncDingTalkStudents(manager, [
      { dingUserId: 'u1', name: '张三', mobile: '13800000000' },
      { dingUserId: 'u1', name: '重复项', mobile: '13800000000' },
    ]);

    expect(result).toMatchObject({ created: 1, updated: 0, conflicts: [] });
    expect(saves).toContainEqual({
      entity: Student,
      values: [expect.objectContaining({ name: '张三', organizationId: 7 })],
    });
    expect(saves).toContainEqual({
      entity: StudentDingMapping,
      values: [expect.objectContaining({ dingUserId: 'u1', studentId: 100 })],
    });
  });

  it('reports a phone conflict without creating a duplicate student', async () => {
    const occupied = { id: 5, phone: '13800000000' } as Student;
    const { manager, saves } = managerFixture({ occupiedPhones: [occupied] });

    const result = await syncDingTalkStudents(manager, [
      { dingUserId: 'u2', name: '李四', mobile: occupied.phone },
    ]);

    expect(result.created).toBe(0);
    expect(result.conflicts).toEqual([
      expect.objectContaining({ dingUserId: 'u2', reason: expect.stringContaining('人工绑定') }),
    ]);
    expect(saves).toEqual([]);
  });

  it('surfaces mapping persistence failure so the surrounding transaction can roll back', async () => {
    const { manager } = managerFixture({ failMappingSave: true });

    await expect(
      syncDingTalkStudents(manager, [{ dingUserId: 'u3', name: '王五' }]),
    ).rejects.toThrow('mapping failed');
  });

  it('keeps an archived mapped student archived while refreshing profile data', async () => {
    const archived = { id: 8, name: '旧名', phone: null, status: 'archived' } as unknown as Student;
    const mapping = { dingUserId: 'u8', studentId: 8 } as StudentDingMapping;
    const { manager } = managerFixture({ mappings: [mapping], students: [archived] });

    const result = await syncDingTalkStudents(manager, [
      { dingUserId: 'u8', name: '新名', mobile: '13900000000' },
    ]);

    expect(result.updated).toBe(1);
    expect(archived).toMatchObject({ name: '新名', phone: '13900000000', status: 'archived' });
  });
});
