# Team-Select & Team Store Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the `TeamSelect` stub with a real screen that loads the logged-in user's team/player memberships from the backend, auto-continues straight to the team when there's only one, and add a shared `TeamStore`/`MyTeamsStore` plus a team-switcher menu in the `Shell` header so every `/team/:id/*` screen has the active team's data available.

**Architecture:** Two new signal-based, `providedIn: 'root'` stores under `src/app/core/team/`: `MyTeamsStore` (the user's team/player list, consumed by both `TeamSelect` and `Shell`'s switcher) and `TeamStore` (the currently active team's full detail, loaded by `Shell` whenever the route's `:id` param changes, consumed by this and future plans' `Overview`/`Members`/`Cashbox`/`More` screens). Both stores load through a single `TeamsApi` service. No resolvers — loading is signal/effect-driven, consistent with the Foundation plan's architecture decision.

**Tech Stack:** Angular 21 standalone components, Signals (`signal`/`computed`/`effect`), `@angular/core/rxjs-interop` (`takeUntilDestroyed`) for the route-param subscription, Angular Material (`MatListModule`, `MatMenuModule`, `MatButtonModule`, `MatProgressSpinnerModule`), Vitest.

## Global Constraints

- Angular 21, standalone components only — no NgModules, no `standalone: true` flag.
- File/class naming (confirmed via `ng generate` dry-runs in the Foundation plan): components in `<name>/<name>.ts` with `PascalCase` class and no `Component` suffix; services as `<name>.ts` with no `Service` suffix; stores follow the existing `AuthStore` precedent — `<name>-store.ts` file, `PascalCase` class ending in `Store`.
- State management is native Signals in injectable services — no NgRx.
- No i18n — German text hardcoded directly in templates.
- This app is zoneless (no `zone.js`) — tests use `async`/`await` and `await fixture.whenStable()` to let signal effects settle, never `fakeAsync`/`tick`.
- Tests use Vitest globals (`describe`/`it`/`expect`/`vi`), no imports needed. Single-spec-file runs: `npm test -- --include '<glob>'`.
- Backend base URL pattern: `{environment.apiUrl}<controller>/<path>`. Relevant endpoints for this plan (verified against the actual NestJS backend source): `GET {apiUrl}users/:id/teams` → `Player[]` (each with an eager `team` relation), `GET {apiUrl}teams/:id/overview` → `Team` (with `players` relation and a computed `outstanding` field).
- `Player.teamRole` from the backend is an object (`{ id: number; name?: string }`), NOT the Foundation plan's `TeamRole` numeric enum — the two are different things. This plan does not wire permission checks (`canBook`/`canInvite`); that's for the Cashbox/Members plans once real booking actions exist.

---

## Task 1: Team & Player Models

**Files:**
- Create: `src/app/models/team.model.ts`
- Create: `src/app/models/player.model.ts`

**Interfaces:**
- Produces: `interface Team { id: number; name: string; alias: string; balance: number; outstanding?: number; players?: Player[] }`, `interface PlayerTeamRole { id: number; name?: string }`, `interface Player { id: number; firstName: string; lastName: string; teamRole?: PlayerTeamRole | null; team?: Team; balance: number; active: boolean; user?: User | null }`.

No dedicated test file — these are pure data-shape interfaces with no logic, consistent with how `role.model.ts`/`status.model.ts`/`user.model.ts` were handled in the Foundation plan (only files with actual logic, like `team-role.model.ts`'s permission helpers, got tests).

- [ ] **Step 1: Create the Team model**

Create `src/app/models/team.model.ts`:

```ts
import { Player } from './player.model';

export interface Team {
  id: number;
  name: string;
  alias: string;
  balance: number;
  outstanding?: number;
  players?: Player[];
}
```

- [ ] **Step 2: Create the Player model**

Create `src/app/models/player.model.ts`:

```ts
import { Team } from './team.model';
import { User } from './user.model';

export interface PlayerTeamRole {
  id: number;
  name?: string;
}

export interface Player {
  id: number;
  firstName: string;
  lastName: string;
  teamRole?: PlayerTeamRole | null;
  team?: Team;
  balance: number;
  active: boolean;
  user?: User | null;
}
```

`team.model.ts` and `player.model.ts` import from each other — this is safe for TypeScript `interface`-only imports (interfaces are erased at compile time, so there's no runtime circular dependency).

- [ ] **Step 3: Verify the project still compiles**

Run: `npm run build -- --configuration development`
Expected: no compile errors (both new files are unused so far, but must be valid TypeScript).

- [ ] **Step 4: Commit**

```bash
git add src/app/models/team.model.ts src/app/models/player.model.ts
git commit -m "feat: add Team and Player domain models"
```

---

## Task 2: TeamsApi

**Files:**
- Create: `src/app/core/team/teams-api.ts`
- Test: `src/app/core/team/teams-api.spec.ts`

**Interfaces:**
- Consumes: `environment` from `../../../environments/environment`, `Player` from `../../models/player.model`, `Team` from `../../models/team.model`.
- Produces: `class TeamsApi` (`providedIn: 'root'`) with `loadMyTeams(userId: number): Observable<Player[]>` → `GET {apiUrl}users/{userId}/teams`, `loadTeamOverview(teamId: number): Observable<Team>` → `GET {apiUrl}teams/{teamId}/overview`.

- [ ] **Step 1: Write the failing test**

Create `src/app/core/team/teams-api.spec.ts`:

```ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TeamsApi } from './teams-api';
import { environment } from '../../../environments/environment';
import { Player } from '../../models/player.model';
import { Team } from '../../models/team.model';

describe('TeamsApi', () => {
  let service: TeamsApi;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    service = TestBed.inject(TeamsApi);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('fetches the players/teams belonging to a user', () => {
    const players: Player[] = [
      { id: 1, firstName: 'A', lastName: 'B', balance: 0, active: true },
    ];

    service.loadMyTeams(42).subscribe((response) => {
      expect(response).toEqual(players);
    });

    const request = httpMock.expectOne(`${environment.apiUrl}users/42/teams`);
    expect(request.request.method).toBe('GET');
    request.flush(players);
  });

  it('fetches a team overview by id', () => {
    const team: Team = { id: 5, name: 'Team A', alias: 'team-a', balance: 0 };

    service.loadTeamOverview(5).subscribe((response) => {
      expect(response).toEqual(team);
    });

    const request = httpMock.expectOne(`${environment.apiUrl}teams/5/overview`);
    expect(request.request.method).toBe('GET');
    request.flush(team);
  });
});
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `npm test -- --include '**/teams-api.spec.ts'`
Expected: FAIL — `teams-api.ts` does not exist yet.

- [ ] **Step 3: Implement TeamsApi**

Create `src/app/core/team/teams-api.ts`:

```ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { Player } from '../../models/player.model';
import { Team } from '../../models/team.model';

@Injectable({ providedIn: 'root' })
export class TeamsApi {
  private readonly http = inject(HttpClient);

  loadMyTeams(userId: number): Observable<Player[]> {
    return this.http.get<Player[]>(`${environment.apiUrl}users/${userId}/teams`);
  }

  loadTeamOverview(teamId: number): Observable<Team> {
    return this.http.get<Team>(`${environment.apiUrl}teams/${teamId}/overview`);
  }
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run: `npm test -- --include '**/teams-api.spec.ts'`
Expected: PASS (2 tests).

- [ ] **Step 5: Commit**

```bash
git add src/app/core/team/teams-api.ts src/app/core/team/teams-api.spec.ts
git commit -m "feat: add TeamsApi for loading a user's teams and a team overview"
```

---

## Task 3: MyTeamsStore

**Files:**
- Create: `src/app/core/team/my-teams-store.ts`
- Test: `src/app/core/team/my-teams-store.spec.ts`

**Interfaces:**
- Consumes: `TeamsApi` from `./teams-api`, `Player` from `../../models/player.model`.
- Produces: `class MyTeamsStore` (`providedIn: 'root'`) with `readonly players: Signal<Player[]>`, `readonly loading: Signal<boolean>`, `ensureLoaded(userId: number): void` (no-op if already loaded for that user, or already loading).

- [ ] **Step 1: Write the failing test**

Create `src/app/core/team/my-teams-store.spec.ts`:

```ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { MyTeamsStore } from './my-teams-store';
import { environment } from '../../../environments/environment';
import { Player } from '../../models/player.model';

describe('MyTeamsStore', () => {
  let store: MyTeamsStore;
  let httpMock: HttpTestingController;

  const player: Player = {
    id: 1,
    firstName: 'A',
    lastName: 'B',
    balance: 0,
    active: true,
    team: { id: 5, name: 'Team A', alias: 'team-a', balance: 0 },
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    store = TestBed.inject(MyTeamsStore);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('loads and exposes the players for a user', () => {
    store.ensureLoaded(42);
    expect(store.loading()).toBe(true);

    const request = httpMock.expectOne(`${environment.apiUrl}users/42/teams`);
    request.flush([player]);

    expect(store.loading()).toBe(false);
    expect(store.players()).toEqual([player]);
  });

  it('does not reload for the same user id', () => {
    store.ensureLoaded(42);
    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([player]);

    store.ensureLoaded(42);

    httpMock.expectNone(`${environment.apiUrl}users/42/teams`);
  });

  it('resets loading on error without throwing', () => {
    store.ensureLoaded(42);

    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush('error', {
      status: 500,
      statusText: 'Server Error',
    });

    expect(store.loading()).toBe(false);
    expect(store.players()).toEqual([]);
  });
});
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `npm test -- --include '**/my-teams-store.spec.ts'`
Expected: FAIL — `my-teams-store.ts` does not exist yet.

- [ ] **Step 3: Implement MyTeamsStore**

Create `src/app/core/team/my-teams-store.ts`:

```ts
import { Injectable, inject, signal } from '@angular/core';
import { Player } from '../../models/player.model';
import { TeamsApi } from './teams-api';

@Injectable({ providedIn: 'root' })
export class MyTeamsStore {
  private readonly teamsApi = inject(TeamsApi);

  private readonly playersSignal = signal<Player[]>([]);
  private readonly loadingSignal = signal(false);
  private readonly loadedForUserId = signal<number | null>(null);

  readonly players = this.playersSignal.asReadonly();
  readonly loading = this.loadingSignal.asReadonly();

  ensureLoaded(userId: number): void {
    if (this.loadedForUserId() === userId || this.loadingSignal()) {
      return;
    }

    this.loadingSignal.set(true);
    this.teamsApi.loadMyTeams(userId).subscribe({
      next: (players) => {
        this.playersSignal.set(players);
        this.loadedForUserId.set(userId);
        this.loadingSignal.set(false);
      },
      error: () => {
        this.loadingSignal.set(false);
      },
    });
  }
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run: `npm test -- --include '**/my-teams-store.spec.ts'`
Expected: PASS (3 tests).

- [ ] **Step 5: Commit**

```bash
git add src/app/core/team/my-teams-store.ts src/app/core/team/my-teams-store.spec.ts
git commit -m "feat: add MyTeamsStore for the user's team memberships"
```

---

## Task 4: TeamStore

**Files:**
- Create: `src/app/core/team/team-store.ts`
- Test: `src/app/core/team/team-store.spec.ts`

**Interfaces:**
- Consumes: `TeamsApi` from `./teams-api`, `Team` from `../../models/team.model`.
- Produces: `class TeamStore` (`providedIn: 'root'`) with `readonly team: Signal<Team | null>`, `readonly loading: Signal<boolean>`, `loadTeam(teamId: number): void` (no-op if already loaded for that team id).

- [ ] **Step 1: Write the failing test**

Create `src/app/core/team/team-store.spec.ts`:

```ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TeamStore } from './team-store';
import { environment } from '../../../environments/environment';
import { Team } from '../../models/team.model';

describe('TeamStore', () => {
  let store: TeamStore;
  let httpMock: HttpTestingController;

  const team: Team = { id: 5, name: 'Team A', alias: 'team-a', balance: 100 };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    store = TestBed.inject(TeamStore);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('loads and exposes the active team', () => {
    store.loadTeam(5);
    expect(store.loading()).toBe(true);

    const request = httpMock.expectOne(`${environment.apiUrl}teams/5/overview`);
    request.flush(team);

    expect(store.loading()).toBe(false);
    expect(store.team()).toEqual(team);
  });

  it('does not reload for the same team id', () => {
    store.loadTeam(5);
    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush(team);

    store.loadTeam(5);

    httpMock.expectNone(`${environment.apiUrl}teams/5/overview`);
  });

  it('reloads when the team id changes', () => {
    store.loadTeam(5);
    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush(team);

    store.loadTeam(6);

    httpMock.expectOne(`${environment.apiUrl}teams/6/overview`).flush({ ...team, id: 6 });
    expect(store.team()?.id).toBe(6);
  });

  it('resets loading on error without throwing', () => {
    store.loadTeam(5);

    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush('error', {
      status: 500,
      statusText: 'Server Error',
    });

    expect(store.loading()).toBe(false);
    expect(store.team()).toBeNull();
  });
});
```

- [ ] **Step 2: Run the test to verify it fails**

Run: `npm test -- --include '**/team-store.spec.ts'`
Expected: FAIL — `team-store.ts` does not exist yet.

- [ ] **Step 3: Implement TeamStore**

Create `src/app/core/team/team-store.ts`:

```ts
import { Injectable, inject, signal } from '@angular/core';
import { Team } from '../../models/team.model';
import { TeamsApi } from './teams-api';

@Injectable({ providedIn: 'root' })
export class TeamStore {
  private readonly teamsApi = inject(TeamsApi);

  private readonly teamSignal = signal<Team | null>(null);
  private readonly loadingSignal = signal(false);
  private readonly loadedTeamId = signal<number | null>(null);

  readonly team = this.teamSignal.asReadonly();
  readonly loading = this.loadingSignal.asReadonly();

  loadTeam(teamId: number): void {
    if (this.loadedTeamId() === teamId) {
      return;
    }

    this.loadingSignal.set(true);
    this.teamsApi.loadTeamOverview(teamId).subscribe({
      next: (team) => {
        this.teamSignal.set(team);
        this.loadedTeamId.set(teamId);
        this.loadingSignal.set(false);
      },
      error: () => {
        this.loadingSignal.set(false);
      },
    });
  }
}
```

- [ ] **Step 4: Run the test to verify it passes**

Run: `npm test -- --include '**/team-store.spec.ts'`
Expected: PASS (4 tests).

- [ ] **Step 5: Commit**

```bash
git add src/app/core/team/team-store.ts src/app/core/team/team-store.spec.ts
git commit -m "feat: add TeamStore for the active team's detail"
```

---

## Task 5: Real Team-Select Screen

**Files:**
- Modify: `src/app/features/team-select/team-select.ts` (currently a stub: `@Component({ selector: 'app-team-select', template: '<h1>Team auswählen</h1>' }) export class TeamSelect {}`)
- Create: `src/app/features/team-select/team-select.html`
- Create: `src/app/features/team-select/team-select.scss`
- Modify: `src/app/features/team-select/team-select.spec.ts` (currently a single stub test asserting the `<h1>` text)

**Interfaces:**
- Consumes: `AuthStore` from `../../core/auth/auth-store` (`.currentUser()`), `MyTeamsStore` from `../../core/team/my-teams-store` (`.players()`, `.loading()`, `.ensureLoaded(userId)`).
- Produces: `class TeamSelect` — renders a loading state, an empty state, or a list of team entries; auto-navigates to `/team/:id/overview` when the user belongs to exactly one team.

- [ ] **Step 1: Write the failing tests**

Replace `src/app/features/team-select/team-select.spec.ts`:

```ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideRouter, Router } from '@angular/router';
import { TeamSelect } from './team-select';
import { environment } from '../../../environments/environment';
import { AuthStore } from '../../core/auth/auth-store';
import { Player } from '../../models/player.model';

describe('TeamSelect', () => {
  let httpMock: HttpTestingController;
  let router: Router;
  let authStore: AuthStore;

  beforeEach(async () => {
    localStorage.clear();
    await TestBed.configureTestingModule({
      imports: [TeamSelect],
      providers: [provideHttpClient(), provideHttpClientTesting(), provideRouter([])],
    }).compileComponents();

    httpMock = TestBed.inject(HttpTestingController);
    router = TestBed.inject(Router);
    authStore = TestBed.inject(AuthStore);
    authStore.setSession('token', { id: 42, email: 'a@b.de', firstName: 'A', lastName: 'B' });
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('renders one entry per team when the user has several', async () => {
    const players: Player[] = [
      {
        id: 1,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 5, name: 'Team A', alias: 'a', balance: 0 },
      },
      {
        id: 2,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 6, name: 'Team B', alias: 'b', balance: 0 },
      },
    ];

    const fixture = TestBed.createComponent(TeamSelect);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush(players);
    await fixture.whenStable();
    fixture.detectChanges();

    const items = fixture.nativeElement.querySelectorAll('a[mat-list-item]');
    expect(items.length).toBe(2);
  });

  it('redirects automatically when the user has exactly one team', async () => {
    const players: Player[] = [
      {
        id: 1,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 5, name: 'Team A', alias: 'a', balance: 0 },
      },
    ];
    const navigateSpy = vi.spyOn(router, 'navigate');

    const fixture = TestBed.createComponent(TeamSelect);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush(players);
    await fixture.whenStable();

    expect(navigateSpy).toHaveBeenCalledWith(['/team', 5, 'overview']);
  });

  it('shows an empty state when the user has no teams', async () => {
    const fixture = TestBed.createComponent(TeamSelect);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('Du bist noch keinem Team zugeordnet.');
  });
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `npm test -- --include '**/team-select.spec.ts'`
Expected: FAIL — the stub component doesn't inject `AuthStore`/`MyTeamsStore` or render a list, so none of the 3 new assertions pass.

- [ ] **Step 3: Implement the real TeamSelect component**

Replace `src/app/features/team-select/team-select.ts`:

```ts
import { Component, effect, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Router } from '@angular/router';
import { MatListModule } from '@angular/material/list';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { AuthStore } from '../../core/auth/auth-store';
import { MyTeamsStore } from '../../core/team/my-teams-store';

@Component({
  selector: 'app-team-select',
  imports: [RouterLink, MatListModule, MatProgressSpinnerModule],
  templateUrl: './team-select.html',
  styleUrl: './team-select.scss',
})
export class TeamSelect {
  private readonly authStore = inject(AuthStore);
  private readonly myTeamsStore = inject(MyTeamsStore);
  private readonly router = inject(Router);

  protected readonly players = this.myTeamsStore.players;
  protected readonly loading = this.myTeamsStore.loading;

  constructor() {
    const userId = this.authStore.currentUser()?.id;
    if (userId) {
      this.myTeamsStore.ensureLoaded(userId);
    }

    effect(() => {
      const players = this.myTeamsStore.players();
      if (!this.myTeamsStore.loading() && players.length === 1 && players[0].team) {
        void this.router.navigate(['/team', players[0].team.id, 'overview']);
      }
    });
  }
}
```

Create `src/app/features/team-select/team-select.html`:

```html
@if (loading()) {
  <div class="team-select-loading">
    <mat-spinner diameter="32" />
  </div>
} @else if (players().length === 0) {
  <div class="team-select-empty">
    <p>Du bist noch keinem Team zugeordnet.</p>
  </div>
} @else {
  <div class="team-select-page">
    <h1>Team auswählen</h1>
    <mat-nav-list>
      @for (player of players(); track player.id) {
        <a mat-list-item [routerLink]="['/team', player.team?.id, 'overview']">
          <span matListItemTitle>{{ player.team?.name }}</span>
          <span matListItemLine>{{ player.firstName }} {{ player.lastName }}</span>
        </a>
      }
    </mat-nav-list>
  </div>
}
```

Create `src/app/features/team-select/team-select.scss`:

```scss
.team-select-loading,
.team-select-empty {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 60dvh;
  padding: 1rem;
  text-align: center;
}

.team-select-page {
  padding: 1rem;
}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `npm test -- --include '**/team-select.spec.ts'`
Expected: PASS (3 tests).

- [ ] **Step 5: Commit**

```bash
git add src/app/features/team-select
git commit -m "feat: implement real team-select screen with auto-redirect for a single team"
```

---

## Task 6: Team Switcher in the App Shell

**Files:**
- Modify: `src/app/core/layout/shell/shell.ts`
- Modify: `src/app/core/layout/shell/shell.html`
- Modify: `src/app/core/layout/shell/shell.scss`
- Modify: `src/app/core/layout/shell/shell.spec.ts`

**Interfaces:**
- Consumes: `AuthStore` from `../../auth/auth-store`, `MyTeamsStore`/`TeamStore` from `../../team/my-teams-store` and `../../team/team-store`, `Team` from `../../../models/team.model`.
- Produces: `Shell` now loads the active team (via the route's `:id` param) into `TeamStore` on every navigation into `/team/:id/*`, and shows a team-switcher menu in the header when the user belongs to more than one team.

- [ ] **Step 1: Write the failing tests**

Replace `src/app/core/layout/shell/shell.spec.ts`:

```ts
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { Shell } from './shell';
import { environment } from '../../../../environments/environment';
import { AuthStore } from '../../auth/auth-store';
import { Player } from '../../../models/player.model';

describe('Shell', () => {
  let httpMock: HttpTestingController;
  let authStore: AuthStore;

  beforeEach(async () => {
    localStorage.clear();
    await TestBed.configureTestingModule({
      imports: [Shell],
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        provideRouter([]),
        {
          provide: ActivatedRoute,
          useValue: { paramMap: of(convertToParamMap({ id: '5' })) },
        },
      ],
    }).compileComponents();

    httpMock = TestBed.inject(HttpTestingController);
    authStore = TestBed.inject(AuthStore);
    authStore.setSession('token', { id: 42, email: 'a@b.de', firstName: 'A', lastName: 'B' });
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('renders the bottom navigation with four tabs', () => {
    const fixture = TestBed.createComponent(Shell);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush({
      id: 5,
      name: 'Team A',
      alias: 'a',
      balance: 0,
    });
    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]);

    const links = fixture.nativeElement.querySelectorAll('.shell-bottom-nav__item');
    expect(links.length).toBe(4);
  });

  it('loads the team for the route id and shows its name in the header', async () => {
    const fixture = TestBed.createComponent(Shell);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush({
      id: 5,
      name: 'Team A',
      alias: 'a',
      balance: 0,
    });
    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush([]);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.shell-header')?.textContent).toContain('Team A');
  });

  it('shows a team switcher when the user belongs to more than one team', async () => {
    const players: Player[] = [
      {
        id: 1,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 5, name: 'Team A', alias: 'a', balance: 0 },
      },
      {
        id: 2,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 6, name: 'Team B', alias: 'b', balance: 0 },
      },
    ];

    const fixture = TestBed.createComponent(Shell);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush({
      id: 5,
      name: 'Team A',
      alias: 'a',
      balance: 0,
    });
    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush(players);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.shell-team-switcher')).toBeTruthy();
  });

  it('does not show a team switcher when the user belongs to only one team', async () => {
    const players: Player[] = [
      {
        id: 1,
        firstName: 'A',
        lastName: 'B',
        balance: 0,
        active: true,
        team: { id: 5, name: 'Team A', alias: 'a', balance: 0 },
      },
    ];

    const fixture = TestBed.createComponent(Shell);
    fixture.detectChanges();

    httpMock.expectOne(`${environment.apiUrl}teams/5/overview`).flush({
      id: 5,
      name: 'Team A',
      alias: 'a',
      balance: 0,
    });
    httpMock.expectOne(`${environment.apiUrl}users/42/teams`).flush(players);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.shell-team-switcher')).toBeFalsy();
  });
});
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `npm test -- --include '**/shell.spec.ts'`
Expected: FAIL — the current `Shell` doesn't inject `ActivatedRoute`/`TeamStore`/`MyTeamsStore`, doesn't make the two HTTP calls the tests expect, and has no `.shell-team-switcher` element.

- [ ] **Step 3: Implement the team switcher**

Replace `src/app/core/layout/shell/shell.ts`:

```ts
import { Component, computed, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { AuthStore } from '../../auth/auth-store';
import { MyTeamsStore } from '../../team/my-teams-store';
import { TeamStore } from '../../team/team-store';
import { Team } from '../../../models/team.model';

@Component({
  selector: 'app-shell',
  imports: [
    RouterOutlet,
    RouterLink,
    RouterLinkActive,
    MatToolbarModule,
    MatIconModule,
    MatMenuModule,
    MatButtonModule,
  ],
  templateUrl: './shell.html',
  styleUrl: './shell.scss',
})
export class Shell {
  private readonly route = inject(ActivatedRoute);
  private readonly router = inject(Router);
  private readonly authStore = inject(AuthStore);
  private readonly myTeamsStore = inject(MyTeamsStore);
  private readonly teamStore = inject(TeamStore);

  protected readonly currentTeam = this.teamStore.team;

  protected readonly myTeams = computed(() => {
    const seen = new Set<number>();
    const teams: Team[] = [];
    for (const player of this.myTeamsStore.players()) {
      if (player.team && !seen.has(player.team.id)) {
        seen.add(player.team.id);
        teams.push(player.team);
      }
    }
    return teams;
  });

  constructor() {
    const userId = this.authStore.currentUser()?.id;
    if (userId) {
      this.myTeamsStore.ensureLoaded(userId);
    }

    // A direct subscription (not `effect()` + `toSignal()`) so the initial
    // team load happens synchronously during construction, exactly like
    // `ensureLoaded` above — `ActivatedRoute.paramMap` always replays its
    // current value synchronously to a new subscriber. This keeps the
    // component's behavior deterministic and trivial to test: no signal
    // effect scheduling to wait for.
    this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => {
      const id = Number(params.get('id'));
      if (!Number.isNaN(id)) {
        this.teamStore.loadTeam(id);
      }
    });
  }

  protected switchTeam(teamId: number): void {
    void this.router.navigate(['/team', teamId, 'overview']);
  }
}
```

Replace `src/app/core/layout/shell/shell.html`:

```html
<mat-toolbar class="shell-header">
  @if (myTeams().length > 1) {
    <button mat-button [matMenuTriggerFor]="teamMenu" class="shell-team-switcher">
      <span>{{ currentTeam()?.name ?? 'TeamWallet' }}</span>
      <mat-icon>arrow_drop_down</mat-icon>
    </button>
    <mat-menu #teamMenu="matMenu">
      @for (team of myTeams(); track team.id) {
        <button mat-menu-item (click)="switchTeam(team.id)">{{ team.name }}</button>
      }
    </mat-menu>
  } @else {
    <span>{{ currentTeam()?.name ?? 'TeamWallet' }}</span>
  }
</mat-toolbar>

<main class="shell-content">
  <router-outlet />
</main>

<nav class="shell-bottom-nav">
  <a routerLink="overview" routerLinkActive="active" class="shell-bottom-nav__item">
    <mat-icon>account_balance_wallet</mat-icon>
    <span>Übersicht</span>
  </a>
  <a routerLink="members" routerLinkActive="active" class="shell-bottom-nav__item">
    <mat-icon>groups</mat-icon>
    <span>Mitglieder</span>
  </a>
  <a routerLink="cashbox" routerLinkActive="active" class="shell-bottom-nav__item">
    <mat-icon>payments</mat-icon>
    <span>Kasse</span>
  </a>
  <a routerLink="more" routerLinkActive="active" class="shell-bottom-nav__item">
    <mat-icon>more_horiz</mat-icon>
    <span>Mehr</span>
  </a>
</nav>
```

Edit `src/app/core/layout/shell/shell.scss` — add a rule for the new button so it matches the existing header's color instead of the Material default button colors:

```scss
.shell-team-switcher {
  color: var(--mat-sys-on-primary);
}
```

Add this rule right after the existing `.shell-header` rule (keep everything else in the file unchanged).

- [ ] **Step 4: Run the tests to verify they pass**

Run: `npm test -- --include '**/shell.spec.ts'`
Expected: PASS (4 tests).

- [ ] **Step 5: Commit**

```bash
git add src/app/core/layout/shell
git commit -m "feat: load the active team in Shell and add a team switcher menu"
```

---

## Task 7: Final Verification

**Files:** none (verification only)

- [ ] **Step 1: Run the full unit test suite**

Run: `npm test`
Expected: all tests pass, no failures (Foundation plan's 36 tests plus this plan's new ones).

- [ ] **Step 2: Run a production build**

Run: `npm run build`
Expected: build succeeds with no errors or new warnings.

- [ ] **Step 3: Check formatting**

Run: `npx prettier --check "src/**/*.{ts,html,scss}"`
Expected: no formatting issues. If issues are reported, run `npx prettier --write "src/**/*.{ts,html,scss}"` and re-check.

- [ ] **Step 4: Commit (only if Steps 1–3 required fixes)**

```bash
git add -A
git commit -m "chore: fix formatting/build issues found during final verification"
```
