import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils';

// Reads the page count directly out of the raw PDF bytes instead of pulling in
// a parser dependency. Coupled to pdfkit's current /Pages dict serialization -
// a pdfkit upgrade that reorders/reflows it could require adjusting this regex.
function pdfPageCount(buffer: Buffer): number {
  const match = buffer.toString('latin1').match(/\/Type\s*\/Pages[\s\S]{0,80}?\/Count\s+(\d+)/);
  if (!match) throw new Error('Could not find page count in PDF buffer');
  return Number(match[1]);
}

describe('buildRows', () => {
  const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({
    id: 5,
    name: 'Team A',
    alias: 'team-a',
    transactions: [],
    players: [],
    ...overrides,
  });

  it('includes team-wallet credit rows as-is and negates expense rows so they reduce the budget', () => {
    // DB stores TeamWalletTransaction.amount as a positive number even for
    // expenses (see team-wallet-transaction.entity.ts setBalance()); buildRows
    // must negate expenses itself so they subtract from the running total.
    const rows = buildRows(
      team({
        transactions: [
          { date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Sponsoring', type: { name: 'credit' } },
          { date: '2026-08-10T00:00:00.000Z', amount: 20, note: 'Bälle', type: { name: 'expense' } },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows).toEqual([
      { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Sponsoring', amount: 100, runningTotal: 100 },
      { date: '2026-08-10T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Bälle', amount: -20, runningTotal: 80 },
    ]);
  });

  it('includes only "payment" player transactions, excluding fee/levy/fine', () => {
    const rows = buildRows(
      team({
        players: [
          {
            firstName: 'Alex',
            lastName: 'Muster',
            transactions: [
              { date: '2026-08-03T00:00:00.000Z', amount: 10, note: 'Bar bezahlt', type: { name: 'payment' } },
              { date: '2026-08-04T00:00:00.000Z', amount: 15, note: 'Monatsbeitrag', type: { name: 'fee' } },
            ],
          },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows).toEqual([
      { date: '2026-08-03T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 },
    ]);
  });

  it('excludes rows outside the [from, to] range and sorts the rest chronologically', () => {
    const rows = buildRows(
      team({
        transactions: [
          { date: '2026-07-31T23:59:00.000Z', amount: 5, note: 'zu früh', type: { name: 'credit' } },
          { date: '2026-09-01T00:00:01.000Z', amount: 5, note: 'zu spät', type: { name: 'credit' } },
          { date: '2026-08-20T00:00:00.000Z', amount: 5, note: 'zweitens', type: { name: 'credit' } },
          { date: '2026-08-01T00:00:00.000Z', amount: 5, note: 'erstens', type: { name: 'credit' } },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows.map((row) => row.note)).toEqual(['erstens', 'zweitens']);
  });

  it('returns an empty array when nothing falls in range', () => {
    const rows = buildRows(team() as any, '2026-08-01', '2026-08-31');
    expect(rows).toEqual([]);
  });

  it('skips transactions with null type and includes valid ones', () => {
    const rows = buildRows(
      team({
        transactions: [
          { date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Valid credit', type: { name: 'credit' } },
          { date: '2026-08-06T00:00:00.000Z', amount: 50, note: 'Null type team wallet', type: null },
        ],
        players: [
          {
            firstName: 'Bob',
            lastName: 'Smith',
            transactions: [
              { date: '2026-08-07T00:00:00.000Z', amount: 20, note: 'Valid payment', type: { name: 'payment' } },
              { date: '2026-08-08T00:00:00.000Z', amount: 30, note: 'Null type player', type: null },
            ],
          },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows).toEqual([
      { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Valid credit', amount: 100, runningTotal: 100 },
      { date: '2026-08-07T00:00:00.000Z', type: 'payment', who: 'Bob Smith', note: 'Valid payment', amount: 20, runningTotal: 120 },
    ]);
  });
});

describe('buildReceivableRows', () => {
  const team = (overrides: Partial<{ players: any[] }> = {}) => ({
    id: 5,
    name: 'Team A',
    alias: 'team-a',
    players: [],
    ...overrides,
  });

  it('includes fine, levy and fee player transactions, excluding payment and credit', () => {
    const rows = buildReceivableRows(
      team({
        players: [
          {
            firstName: 'Alex',
            lastName: 'Muster',
            transactions: [
              { date: '2026-08-03T00:00:00.000Z', amount: 10, note: 'Bar bezahlt', type: { name: 'payment' } },
              { date: '2026-08-04T00:00:00.000Z', amount: 15, note: 'Monatsbeitrag', type: { name: 'fee' } },
              { date: '2026-08-05T00:00:00.000Z', amount: 5, note: 'Zu spät', type: { name: 'fine' } },
              { date: '2026-08-06T00:00:00.000Z', amount: 20, note: 'Umlage Trikots', type: { name: 'levy' } },
            ],
          },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows).toEqual([
      { date: '2026-08-04T00:00:00.000Z', type: 'fee', who: 'Alex Muster', note: 'Monatsbeitrag', amount: 15 },
      { date: '2026-08-05T00:00:00.000Z', type: 'fine', who: 'Alex Muster', note: 'Zu spät', amount: 5 },
      { date: '2026-08-06T00:00:00.000Z', type: 'levy', who: 'Alex Muster', note: 'Umlage Trikots', amount: 20 },
    ]);
  });

  it('excludes rows outside the [from, to] range and sorts the rest chronologically', () => {
    const rows = buildReceivableRows(
      team({
        players: [
          {
            firstName: 'Bob',
            lastName: 'Smith',
            transactions: [
              { date: '2026-07-31T23:59:00.000Z', amount: 5, note: 'zu früh', type: { name: 'fine' } },
              { date: '2026-09-01T00:00:01.000Z', amount: 5, note: 'zu spät', type: { name: 'fine' } },
              { date: '2026-08-20T00:00:00.000Z', amount: 5, note: 'zweitens', type: { name: 'fee' } },
              { date: '2026-08-01T00:00:00.000Z', amount: 5, note: 'erstens', type: { name: 'levy' } },
            ],
          },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows.map((row) => row.note)).toEqual(['erstens', 'zweitens']);
  });

  it('skips transactions with null type and returns an empty array when nothing matches', () => {
    const rows = buildReceivableRows(
      team({
        players: [
          {
            firstName: 'Carla',
            lastName: 'Beispiel',
            transactions: [
              { date: '2026-08-05T00:00:00.000Z', amount: 10, note: 'Null type', type: null },
              { date: '2026-08-06T00:00:00.000Z', amount: 10, note: 'Zahlung', type: { name: 'payment' } },
            ],
          },
        ],
      }) as any,
      '2026-08-01',
      '2026-08-31',
    );

    expect(rows).toEqual([]);
  });
});

describe('buildCsv', () => {
  it('renders the header and formatted rows with German decimals', () => {
    const csv = buildCsv([
      { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 },
      { date: '2026-08-10T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Bälle', amount: -20.5, runningTotal: -10.5 },
    ]);

    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' +
        '2026-08-05;Zahlung;Alex Muster;Bar bezahlt;10,00;10,00\r\n' +
        '2026-08-10;Ausgabe;Teamkasse;Bälle;-20,50;-10,50',
    );
  });

  it('quotes notes containing a semicolon and escapes embedded quotes', () => {
    const csv = buildCsv([
      { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Spende; "danke"', amount: 5, runningTotal: 5 },
    ]);

    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' +
        '2026-08-05;Guthaben;Teamkasse;"Spende; ""danke""";5,00;5,00',
    );
  });

  it('shows a placeholder row when there are no bookings', () => {
    const csv = buildCsv([]);
    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\nKeine Buchungen im gewählten Zeitraum',
    );
  });

  it('formats negative-zero as positive zero', () => {
    const csv = buildCsv([
      { date: '2026-08-05T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Test', amount: -0.001, runningTotal: 0 },
    ]);

    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' +
        '2026-08-05;Ausgabe;Teamkasse;Test;0,00;0,00',
    );
  });

  it('rounds half-cent boundaries correctly', () => {
    const csv = buildCsv([
      { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Test', amount: 1.005, runningTotal: 1.005 },
    ]);

    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' +
        '2026-08-05;Zahlung;Alex Muster;Test;1,01;1,01',
    );
  });

  it('rounds negative half-cent boundaries correctly', () => {
    const csv = buildCsv([
      { date: '2026-08-05T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Test', amount: -1.005, runningTotal: -1.005 },
    ]);

    expect(csv).toBe(
      'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' +
        '2026-08-05;Ausgabe;Teamkasse;Test;-1,01;-1,01',
    );
  });
});

describe('buildPdf', () => {
  const cashRow = {
    date: '2026-08-05T00:00:00.000Z',
    type: 'payment',
    who: 'Alex Muster',
    note: 'Bar bezahlt',
    amount: 10,
    runningTotal: 10,
  };

  const receivableRow = {
    date: '2026-08-06T00:00:00.000Z',
    type: 'fine',
    who: 'Bob Smith',
    note: 'Zu spät',
    amount: 5,
  };

  it('produces a non-empty valid PDF buffer with cash rows only', async () => {
    const buffer = await buildPdf({ name: 'Team A' } as any, [cashRow], [], '2026-08-01', '2026-08-31');

    expect(Buffer.isBuffer(buffer)).toBe(true);
    expect(buffer.length).toBeGreaterThan(100);
    expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
  });

  it('produces a valid PDF when there are only receivable rows', async () => {
    const buffer = await buildPdf({ name: 'Team A' } as any, [], [receivableRow], '2026-08-01', '2026-08-31');

    expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
  });

  it('produces a valid PDF with both cash and receivable rows', async () => {
    const buffer = await buildPdf(
      { name: 'Team A' } as any,
      [cashRow],
      [receivableRow],
      '2026-08-01',
      '2026-08-31',
    );

    expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
  });

  it('still produces a valid PDF when both sections are empty', async () => {
    const buffer = await buildPdf({ name: 'Team A' } as any, [], [], '2026-08-01', '2026-08-31');

    expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
    expect(pdfPageCount(buffer)).toBe(1);
  });

  it('does not append blank trailing pages when content fits on a single page', async () => {
    const buffer = await buildPdf(
      { name: 'Team A' } as any,
      [cashRow],
      [receivableRow],
      '2026-08-01',
      '2026-08-31',
    );

    expect(pdfPageCount(buffer)).toBe(1);
  });

  it('paginates correctly and stays a valid PDF for many rows', async () => {
    const manyRows = Array.from({ length: 60 }, (_, i) => ({
      ...cashRow,
      date: `2026-08-${String((i % 28) + 1).padStart(2, '0')}T00:00:00.000Z`,
      note: `Buchung ${i}`,
      runningTotal: 10 * (i + 1),
    }));
    const manyReceivables = Array.from({ length: 60 }, (_, i) => ({
      ...receivableRow,
      date: `2026-08-${String((i % 28) + 1).padStart(2, '0')}T00:00:00.000Z`,
      note: `Forderung ${i}`,
    }));

    const buffer = await buildPdf(
      { name: 'Team A' } as any,
      manyRows,
      manyReceivables,
      '2026-08-01',
      '2026-08-31',
    );

    expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-');
    expect(buffer.length).toBeGreaterThan(2000);
    // Guards against the footer loop reintroducing blank trailing pages: with
    // the bug, this dataset produced 12 pages (3x the real content pages).
    expect(pdfPageCount(buffer)).toBe(4);
  });
});
