import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { AuthStore } from '../../../core/auth/auth-store';
import { PenaltyApi } from '../../../core/team/penalty-api';
import { TeamStore } from '../../../core/team/team-store';
import { TransactionsApi } from '../../../core/team/transactions-api';
import { Cashbox } from './cashbox';

describe('Cashbox', () => {
  const team = {
    id: 5,
    name: 'Team A',
    alias: 'team-a',
    balance: 120,
    players: [
      {
        id: 1,
        firstName: 'Alex',
        lastName: 'Muster',
        balance: -12,
        active: true,
        teamRole: { id: 2, name: 'scnd_treasurer' },
        user: { id: 42, email: 'alex@example.com', firstName: 'Alex', lastName: 'Muster' },
      },
      {
        id: 2,
        firstName: 'Bea',
        lastName: 'Test',
        balance: 5,
        active: true,
        teamRole: { id: 1, name: 'player' },
      },
      {
        id: 3,
        firstName: 'Chris',
        lastName: 'Drittel',
        balance: 0,
        active: true,
        teamRole: { id: 1, name: 'player' },
      },
    ],
  };

  const activities = [
    {
      id: 9,
      date: '2026-07-31T10:00:00.000Z',
      amount: 12,
      type: 'payment',
      note: 'Beitrag',
      playerName: 'Bea Test',
      isTeamWalletTransaction: false,
    },
    {
      id: 10,
      date: '2026-07-30T10:00:00.000Z',
      amount: 8,
      type: 'expense',
      note: 'Material',
      isTeamWalletTransaction: true,
    },
    {
      id: 11,
      date: '2026-07-29T10:00:00.000Z',
      amount: 5,
      type: 'fine',
      note: 'Training',
      playerName: 'Alex Muster',
      isTeamWalletTransaction: false,
    },
  ];

  const penalties = [
    { id: 101, description: 'Zu spät zum Training', amount: 5 },
    { id: 102, description: 'Handy vergessen', amount: 2.5 },
  ];

  async function setup(roleId = 2, confirm = true, penaltyIdParam: string | null = null) {
    const createPlayerTransactions = vi.fn(() => of([]));
    const createTeamWalletTransaction = vi.fn(() => of(activities[0]));
    const reverseTransaction = vi.fn(() => of(activities[0]));
    const loadTeamTransactions = vi.fn(() => of(activities));
    const loadTeamTransactionsJournal = vi.fn(() =>
      of({ data: activities, total: activities.length }),
    );
    const loadPenalties = vi.fn(() => of(penalties));
    const refreshTeam = vi.fn();
    const dialog = {
      open: vi.fn(() => ({ afterClosed: () => of(confirm) })),
    };

    await TestBed.configureTestingModule({
      imports: [Cashbox],
      providers: [
        {
          provide: AuthStore,
          useValue: {
            currentUser: signal({
              id: 42,
              email: 'alex@example.com',
              firstName: 'Alex',
              lastName: 'Muster',
              role: { id: roleId === 99 ? 1 : 2 },
            }),
          },
        },
        {
          provide: TeamStore,
          useValue: { team: signal(team), loading: signal(false), refreshTeam },
        },
        {
          provide: TransactionsApi,
          useValue: {
            loadTeamTransactions,
            loadTeamTransactionsJournal,
            createPlayerTransactions,
            createTeamWalletTransaction,
            reverseTransaction,
          },
        },
        { provide: PenaltyApi, useValue: { loadPenalties } },
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: {
              queryParamMap: convertToParamMap(penaltyIdParam ? { penaltyId: penaltyIdParam } : {}),
            },
          },
        },
        { provide: MatDialog, useValue: dialog },
      ],
    }).compileComponents();

    if (roleId === 1) {
      team.players[0].teamRole.id = 1;
    } else {
      team.players[0].teamRole.id = 2;
    }

    const fixture = TestBed.createComponent(Cashbox);
    fixture.detectChanges();
    return {
      fixture,
      component: fixture.componentInstance,
      createPlayerTransactions,
      loadTeamTransactionsJournal,
      reverseTransaction,
      dialog,
      refreshTeam,
    };
  }

  it('shows the Kassenjournal grid and booking controls to a treasurer', async () => {
    const { fixture } = await setup();

    expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).not.toBeNull();
    expect(fixture.nativeElement.textContent).toContain('Buchungen verstehen');
    expect(fixture.nativeElement.textContent).toContain('Alle Buchungen');
    expect(fixture.nativeElement.querySelector('ag-grid-angular')).not.toBeNull();
  });

  // AG Grid virtualizes rows based on real layout measurements (container
  // height, ResizeObserver) that jsdom doesn't provide, so it never actually
  // requests rows in this environment - row/cell content is verified
  // manually in the browser (see AmountCellRenderer/ReverseActionCellRenderer
  // specs for the cell logic in isolation). Here we call the grid's
  // datasource factory directly to verify the query it builds.
  it('builds a journal datasource that requests page 1 sorted by date descending', async () => {
    const { component, loadTeamTransactionsJournal } = await setup();
    const successCallback = vi.fn();

    const datasource = component['buildJournalDatasource'](5);
    datasource.getRows({
      startRow: 0,
      endRow: 25,
      sortModel: [],
      filterModel: {},
      successCallback,
      failCallback: vi.fn(),
    } as unknown as Parameters<typeof datasource.getRows>[0]);

    expect(loadTeamTransactionsJournal).toHaveBeenCalledWith(
      5,
      expect.objectContaining({ page: 1, limit: 25, sortBy: 'date', sortDir: 'desc' }),
    );
    expect(successCallback).toHaveBeenCalledWith(activities, activities.length);
  });

  it('maps the grid sort model onto the journal query for a different column', async () => {
    const { component, loadTeamTransactionsJournal } = await setup();

    const datasource = component['buildJournalDatasource'](5);
    datasource.getRows({
      startRow: 25,
      endRow: 50,
      sortModel: [{ colId: 'amount', sort: 'asc' }],
      filterModel: {},
      successCallback: vi.fn(),
      failCallback: vi.fn(),
    } as unknown as Parameters<typeof datasource.getRows>[0]);

    expect(loadTeamTransactionsJournal).toHaveBeenCalledWith(
      5,
      expect.objectContaining({ page: 2, limit: 25, sortBy: 'amount', sortDir: 'asc' }),
    );
  });

  it('submits cent-preserving split transactions for selected players', async () => {
    const { component, createPlayerTransactions, refreshTeam } = await setup();
    component['playerForm'].setValue({
      playerIds: [1, 2, 3],
      amount: 10,
      type: 11,
      note: 'Training',
      date: '2026-07-31',
      total: true,
    });

    component['submitPlayerBooking']();

    expect(createPlayerTransactions).toHaveBeenCalledWith([
      expect.objectContaining({ playerId: 1, amount: 3.34, type: 11 }),
      expect.objectContaining({ playerId: 2, amount: 3.33, type: 11 }),
      expect.objectContaining({ playerId: 3, amount: 3.33, type: 11 }),
    ]);
    expect(refreshTeam).toHaveBeenCalled();
  });

  it('asks for confirmation before booking a high amount', async () => {
    const { component, createPlayerTransactions, dialog } = await setup(2, false);
    component['playerForm'].setValue({
      playerIds: [1],
      amount: 300,
      type: 11,
      note: '',
      date: '2026-07-31',
      total: false,
    });

    component['submitPlayerBooking']();

    expect(dialog.open).toHaveBeenCalled();
    expect(createPlayerTransactions).not.toHaveBeenCalled();
  });

  it('does not show mutation controls to a regular member', async () => {
    const { fixture } = await setup(1);

    expect(fixture.nativeElement.querySelector('[data-testid="player-booking"]')).toBeNull();
    expect(fixture.nativeElement.querySelector('[data-testid="reverse-booking"]')).toBeNull();
  });

  it('prefills amount, note and type from a manually selected catalog entry, and stays editable', async () => {
    const { component } = await setup();

    component['onPenaltySelect'](102);

    expect(component['playerForm'].getRawValue()).toEqual(
      expect.objectContaining({ amount: 2.5, note: 'Handy vergessen', type: 11 }),
    );

    component['playerForm'].patchValue({ amount: 7 });
    expect(component['playerForm'].getRawValue().amount).toBe(7);
  });

  it('prefills the booking form automatically from a penaltyId query param', async () => {
    const { component } = await setup(2, true, '101');

    expect(component['playerForm'].getRawValue()).toEqual(
      expect.objectContaining({ amount: 5, note: 'Zu spät zum Training', type: 11 }),
    );
  });

  it('ignores an unknown penaltyId query param without error', async () => {
    const { component } = await setup(2, true, '999');

    expect(component['playerForm'].getRawValue()).toEqual(
      expect.objectContaining({ amount: 0, note: '', type: 11 }),
    );
  });
});
