import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ActivatedRoute, ParamMap, Router, 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';
import { NotificationsStore } from '../../notifications/notifications-store';

describe('Shell', () => {
  let httpMock: HttpTestingController;
  let authStore: AuthStore;
  let routeParams: BehaviorSubject<ReturnType<typeof convertToParamMap>>;
  let notificationsStore: {
    unreadCount: ReturnType<typeof signal<number>>;
    notifications: ReturnType<typeof signal<any[]>>;
    startPolling: ReturnType<typeof vi.fn>;
    loadRecent: ReturnType<typeof vi.fn>;
    markRead: ReturnType<typeof vi.fn>;
    markAllRead: ReturnType<typeof vi.fn>;
  };

  beforeEach(async () => {
    localStorage.clear();
    routeParams = new BehaviorSubject(convertToParamMap({ id: '5' }));
    notificationsStore = {
      unreadCount: signal(3),
      notifications: signal([
        {
          id: 1,
          event: 'player_creation',
          actorUserId: 9,
          payload: { playerId: 21, playerName: 'Ada Lovelace' },
          read: false,
          createdAt: '2026-08-04T10:00:00.000Z',
        },
      ]),
      startPolling: vi.fn(),
      loadRecent: vi.fn(),
      markRead: vi.fn(),
      markAllRead: vi.fn(),
    };
    await TestBed.configureTestingModule({
      imports: [Shell],
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        provideRouter([]),
        { provide: NotificationsStore, useValue: notificationsStore },
        {
          provide: ActivatedRoute,
          useValue: { paramMap: routeParams.asObservable() },
        },
      ],
    }).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();
  });

  it('ignores missing, non-positive, fractional, and non-numeric team ids', () => {
    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([]);

    for (const id of [null, '0', '-1', '1.5', 'abc']) {
      routeParams.next(convertToParamMap(id === null ? {} : { id }));
    }

    expect(httpMock.match((request) => request.url.includes('/teams/')).length).toBe(0);
  });

  it('starts polling notifications for the routed team id', () => {
    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([]);

    expect(notificationsStore.startPolling).toHaveBeenCalledWith(5);
  });

  it('exposes the unread count from the notifications store', () => {
    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([]);

    expect((fixture.componentInstance as any).unreadCount()).toBe(3);
  });

  it('loads recent notifications when the bell menu is opened', () => {
    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([]);

    (fixture.componentInstance as any).onNotificationsMenuOpened();

    expect(notificationsStore.loadRecent).toHaveBeenCalledWith(5);
  });

  it('marks a clicked notification as read and navigates to its target', () => {
    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 navigateSpy = vi.spyOn(TestBed.inject(Router), 'navigate');

    const item = notificationsStore.notifications()[0];
    (fixture.componentInstance as any).onNotificationClick(item);

    expect(notificationsStore.markRead).toHaveBeenCalledWith(5, 1);
    expect(navigateSpy).toHaveBeenCalledWith(['/team', 5, 'members', 21]);
  });

  it('marks all notifications as read', () => {
    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([]);

    (fixture.componentInstance as any).onMarkAllRead();

    expect(notificationsStore.markAllRead).toHaveBeenCalledWith(5);
  });
});
