import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } 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 } from 'rxjs';
import { environment } from '../../../environments/environment';
import { AuthStore } from '../../core/auth/auth-store';
import { AdminPlayerPage, AdminUserDirectorySummary, UserDirectoryPage } from '../../models/user-directory.model';
import { Users } from './users';

const api = `${environment.apiUrl}users/directory`;
const adminApi = `${environment.apiUrl}admin/users`;

const ada: AdminUserDirectorySummary = {
  id: 7,
  firstName: 'Ada',
  lastName: 'Lovelace',
  email: 'ada@example.test',
  role: { id: 2, name: 'User' },
  status: { id: 1, name: 'Active' },
  assignments: [
    {
      id: 101,
      firstName: 'Ada',
      lastName: 'Lovelace',
      active: true,
      team: { id: 5, name: 'First Team', alias: 'first' },
      teamRole: { id: 1, name: 'player' },
    },
  ],
};

const admin: AdminUserDirectorySummary = {
  id: 1,
  firstName: 'Grace',
  lastName: 'Admin',
  email: 'grace@example.test',
  role: { id: 1, name: 'Admin' },
  status: { id: 1, name: 'Active' },
  assignments: [],
};

function directoryPage(data = [ada], page = 1, total = data.length, hasNextPage = false): UserDirectoryPage {
  return { data, page, limit: 20, total, hasNextPage };
}

function playersPage(overrides: Partial<AdminPlayerPage> = {}): AdminPlayerPage {
  return {
    data: [
      {
        id: 202,
        firstName: 'Linus',
        lastName: 'Player',
        active: true,
        team: { id: 6, name: 'Second Team', alias: 'second' },
        currentUser: null,
      },
    ],
    page: 1,
    limit: 20,
    total: 1,
    hasNextPage: false,
    ...overrides,
  };
}

describe('Users directory', () => {
  let fixture: ComponentFixture<Users>;
  let http: HttpTestingController;
  let isAdmin: ReturnType<typeof signal<boolean>>;
  let currentUser: ReturnType<typeof signal<{ id: number; firstName: string; lastName: string; role: { id: number } }>>;
  let updateUser: ReturnType<typeof vi.fn>;
  let closeDialog: Subject<boolean>;
  let dialog: { open: ReturnType<typeof vi.fn> };

  beforeEach(async () => {
    isAdmin = signal(false);
    currentUser = signal({ id: 99, firstName: 'Nora', lastName: 'Viewer', role: { id: 2 } });
    updateUser = vi.fn();
    closeDialog = new Subject<boolean>();
    dialog = { open: vi.fn(() => ({ afterClosed: () => closeDialog.asObservable() })) };

    await TestBed.configureTestingModule({
      imports: [Users],
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        provideRouter([]),
        { provide: AuthStore, useValue: { isGlobalAdmin: isAdmin, currentUser, updateUser } },
        { provide: MatDialog, useValue: dialog },
      ],
    }).compileComponents();

    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => http.verify({ ignoreCancelled: true }));

  function create(): void {
    fixture = TestBed.createComponent(Users);
    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('renders unknown role and status values explicitly', () => {
    create();
    flushDirectory();

    expect(fixture.componentInstance['statusName']()).toBe('Unbekannt');
    expect(fixture.componentInstance['statusName'](99)).toBe('Unbekannt');
    expect(fixture.componentInstance['roleName']()).toBe('Unbekannt');
    expect(fixture.componentInstance['roleName'](99)).toBe('Unbekannt');
  });

  it('persists returned names when an admin edits their own profile', () => {
    isAdmin.set(true);
    currentUser.set({ id: 7, firstName: 'Ada', lastName: 'Lovelace', role: { id: 1 } });
    create();
    flushDirectory(directoryPage([admin, ada]));

    fixture.componentInstance['saveEdit'](ada, {
      firstName: 'Augusta',
      lastName: 'King',
      role: 2,
    });
    http.expectOne(`${adminApi}/7/profile`).flush({
      ...ada,
      firstName: 'Augusta',
      lastName: 'King',
    });
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());

    expect(updateUser).toHaveBeenCalledWith({
      id: 7,
      firstName: 'Augusta',
      lastName: 'King',
      role: { id: 1 },
    });
  });


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

  function flushDirectory(page = directoryPage()): void {
    http.expectOne(`${api}?page=1&limit=20`).flush(page);
    fixture.detectChanges();
  }

  it('loads the first directory page, searches from page one, and pages forward', () => {
    create();
    flushDirectory(directoryPage([ada], 1, 12, true));

    expect(text()).toContain('Ada Lovelace');
    expect(text()).toContain('First Team');
    expect(text()).toContain('Aktiv');

    const search = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[type="search"]')!;
    search.value = '  Linus  ';
    search.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    search.closest('form')!.dispatchEvent(new Event('submit'));
    fixture.detectChanges();
    http.expectOne(`${api}?page=1&limit=20&search=Linus`).flush(directoryPage([], 1));
    fixture.detectChanges();

    search.value = '';
    search.dispatchEvent(new Event('input'));
    fixture.detectChanges();
    search.closest('form')!.dispatchEvent(new Event('submit'));
    fixture.detectChanges();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([ada], 1, 12, true));
    fixture.detectChanges();
    button('Weiter').click();
    fixture.detectChanges();
    http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([admin], 2, 12, false));
  });

  it('cancels an older directory load when a newer search starts', () => {
    create();
    const older = http.expectOne(`${api}?page=1&limit=20`);
    const search = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[type="search"]')!;
    search.value = 'Grace';
    search.dispatchEvent(new Event('input'));
    search.closest('form')!.dispatchEvent(new Event('submit'));
    const newer = http.expectOne(`${api}?page=1&limit=20&search=Grace`);

    const olderWasCancelled = older.cancelled;
    newer.flush(directoryPage([admin]));
    if (!olderWasCancelled) older.flush(directoryPage([ada]));
    fixture.detectChanges();

    expect(olderWasCancelled).toBe(true);
    expect(text()).toContain('Grace Admin');
    expect(text()).not.toContain('Ada Lovelace');
  });

  it('redacts admin-only data and controls for a non-admin even if extra fields arrive', () => {
    create();
    flushDirectory(directoryPage([ada]));

    expect(text()).toContain('Ada Lovelace');
    expect(text()).not.toContain('ada@example.test');
    expect(text()).not.toContain('Globale Rolle');
    expect(text()).not.toContain('Bearbeiten');
    expect(text()).not.toContain('Zuordnungen verwalten');
  });

  it('shows admin fields and actions while disabling self-demotion and self-deactivation', () => {
    isAdmin.set(true);
    currentUser.set({ id: 1, firstName: 'Grace', lastName: 'Admin', role: { id: 1 } });
    create();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([admin, ada]));
    fixture.detectChanges();

    expect(text()).toContain('ada@example.test');
    expect(text()).toContain('Administrator');
    expect(text()).toContain('Bearbeiten');
    expect(text()).toContain('Zuordnungen verwalten');
    const selfRow = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('[data-user-id="1"]')!;
    const selfButtons = [...selfRow.querySelectorAll('button')];
    expect(selfButtons.find((item) => item.textContent?.includes('Deaktivieren'))?.disabled).toBe(true);
    selfButtons.find((item) => item.textContent?.includes('Bearbeiten'))?.click();
    fixture.detectChanges();
    expect(selfRow.querySelector<HTMLSelectElement>('select[name="role"]')?.disabled).toBe(true);
  });

  it('edits a profile and role pessimistically, then reloads the directory', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Bearbeiten').click();
    fixture.detectChanges();

    const firstName = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="firstName"]')!;
    const lastName = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="lastName"]')!;
    const role = (fixture.nativeElement as HTMLElement).querySelector<HTMLSelectElement>('select[name="role"]')!;
    firstName.value = 'Augusta';
    firstName.dispatchEvent(new Event('input'));
    lastName.value = 'King';
    lastName.dispatchEvent(new Event('input'));
    role.value = '1';
    role.dispatchEvent(new Event('change'));
    firstName.closest('form')!.dispatchEvent(new Event('submit'));
    fixture.detectChanges();

    const profile = http.expectOne(`${adminApi}/7/profile`);
    expect(profile.request.body).toEqual({ firstName: 'Augusta', lastName: 'King' });
    expect(text()).toContain('Wird gespeichert');
    profile.flush(ada);
    const roleRequest = http.expectOne(`${adminApi}/7/role`);
    expect(roleRequest.request.body).toEqual({ role: 1 });
    roleRequest.flush(ada);
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([{ ...ada, firstName: 'Augusta', lastName: 'King' }]));
    fixture.detectChanges();
    expect(text()).toContain('Augusta King');
  });

  it('keeps the editor draft while saving input changes and on an initial profile failure', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Bearbeiten').click();
    fixture.detectChanges();
    const firstName = (fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="firstName"]')!;
    firstName.value = 'Operator draft';
    firstName.dispatchEvent(new Event('input'));
    firstName.closest('form')!.dispatchEvent(new Event('submit'));
    fixture.detectChanges();

    expect(firstName.value).toBe('Operator draft');
    http.expectOne(`${adminApi}/7/profile`).flush(
      { message: 'Profile rejected' },
      { status: 400, statusText: 'Bad Request' },
    );
    http.expectNone(`${api}?page=1&limit=20`);
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector<HTMLInputElement>('input[name="firstName"]')!.value).toBe(
      'Operator draft',
    );
    expect(text()).toContain('Profile rejected');
  });

  it('reloads authoritative directory state when a role change fails after the profile was saved', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Bearbeiten').click();
    fixture.detectChanges();
    const role = (fixture.nativeElement as HTMLElement).querySelector<HTMLSelectElement>('select[name="role"]')!;
    role.value = '1';
    role.dispatchEvent(new Event('change'));
    role.closest('form')!.dispatchEvent(new Event('submit'));

    http.expectOne(`${adminApi}/7/profile`).flush(ada);
    http.expectOne(`${adminApi}/7/role`).flush(
      { message: 'At least one active admin must remain' },
      { status: 409, statusText: 'Conflict' },
    );
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
    fixture.detectChanges();
    expect(text()).toContain('At least one active admin must remain');
  });

  it('confirms status changes before mutating and refreshes after success', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Deaktivieren').click();

    expect(dialog.open).toHaveBeenCalled();
    expect(dialog.open.mock.calls[0][1].data.message).toContain('Ada Lovelace');
    http.expectNone(`${adminApi}/7/status`);
    closeDialog.next(true);
    const request = http.expectOne(`${adminApi}/7/status`);
    expect(request.request.body).toEqual({ status: 2 });
    fixture.detectChanges();
    expect(text()).toContain('Status wird geändert');
    expect((fixture.nativeElement as HTMLElement).querySelector('[data-user-id="7"]')?.getAttribute('aria-busy')).toBe('true');
    request.flush(ada);
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([{ ...ada, status: { id: 2, name: 'Inactive' } }]));
  });

  it('searches players from page one after paging and assigns before refreshing both lists', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
    expect(panel.textContent).toContain('Spieler werden geladen');
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage({ hasNextPage: true, total: 21 }));
    fixture.detectChanges();
    expect(panel.querySelector('nav[aria-label="Spielerseiten"]')).not.toBeNull();
    childButton(panel, 'Weiter').click();
    http.expectOne(`${adminApi}/players?assignment=all&page=2&limit=20`).flush(playersPage({ page: 2 }));
    fixture.detectChanges();
    const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
    playerSearch.value = 'Linus';
    playerSearch.dispatchEvent(new Event('input'));
    playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
    http.expectOne(`${adminApi}/players?search=Linus&assignment=all&page=1&limit=20`).flush(playersPage());
    fixture.detectChanges();
    expect(text()).toContain('Linus Player');
    expect(text()).toContain('Second Team');

    childButton(panel, 'Zuordnen').click();
    const assign = http.expectOne(`${adminApi}/7/players/202`);
    expect(assign.request.method).toBe('PUT');
    assign.flush(ada);
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
    http.expectOne(`${adminApi}/players?search=Linus&assignment=all&page=1&limit=20`).flush(playersPage());
  });

  it('cancels an older player load when a newer player search starts', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    const older = http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`);
    const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
    const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
    playerSearch.value = 'New';
    playerSearch.dispatchEvent(new Event('input'));
    playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
    const newer = http.expectOne(`${adminApi}/players?search=New&assignment=all&page=1&limit=20`);

    const olderWasCancelled = older.cancelled;
    newer.flush(playersPage({ data: [{ ...playersPage().data[0], firstName: 'New' }] }));
    if (!olderWasCancelled) older.flush(playersPage());
    fixture.detectChanges();

    expect(olderWasCancelled).toBe(true);
    expect(panel.textContent).toContain('New Player');
    expect(panel.textContent).not.toContain('Linus Player');
  });

  it('clears stale player rows on load errors and retries with load-specific copy', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
    fixture.detectChanges();
    const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
    expect(panel.textContent).toContain('Linus Player');
    const playerSearch = panel.querySelector<HTMLInputElement>('input[type="search"]')!;
    playerSearch.value = 'broken';
    playerSearch.dispatchEvent(new Event('input'));
    playerSearch.closest('form')!.dispatchEvent(new Event('submit'));
    http.expectOne(`${adminApi}/players?search=broken&assignment=all&page=1&limit=20`).flush(
      { message: 'Search unavailable' },
      { status: 500, statusText: 'Server Error' },
    );
    fixture.detectChanges();

    expect(panel.textContent).not.toContain('Linus Player');
    expect(panel.textContent).toContain('Spieler konnten nicht geladen werden');
    expect(panel.textContent).toContain('Search unavailable');
    childButton(panel, 'Erneut versuchen').click();
    http.expectOne(`${adminApi}/players?search=broken&assignment=all&page=1&limit=20`).flush(playersPage());
  });

  it('confirms unlinking and reassignment with the affected user names', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(
      playersPage({
        data: [
          {
            id: 101,
            firstName: 'Ada',
            lastName: 'Lovelace',
            active: true,
            team: { id: 5, name: 'First Team', alias: 'first' },
            currentUser: { id: 7, firstName: 'Ada', lastName: 'Lovelace', status: { id: 1, name: 'Active' } },
          },
          {
            id: 203,
            firstName: 'Other',
            lastName: 'Player',
            active: true,
            team: { id: 6, name: 'Second Team', alias: 'second' },
            currentUser: { id: 8, firstName: 'Alan', lastName: 'Turing', status: { id: 1, name: 'Active' } },
          },
        ],
        total: 2,
      }),
    );
    fixture.detectChanges();

    button('Verknüpfung lösen').click();
    expect(dialog.open.mock.calls[0][1].data.message).toContain('Ada Lovelace');
    closeDialog.next(false);
    http.expectNone(`${adminApi}/7/players/101`);

    closeDialog = new Subject<boolean>();
    dialog.open.mockReturnValue({ afterClosed: () => closeDialog.asObservable() });
    button('Neu zuordnen').click();
    const message = dialog.open.mock.calls[1][1].data.message;
    expect(message).toContain('Alan Turing');
    expect(message).toContain('Ada Lovelace');
    closeDialog.next(true);
    http.expectOne(`${adminApi}/7/players/203`).flush(ada);
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
  });

  it('keeps an unlink pending until success and then reloads directory and player results', () => {
    isAdmin.set(true);
    create();
    flushDirectory();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(
      playersPage({
        data: [
          {
            id: 101,
            firstName: 'Ada',
            lastName: 'Lovelace',
            active: true,
            team: { id: 5, name: 'First Team', alias: 'first' },
            currentUser: { id: 7, firstName: 'Ada', lastName: 'Lovelace', status: { id: 1, name: 'Active' } },
          },
        ],
      }),
    );
    fixture.detectChanges();

    button('Verknüpfung lösen').click();
    http.expectNone(`${adminApi}/7/players/101`);
    closeDialog.next(true);
    fixture.detectChanges();
    const unlink = http.expectOne(`${adminApi}/7/players/101`);
    expect(unlink.request.method).toBe('DELETE');
    expect(text()).toContain('Wird gelöst');
    const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
    expect(panel.querySelector('section')?.getAttribute('aria-busy')).toBe('true');
    expect(childButton(panel, 'Schließen').disabled).toBe(true);
    expect(childButton(panel, 'Suchen').disabled).toBe(true);
    const mainToggle = [...(fixture.nativeElement as HTMLElement).querySelectorAll<HTMLButtonElement>('[data-user-id="7"] > .user-row__actions button')].find(
      (item) => item.textContent?.includes('Zuordnungen verwalten'),
    )!;
    expect(mainToggle.disabled).toBe(true);
    childButton(panel, 'Schließen').click();
    fixture.detectChanges();
    expect((fixture.nativeElement as HTMLElement).querySelector('app-player-assignments')).not.toBeNull();
    unlink.flush(ada);
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
  });

  it('blocks directory search and paging while an assignment mutation is pending, then restores them', () => {
    isAdmin.set(true);
    create();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([ada], 1, 60, true));
    fixture.detectChanges();
    button('Weiter').click();
    http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([ada], 2, 60, true));
    fixture.detectChanges();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(
      playersPage({
        data: [
          {
            id: 101,
            firstName: 'Ada',
            lastName: 'Lovelace',
            active: true,
            team: { id: 5, name: 'First Team', alias: 'first' },
            currentUser: { id: 7, firstName: 'Ada', lastName: 'Lovelace', status: { id: 1, name: 'Active' } },
          },
        ],
      }),
    );
    fixture.detectChanges();
    childButton(
      (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!,
      'Verknüpfung lösen',
    ).click();
    closeDialog.next(true);
    fixture.detectChanges();
    const unlink = http.expectOne(`${adminApi}/7/players/101`);

    const directoryForm = (fixture.nativeElement as HTMLElement).querySelector<HTMLFormElement>('.directory-search')!;
    const directorySearch = directoryForm.querySelector<HTMLInputElement>('input[type="search"]')!;
    const directorySearchButton = directoryForm.querySelector<HTMLButtonElement>('button[type="submit"]')!;
    const paging = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('nav.pagination')!;
    const [previous, next] = [...paging.querySelectorAll<HTMLButtonElement>('button')];
    const controlsWereBlocked = directorySearch.disabled && directorySearchButton.disabled && previous.disabled && next.disabled;

    directorySearch.value = 'Other';
    directorySearch.dispatchEvent(new Event('input'));
    directoryForm.dispatchEvent(new Event('submit'));
    previous.click();
    next.click();
    fixture.detectChanges();
    const unexpectedQueries = http.match((request) => request.url.startsWith(api));
    const mutationStayedActive = !unlink.cancelled;

    if (controlsWereBlocked && unexpectedQueries.length === 0 && mutationStayedActive) {
      unlink.flush(ada);
      http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([ada], 2, 60, true));
      http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(playersPage());
      fixture.detectChanges();
      expect(directorySearch.disabled).toBe(false);
      expect(directorySearchButton.disabled).toBe(false);
      expect(previous.disabled).toBe(false);
      expect(next.disabled).toBe(false);
      directoryForm.dispatchEvent(new Event('submit'));
      http.expectOne(`${api}?page=1&limit=20&search=Other`).flush(directoryPage([]));
    } else {
      for (const query of unexpectedQueries) query.flush(directoryPage([ada], 1, 60, true));
      fixture.detectChanges();
      if (!unlink.cancelled) unlink.flush(ada);
      for (const playerLoad of http.match((request) => request.url.startsWith(`${adminApi}/players`))) {
        playerLoad.flush(playersPage());
      }
    }

    expect(controlsWereBlocked).toBe(true);
    expect(unexpectedQueries).toHaveLength(0);
    expect(mutationStayedActive).toBe(true);
  });

  it('keeps directory controls blocked until a failed assignment mutation clears', () => {
    isAdmin.set(true);
    create();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([ada], 1, 60, true));
    fixture.detectChanges();
    button('Weiter').click();
    http.expectOne(`${api}?page=2&limit=20`).flush(directoryPage([ada], 2, 60, true));
    fixture.detectChanges();
    button('Zuordnungen verwalten').click();
    fixture.detectChanges();
    http.expectOne(`${adminApi}/players?assignment=all&page=1&limit=20`).flush(
      playersPage({
        data: [
          {
            id: 101,
            firstName: 'Ada',
            lastName: 'Lovelace',
            active: true,
            team: { id: 5, name: 'First Team', alias: 'first' },
            currentUser: { id: 7, firstName: 'Ada', lastName: 'Lovelace', status: { id: 1, name: 'Active' } },
          },
        ],
      }),
    );
    fixture.detectChanges();

    const panel = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('app-player-assignments')!;
    panel.querySelector<HTMLButtonElement>('.player-row button')!.click();
    closeDialog.next(true);
    fixture.detectChanges();
    const unlink = http.expectOne(`${adminApi}/7/players/101`);

    const directoryForm = (fixture.nativeElement as HTMLElement).querySelector<HTMLFormElement>('.directory-search')!;
    const directorySearch = directoryForm.querySelector<HTMLInputElement>('input[type="search"]')!;
    const directorySearchButton = directoryForm.querySelector<HTMLButtonElement>('button[type="submit"]')!;
    const [previous, next] = [
      ...(fixture.nativeElement as HTMLElement).querySelectorAll<HTMLButtonElement>('nav.pagination button'),
    ];
    expect(directorySearch.disabled).toBe(true);
    expect(directorySearchButton.disabled).toBe(true);
    expect(previous.disabled).toBe(true);
    expect(next.disabled).toBe(true);

    directorySearch.value = 'Other';
    directorySearch.dispatchEvent(new Event('input'));
    directoryForm.dispatchEvent(new Event('submit'));
    previous.click();
    next.click();
    fixture.detectChanges();
    expect(http.match((request) => request.url.startsWith(api))).toHaveLength(0);
    expect(unlink.cancelled).toBe(false);

    unlink.flush({ message: 'Assignment failed' }, { status: 500, statusText: 'Server Error' });
    fixture.detectChanges();
    expect(text()).toContain('Zuordnung konnte nicht geändert werden. Assignment failed');
    expect(directorySearch.disabled).toBe(false);
    expect(directorySearchButton.disabled).toBe(false);
    expect(previous.disabled).toBe(false);
    expect(next.disabled).toBe(false);
    expect(http.match((request) => request.url.startsWith(api))).toHaveLength(0);

    directoryForm.dispatchEvent(new Event('submit'));
    http.expectOne(`${api}?page=1&limit=20&search=Other`).flush(directoryPage([]));
  });

  it('shows a persistent explanation for disabled self-deactivation', () => {
    isAdmin.set(true);
    currentUser.set({ id: 1, firstName: 'Grace', lastName: 'Admin', role: { id: 1 } });
    create();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([admin]));
    fixture.detectChanges();
    const row = (fixture.nativeElement as HTMLElement).querySelector<HTMLElement>('[data-user-id="1"]')!;
    const statusButton = [...row.querySelectorAll('button')].find((item) => item.textContent?.includes('Deaktivieren'))!;
    const descriptionId = statusButton.getAttribute('aria-describedby');
    expect(descriptionId).toBeTruthy();
    expect((fixture.nativeElement as HTMLElement).querySelector(`#${descriptionId}`)?.textContent).toContain(
      'eigene Konto kann nicht deaktiviert werden',
    );
  });

  it('renders loading, empty, general error, and retry states', () => {
    create();
    expect((fixture.nativeElement as HTMLElement).querySelector('[role="progressbar"]')).not.toBeNull();
    http.expectOne(`${api}?page=1&limit=20`).flush('broken', { status: 500, statusText: 'Server Error' });
    fixture.detectChanges();
    expect(text()).toContain('Benutzer konnten nicht geladen werden');
    button('Erneut versuchen').click();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage([]));
    fixture.detectChanges();
    expect(text()).toContain('Keine Benutzer gefunden');
  });

  it('surfaces directory and mutation authorization errors clearly', () => {
    create();
    http.expectOne(`${api}?page=1&limit=20`).flush({ message: 'Forbidden' }, { status: 403, statusText: 'Forbidden' });
    fixture.detectChanges();
    expect(text()).toContain('Keine Berechtigung');

    isAdmin.set(true);
    button('Erneut versuchen').click();
    http.expectOne(`${api}?page=1&limit=20`).flush(directoryPage());
    fixture.detectChanges();
    button('Deaktivieren').click();
    closeDialog.next(true);
    http.expectOne(`${adminApi}/7/status`).flush(
      { message: 'At least one active admin must remain' },
      { status: 403, statusText: 'Forbidden' },
    );
    fixture.detectChanges();
    expect(text()).toContain('Keine Berechtigung');
    expect(text()).toContain('At least one active admin must remain');
  });
});
