import { ContractDocumentHistoryService } from './contract-document-history.service';

/**
 * `iam.users.name` is a localized jsonb object, not a string. Reading it
 * naively puts "[object Object]" in the audit trail — or, worse, throws and
 * leaves every revision anonymous. These specs pin the resolution rules.
 */
describe('ContractDocumentHistoryService actor names', () => {
  const build = (rows: unknown[]) => {
    const saved: Array<Record<string, unknown>> = [];
    const service = Object.create(
      ContractDocumentHistoryService.prototype,
    ) as ContractDocumentHistoryService;
    Object.assign(service, {
      logger: { warn: jest.fn(), error: jest.fn() },
      dataSource: { query: jest.fn().mockResolvedValue(rows) },
      revisionRepo: {
        create: (row: Record<string, unknown>) => row,
        save: jest.fn((row: Record<string, unknown>) => {
          saved.push(row);
          return Promise.resolve(row);
        }),
        find: jest.fn().mockResolvedValue([]),
      },
    });
    return { service, saved };
  };

  const change = {
    kind: 'FIELD_CHANGED' as const,
    field: 'routes',
    label: 'Routes',
    from: 'A → B',
    to: 'A → C',
  };

  it('prefers the English label from the localized name object', async () => {
    const { service, saved } = build([
      { id: 'u-1', name: { am: 'ሱፐር አድሚን', en: 'Super Admin' }, username: 'superadmin' },
    ]);

    await service.recordChanges({ contractId: 'c-1', changes: [change], actorId: 'u-1' });

    expect(saved[0].actorName).toBe('Super Admin');
  });

  it('falls back to another locale, then username, then email', async () => {
    const onlyAmharic = build([{ id: 'u-1', name: { am: 'ሱፐር' }, username: 'x' }]);
    await onlyAmharic.service.recordChanges({
      contractId: 'c-1',
      changes: [change],
      actorId: 'u-1',
    });
    expect(onlyAmharic.saved[0].actorName).toBe('ሱፐር');

    const noName = build([{ id: 'u-1', name: null, username: 'operator', email: 'o@edr' }]);
    await noName.service.recordChanges({
      contractId: 'c-1',
      changes: [change],
      actorId: 'u-1',
    });
    expect(noName.saved[0].actorName).toBe('operator');

    const emailOnly = build([{ id: 'u-1', name: {}, username: null, email: 'o@edr.local' }]);
    await emailOnly.service.recordChanges({
      contractId: 'c-1',
      changes: [change],
      actorId: 'u-1',
    });
    expect(emailOnly.saved[0].actorName).toBe('o@edr.local');
  });

  it('never writes "[object Object]" as the actor name', async () => {
    const { service, saved } = build([{ id: 'u-1', name: { en: 'Real Name' } }]);

    await service.recordChanges({ contractId: 'c-1', changes: [change], actorId: 'u-1' });

    expect(String(saved[0].actorName)).not.toContain('object Object');
  });

  it('records nothing when the change set is empty', async () => {
    const { service, saved } = build([]);

    await service.recordChanges({ contractId: 'c-1', changes: [], actorId: 'u-1' });

    expect(saved).toHaveLength(0);
  });

  it('still records the revision when the user lookup fails', async () => {
    const { service, saved } = build([]);
    Object.assign(service, {
      dataSource: { query: jest.fn().mockRejectedValue(new Error('iam down')) },
    });

    await service.recordChanges({ contractId: 'c-1', changes: [change], actorId: 'u-1' });

    expect(saved).toHaveLength(1);
    expect(saved[0].actorName).toBeNull();
  });

  it('resolves names for legacy rows that predate the actor_name column', async () => {
    const { service } = build([{ id: 'u-1', name: { en: 'Abenezer Haile' } }]);
    Object.assign(service, {
      revisionRepo: {
        find: jest
          .fn()
          .mockResolvedValue([{ id: 'r-1', actorId: 'u-1', actorName: null }]),
      },
    });

    const [revision] = await service.list('c-1');

    expect(revision.actorName).toBe('Abenezer Haile');
  });
});
