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 { Router, provideRouter } from '@angular/router';
import { Subject, of, throwError } from 'rxjs';
import { AuthStore } from '../../../../core/auth/auth-store';
import { PenaltyApi } from '../../../../core/team/penalty-api';
import { TeamStore } from '../../../../core/team/team-store';
import { Penalty } from '../../../../models/penalty.model';
import { Penalties } from './penalties';

const first: Penalty = {
  id: 1,
  description: 'Zu spät',
  amount: 5,
  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('Penalties', () => {
  let fixture: ComponentFixture<Penalties>;
  let currentUser: ReturnType<typeof signal<{ id: number; role: { id: number } }>>;
  let loadPenalties: ReturnType<typeof vi.fn>;
  let createPenalty: ReturnType<typeof vi.fn>;
  let updatePenalty: ReturnType<typeof vi.fn>;
  let deletePenalty: ReturnType<typeof vi.fn>;
  let dialogClosed: Subject<boolean>;
  let dialog: { open: ReturnType<typeof vi.fn> };

  beforeEach(async () => {
    currentUser = signal({ id: 42, role: { id: 2 } });
    loadPenalties = vi.fn(() => of([first]));
    createPenalty = vi.fn(() => of({ id: 2, description: 'Handy', amount: 3 }));
    updatePenalty = vi.fn(() => of({ ...first, description: 'Neu', amount: 6 }));
    deletePenalty = vi.fn(() => of(undefined));
    dialogClosed = new Subject<boolean>();
    dialog = { open: vi.fn(() => ({ afterClosed: () => dialogClosed.asObservable() })) };

    await TestBed.configureTestingModule({
      imports: [Penalties],
      providers: [
        provideRouter([]),
        { provide: TeamStore, useValue: { team: signal(team) } },
        { provide: AuthStore, useValue: { currentUser } },
        {
          provide: PenaltyApi,
          useValue: { loadPenalties, createPenalty, updatePenalty, deletePenalty },
        },
        { provide: MatDialog, useValue: dialog },
      ],
    }).compileComponents();
  });

  function create(): void {
    fixture = TestBed.createComponent(Penalties);
    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 the catalog without mutation controls to a reader', () => {
    currentUser.set({ id: 7, role: { id: 2 } });
    create();

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

  it('lets a captain create an entry and reloads authoritative data', () => {
    loadPenalties
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(of([first, { id: 2, description: 'Handy', amount: 3 }]));
    create();
    fixture.componentInstance['form'].setValue({ description: 'Handy', amount: 3 });
    expect(text()).toContain('Strafenkatalog richtig nutzen');

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

    expect(createPenalty).toHaveBeenCalledWith({ teamId: 5, description: 'Handy', amount: 3 });
    expect(loadPenalties).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Handy');
  });

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

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

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

  it('opens one inline editor, supports cancel, and saves pessimistically', () => {
    const updateResult = new Subject<Penalty>();
    updatePenalty.mockReturnValue(updateResult);
    loadPenalties
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(of([{ ...first, description: 'Neu', amount: 6 }]));
    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'].setValue({ description: 'Neu', amount: 6 });
    fixture.componentInstance['savePenalty'](first);
    fixture.detectChanges();
    expect(fixture.componentInstance['editingPenaltyId']()).toBe(first.id);
    expect(fixture.componentInstance['penalties']()).toEqual([first]);
    expect(loadPenalties).toHaveBeenCalledTimes(1);

    updateResult.next({ ...first, description: 'Neu', amount: 6 });
    fixture.detectChanges();
    expect(updatePenalty).toHaveBeenCalledWith(1, { description: 'Neu', amount: 6 });
    expect(loadPenalties).toHaveBeenCalledTimes(2);
    expect(text()).toContain('Neu');
  });

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

    button('Löschen').click();
    expect(dialog.open).toHaveBeenCalled();
    dialogClosed.next(true);
    expect(deletePenalty).toHaveBeenCalledWith(1);
    expect(loadPenalties).toHaveBeenCalledTimes(1);
    expect(text()).toContain('Zu spät');

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

  it('keeps the inline editor and explains a duplicate conflict', () => {
    updatePenalty.mockReturnValue(
      throwError(
        () =>
          new HttpErrorResponse({
            status: 409,
            error: { message: 'Ein Eintrag mit dieser Beschreibung existiert bereits.' },
          }),
      ),
    );
    create();
    button('Bearbeiten').click();
    fixture.componentInstance['editForm'].setValue({ description: 'Zu spät', amount: 6 });

    fixture.componentInstance['savePenalty'](first);
    fixture.detectChanges();

    expect((fixture.nativeElement as HTMLElement).querySelector('.edit-form')).not.toBeNull();
    expect(text()).toContain('existiert bereits');
  });

  it('distinguishes a successful update from a failed authoritative reload', () => {
    loadPenalties
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 500 })));
    create();
    button('Bearbeiten').click();
    fixture.componentInstance['editForm'].setValue({ description: 'Neu', amount: 6 });

    fixture.componentInstance['savePenalty'](first);
    fixture.detectChanges();

    expect(fixture.componentInstance['editingPenaltyId']()).toBeNull();
    expect(fixture.componentInstance['mutationError']()).toBeNull();
    expect(text()).toContain('Änderung wurde gespeichert');
    expect(text()).toContain('Erneut versuchen');
  });

  it('clears the create form after success even when the reload fails', () => {
    loadPenalties
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 500 })));
    create();
    fixture.componentInstance['form'].setValue({ description: 'Handy', amount: 3 });

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

    expect(fixture.componentInstance['form'].getRawValue()).toEqual({
      description: '',
      amount: 0,
    });
    expect(fixture.componentInstance['mutationError']()).toBeNull();
    expect(text()).toContain('Änderung wurde gespeichert');
  });

  it('reports a reload problem instead of a delete failure after successful deletion', () => {
    loadPenalties
      .mockReturnValueOnce(of([first]))
      .mockReturnValueOnce(throwError(() => new HttpErrorResponse({ status: 500 })));
    create();

    button('Löschen').click();
    dialogClosed.next(true);
    fixture.detectChanges();

    expect(deletePenalty).toHaveBeenCalledWith(first.id);
    expect(fixture.componentInstance['mutationError']()).toBeNull();
    expect(text()).toContain('Änderung wurde gespeichert');
  });

  it('prevents overlapping catalog mutations', () => {
    const creation = new Subject<Penalty>();
    createPenalty.mockReturnValue(creation);
    create();
    fixture.componentInstance['form'].setValue({ description: 'Handy', amount: 3 });

    fixture.componentInstance['createPenalty']();
    fixture.componentInstance['startEdit'](first);
    fixture.componentInstance['confirmDelete'](first);

    expect(fixture.componentInstance['editingPenaltyId']()).toBeNull();
    expect(dialog.open).not.toHaveBeenCalled();
  });

  it('shows a "Buchen" button to a 2. Kassenwart who cannot manage the catalog, and navigates to the cashbox with the penalty preselected', () => {
    team.players[0].teamRole.id = 2;
    create();

    expect(text()).not.toContain('Bearbeiten');
    const router = TestBed.inject(Router);
    const navigateSpy = vi.spyOn(router, 'navigate');

    button('Buchen').click();

    expect(navigateSpy).toHaveBeenCalledWith(['/team', 5, 'cashbox'], {
      queryParams: { penaltyId: 1 },
    });
    team.players[0].teamRole.id = 3;
  });

  it('hides the "Buchen" button from a reader without booking rights', () => {
    currentUser.set({ id: 7, role: { id: 2 } });
    create();

    expect(text()).not.toContain('Buchen');
  });

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

    button('Erneut versuchen').click();
    fixture.detectChanges();
    expect(text()).toContain('Zu spät');

    fixture.componentInstance['search'].set('Nicht vorhanden');
    fixture.detectChanges();
    expect(text()).toContain('Keine passenden Einträge');
  });
});
