import { HttpErrorResponse } from '@angular/common/http';
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { provideRouter } from '@angular/router';
import { Subject, of, throwError } from 'rxjs';
import { AuthStore } from '../../../../core/auth/auth-store';
import { RecurringTransactionApi } from '../../../../core/team/recurring-transaction-api';
import { TeamStore } from '../../../../core/team/team-store';
import { RecurringTransaction } from '../../../../models/recurring-transaction.model';
import { RecurringTransactions } from './recurring-transactions';

const first: RecurringTransaction = {
  id: 1,
  description: 'Monatsbeitrag',
  amount: 10,
  type: 13,
  interval: 'monthly',
  nextRunDate: '2026-09-01T00:00:00.000Z',
  active: true,
  createdAt: '2026-01-01T00:00:00.000Z',
};
const team = {
  id: 5,
  name: 'Team A',
  alias: 'a',
  balance: 0,
  players: [
    {
      id: 1,
      firstName: 'Alex',
      lastName: 'Muster',
      balance: 0,
      active: true,
      teamRole: { id: 3 },
      user: { id: 42 },
    },
  ],
};

describe('RecurringTransactions', () => {
  let fixture: ComponentFixture<RecurringTransactions>;
  let currentUser: ReturnType<typeof signal<{ id: number; role: { id: number } }>>;
  let loadRecurringTransactions: ReturnType<typeof vi.fn>;
  let createRecurringTransaction: ReturnType<typeof vi.fn>;
  let updateRecurringTransaction: ReturnType<typeof vi.fn>;
  let deleteRecurringTransaction: ReturnType<typeof vi.fn>;
  let dialogClosed: Subject<boolean>;
  let dialog: { open: ReturnType<typeof vi.fn> };

  beforeEach(async () => {
    currentUser = signal({ id: 42, role: { id: 2 } });
    loadRecurringTransactions = vi.fn(() => of([first]));
    createRecurringTransaction = vi.fn(() =>
      of({ ...first, id: 2, description: 'Ausrüstung' }),
    );
    updateRecurringTransaction = vi.fn(() => of({ ...first, description: 'Neu', amount: 12 }));
    deleteRecurringTransaction = vi.fn(() => of(undefined));
    dialogClosed = new Subject<boolean>();
    dialog = { open: vi.fn(() => ({ afterClosed: () => dialogClosed.asObservable() })) };

    await TestBed.configureTestingModule({
      imports: [RecurringTransactions],
      providers: [
        provideRouter([]),
        { provide: TeamStore, useValue: { team: signal(team) } },
        { provide: AuthStore, useValue: { currentUser } },
        {
          provide: RecurringTransactionApi,
          useValue: {
            loadRecurringTransactions,
            createRecurringTransaction,
            updateRecurringTransaction,
            deleteRecurringTransaction,
          },
        },
        { provide: MatDialog, useValue: dialog },
      ],
    }).compileComponents();
  });

  function create(): void {
    fixture = TestBed.createComponent(RecurringTransactions);
    fixture.detectChanges();
  }

  function text(): string {
    return (fixture.nativeElement as HTMLElement).textContent ?? '';
  }

  function button(label: string): HTMLButtonElement {
    const match = [...(fixture.nativeElement as HTMLElement).querySelectorAll('button')].find(
      (element) => element.textContent?.includes(label),
    );
    if (!match) throw new Error(`Missing button: ${label}`);
    return match as HTMLButtonElement;
  }

  it('shows entries without mutation controls to a reader', () => {
    currentUser.set({ id: 7, role: { id: 2 } });
    create();

    expect(text()).toContain('Monatsbeitrag');
    expect((fixture.nativeElement as HTMLElement).querySelector('.create-card')).toBeNull();
    expect(text()).not.toContain('Bearbeiten');
    expect(text()).not.toContain('Löschen');
  });

  it('lets an authorized user create an entry and reloads authoritative data', () => {
    loadRecurringTransactions
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(of([first, { ...first, id: 2, description: 'Ausrüstung' }]));
    create();
    fixture.componentInstance['form'].setValue({
      description: 'Ausrüstung',
      amount: 20,
      type: 12,
      interval: 'yearly',
      startDate: '2026-09-01',
    });

    fixture.componentInstance['createEntry']();
    fixture.detectChanges();

    expect(createRecurringTransaction).toHaveBeenCalledWith({
      teamId: 5,
      description: 'Ausrüstung',
      amount: 20,
      type: 12,
      interval: 'yearly',
      startDate: new Date('2026-09-01T12:00:00').toISOString(),
    });
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Ausrüstung');
  });

  it('rejects blank descriptions and amounts with more than two decimals', () => {
    create();

    fixture.componentInstance['form'].patchValue({ description: '   ', amount: 1.234 });

    expect(fixture.componentInstance['form'].invalid).toBe(true);
    fixture.componentInstance['createEntry']();
    expect(createRecurringTransaction).not.toHaveBeenCalled();
  });

  it('opens one inline editor, supports cancel, and saves pessimistically', () => {
    const updateResult = new Subject<RecurringTransaction>();
    updateRecurringTransaction.mockReturnValue(updateResult);
    loadRecurringTransactions
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(of([{ ...first, description: 'Neu', amount: 12 }]));
    create();

    button('Bearbeiten').click();
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).not.toBeNull();
    fixture.componentInstance['cancelEdit']();
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).toBeNull();

    button('Bearbeiten').click();
    fixture.componentInstance['editForm'].patchValue({ description: 'Neu', amount: 12 });
    fixture.componentInstance['saveEntry'](first);
    fixture.detectChanges();
    expect(fixture.componentInstance['editingEntryId']()).toBe(first.id);
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(1);

    updateResult.next({ ...first, description: 'Neu', amount: 12 });
    fixture.detectChanges();
    expect(updateRecurringTransaction).toHaveBeenCalledWith(1, {
      description: 'Neu',
      amount: 12,
      type: 13,
      interval: 'monthly',
      active: true,
    });
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Neu');
  });

  it('pauses and resumes an entry via the active toggle', () => {
    const paused = { ...first, active: false };
    updateRecurringTransaction.mockReturnValue(of(paused));
    loadRecurringTransactions
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(of([paused]));
    create();

    button('Pausieren').click();
    fixture.detectChanges();

    expect(updateRecurringTransaction).toHaveBeenCalledWith(1, {
      description: 'Monatsbeitrag',
      amount: 10,
      type: 13,
      interval: 'monthly',
      active: false,
    });
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Fortsetzen');
  });

  it('confirms deletion and reloads only after server success', () => {
    const deletion = new Subject<void>();
    deleteRecurringTransaction.mockReturnValue(deletion);
    loadRecurringTransactions.mockReturnValueOnce(of([first])).mockReturnValueOnce(of([]));
    create();

    button('Löschen').click();
    expect(dialog.open).toHaveBeenCalled();
    dialogClosed.next(true);
    expect(deleteRecurringTransaction).toHaveBeenCalledWith(1);
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(1);

    deletion.next();
    fixture.detectChanges();
    expect(loadRecurringTransactions).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Noch keine Einträge');
  });

  it('shows a load error and retries', () => {
    loadRecurringTransactions
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 500 })))
      .mockReturnValueOnce(of([first]));
    create();
    fixture.detectChanges();
    expect(text()).toContain('konnte nicht geladen werden');

    button('Erneut versuchen').click();
    fixture.detectChanges();
    expect(text()).toContain('Monatsbeitrag');
  });
});
