import { LoggingService } from './logging.service';

describe('LoggingService', () => {
  it('can persist an event through the caller transaction manager', async () => {
    const defaultRepository = { save: jest.fn() };
    const transactionRepository = { save: jest.fn() };
    const manager = {
      getRepository: jest.fn(() => transactionRepository),
    } as any;
    const service = new LoggingService(defaultRepository as any);
    const event = {
      event: 'admin_user_profile_update' as const,
      details: 'targetUserId=2',
      userId: 1,
    };

    await service.info(event, manager);

    expect(transactionRepository.save).toHaveBeenCalledWith({
      ...event,
      level: 'INFO',
    });
    expect(defaultRepository.save).not.toHaveBeenCalled();
  });
});

describe('LoggingService.findLogs', () => {
  let rows: any[];
  let total: number;
  let query: any;
  let repository: any;
  let service: LoggingService;

  beforeEach(() => {
    rows = [];
    total = 0;
    query = chain({
      getMany: jest.fn(() => rows),
      getCount: jest.fn(() => total),
    });
    repository = {
      createQueryBuilder: jest.fn(() => query),
    };
    service = new LoggingService(repository);
  });

  it('returns a paginated page with data, total and hasNextPage', async () => {
    rows = [
      { id: 1, level: 'INFO', event: 'team_create', details: 'teamId=5', userId: 3, createdAt: new Date('2026-08-01') },
    ];
    total = 21;

    const result = await service.findLogs({ page: 1, limit: 20 });

    expect(result).toEqual({ data: rows, page: 1, limit: 20, total: 21, hasNextPage: true });
    expect(query.orderBy).toHaveBeenCalledWith('log.createdAt', 'DESC');
    expect(query.offset).toHaveBeenCalledWith(0);
    expect(query.limit).toHaveBeenCalledWith(20);
  });

  it('reports hasNextPage=false on the last page', async () => {
    total = 20;

    const result = await service.findLogs({ page: 1, limit: 20 });

    expect(result.hasNextPage).toBe(false);
  });

  it('offsets by (page - 1) * limit', async () => {
    await service.findLogs({ page: 3, limit: 10 });

    expect(query.offset).toHaveBeenCalledWith(20);
  });

  it('filters by level and event when provided', async () => {
    await service.findLogs({ page: 1, limit: 20, level: 'ERROR', event: 'cashbox_export_subscription_run_fail' });

    expect(query.andWhere).toHaveBeenCalledWith('log.level = :level', { level: 'ERROR' });
    expect(query.andWhere).toHaveBeenCalledWith('log.event = :event', {
      event: 'cashbox_export_subscription_run_fail',
    });
  });

  it('filters by an inclusive date range when from/to are provided', async () => {
    await service.findLogs({ page: 1, limit: 20, from: '2026-01-01', to: '2026-01-31' });

    expect(query.andWhere).toHaveBeenCalledWith('log.createdAt >= :from', { from: '2026-01-01' });
    // `to` is a plain date (e.g. from a <input type="date">); comparing it
    // as-is would parse to midnight and exclude the whole last day, so it
    // must be widened to the end of that day to be genuinely inclusive.
    expect(query.andWhere).toHaveBeenCalledWith('log.createdAt <= :to', {
      to: new Date('2026-01-31T23:59:59.999Z'),
    });
  });

  it('does not add level/event/date filters when omitted', async () => {
    await service.findLogs({ page: 1, limit: 20 });

    expect(query.andWhere).not.toHaveBeenCalled();
  });

  it('filters details by a case-insensitive search term', async () => {
    await service.findLogs({ page: 1, limit: 20, search: ' TeamId=5 ' });

    expect(query.andWhere).toHaveBeenCalledWith('LOWER(log.details) LIKE :search', {
      search: '%teamid=5%',
    });
  });

  it('ignores a blank search term', async () => {
    await service.findLogs({ page: 1, limit: 20, search: '   ' });

    expect(query.andWhere).not.toHaveBeenCalled();
  });

  function chain(overrides: Record<string, jest.Mock>) {
    const builder: Record<string, jest.Mock> = {};
    ['andWhere', 'orderBy', 'offset', 'limit'].forEach((method) => {
      builder[method] = jest.fn(() => builder);
    });
    return Object.assign(builder, overrides);
  }
});
