import {
  allocateBookingNumberFromQuery,
  allocateUniqueReferenceNumber,
  extractReferenceDigits,
  normalizeReferencePrefix,
  REFERENCE_NUMBER_MAX_ATTEMPTS,
} from './reference-number';

const TENANT_ID = '11111111-1111-1111-1111-111111111111';
const SIX_DIGIT = /^(0[1-9][0-9]{4}|[1-9][0-9]{5})$/;

describe('allocateUniqueReferenceNumber', () => {
  it('returns BK- + 6 digits with at most one leading zero', async () => {
    const query = jest.fn().mockResolvedValue([]);
    const result = await allocateUniqueReferenceNumber('BK', TENANT_ID, query);
    expect(result).toMatch(/^BK-/);
    expect(result.slice(3)).toMatch(SIX_DIGIT);
    expect(result).not.toMatch(/^BK-00/);
    expect(query).toHaveBeenCalledTimes(1);
    expect(query.mock.calls[0][0]).toMatch(/FROM bookings/);
    expect(query.mock.calls[0][0]).toMatch(/tenant_id = \$1/);
    expect(query.mock.calls[0][0]).toMatch(/booking_number/);
    expect(query.mock.calls[0][1]).toEqual([TENANT_ID, result]);
  });

  it('returns QRY- + 6 digits with at most one leading zero', async () => {
    const query = jest.fn().mockResolvedValue([]);
    const result = await allocateUniqueReferenceNumber('QRY', TENANT_ID, query);
    expect(result).toMatch(/^QRY-/);
    expect(result.slice(4)).toMatch(SIX_DIGIT);
    expect(result).not.toMatch(/^QRY-00/);
    expect(query.mock.calls[0][0]).toMatch(/FROM queries/);
    expect(query.mock.calls[0][0]).toMatch(/tenant_id = \$1/);
    expect(query.mock.calls[0][0]).toMatch(/query_number/);
    expect(query.mock.calls[0][1]).toEqual([TENANT_ID, result]);
  });

  it('retries when the candidate is already taken for this tenant', async () => {
    const query = jest
      .fn()
      .mockResolvedValueOnce([{ taken: 1 }])
      .mockResolvedValueOnce([]);
    const result = await allocateUniqueReferenceNumber('BK', TENANT_ID, query);
    expect(result).toMatch(/^BK-/);
    expect(query).toHaveBeenCalledTimes(2);
  });

  it('throws after the safety cap when every candidate is taken', async () => {
    const query = jest.fn().mockResolvedValue([{}]);
    await expect(allocateUniqueReferenceNumber('QRY', TENANT_ID, query)).rejects.toThrow(
      /could not allocate a unique QRY- number/,
    );
    expect(query).toHaveBeenCalledTimes(REFERENCE_NUMBER_MAX_ATTEMPTS);
  });

  it('never emits more than one leading zero across many draws', async () => {
    const query = jest.fn().mockResolvedValue([]);
    for (let i = 0; i < 50; i++) {
      const result = await allocateUniqueReferenceNumber('BK', TENANT_ID, query);
      expect(result.slice(3)).toMatch(SIX_DIGIT);
      expect(result.startsWith('BK-00')).toBe(false);
    }
  });

  it('prints a custom tenant prefix while still checking the bookings table', async () => {
    const query = jest.fn().mockResolvedValue([]);
    const result = await allocateUniqueReferenceNumber('BK', TENANT_ID, query, 'MT');
    expect(result).toMatch(/^MT-/);
    expect(result.slice(3)).toMatch(SIX_DIGIT);
    expect(query.mock.calls[0][0]).toMatch(/FROM bookings/);
    expect(query.mock.calls[0][1]).toEqual([TENANT_ID, result]);
  });
});

describe('normalizeReferencePrefix', () => {
  it('uppercases and strips non-alphanumerics', () => {
    expect(normalizeReferencePrefix('mt', 'BK')).toBe('MT');
    expect(normalizeReferencePrefix('m-t 1', 'BK')).toBe('MT1');
  });

  it('caps at 10 chars', () => {
    expect(normalizeReferencePrefix('ABCDEFGHIJKLMNOP', 'BK')).toBe('ABCDEFGHIJ');
  });

  it('falls back when empty / null / all-invalid', () => {
    expect(normalizeReferencePrefix('', 'BK')).toBe('BK');
    expect(normalizeReferencePrefix(null, 'BK')).toBe('BK');
    expect(normalizeReferencePrefix('   ', 'BK')).toBe('BK');
    expect(normalizeReferencePrefix('!!!', 'BK')).toBe('BK');
  });
});

describe('extractReferenceDigits', () => {
  it('pulls the 6-digit suffix', () => {
    expect(extractReferenceDigits('QRY-093254')).toBe('093254');
    expect(extractReferenceDigits('MT-746284')).toBe('746284');
  });

  it('returns null when there is no 6-digit suffix', () => {
    expect(extractReferenceDigits('')).toBeNull();
    expect(extractReferenceDigits(null)).toBeNull();
    expect(extractReferenceDigits('QRY-123')).toBeNull();
  });
});

describe('allocateBookingNumberFromQuery', () => {
  it('reuses the query digits with the tenant prefix (1:1)', async () => {
    const query = jest.fn().mockResolvedValue([]); // not taken
    const result = await allocateBookingNumberFromQuery('QRY-093254', TENANT_ID, query, 'MT');
    expect(result).toBe('MT-093254');
    expect(query).toHaveBeenCalledTimes(1);
    expect(query.mock.calls[0][0]).toMatch(/FROM bookings/);
    expect(query.mock.calls[0][1]).toEqual([TENANT_ID, 'MT-093254']);
  });

  it('defaults the prefix to BK', async () => {
    const query = jest.fn().mockResolvedValue([]);
    const result = await allocateBookingNumberFromQuery('QRY-093254', TENANT_ID, query);
    expect(result).toBe('BK-093254');
  });

  it('falls back to a fresh random number when the seeded one is taken', async () => {
    const query = jest
      .fn()
      .mockResolvedValueOnce([{ taken: 1 }]) // MT-093254 already exists
      .mockResolvedValueOnce([]); // random candidate free
    const result = await allocateBookingNumberFromQuery('QRY-093254', TENANT_ID, query, 'MT');
    expect(result).toMatch(/^MT-/);
    expect(result).not.toBe('MT-093254');
    expect(query).toHaveBeenCalledTimes(2);
  });

  it('goes straight to random when the query number has no digits', async () => {
    const query = jest.fn().mockResolvedValue([]);
    const result = await allocateBookingNumberFromQuery(null, TENANT_ID, query, 'MT');
    expect(result).toMatch(/^MT-/);
    expect(query).toHaveBeenCalledTimes(1);
  });
});
