import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { By } from '@angular/platform-browser';
import { ActivatedRoute, ParamMap, convertToParamMap, provideRouter } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { Overview } from './overview';
import { ChartCanvas } from '../../../shared/chart-canvas/chart-canvas';
import { TeamStore } from '../../../core/team/team-store';
import { environment } from '../../../../environments/environment';
import { TeamOverviewStats } from '../../../models/team-stats.model';

// `vi.mock`'s factory is hoisted above regular imports, so the shared mock class is
// loaded via a dynamic import inside `vi.hoisted` rather than a plain top-level import.
const { MockChart } = await vi.hoisted(
  async () => import('../../../shared/chart-canvas/testing/mock-chart'),
);

vi.mock('chart.js', () => ({ Chart: MockChart, registerables: [] }));

const sampleStats: TeamOverviewStats = {
  balanceHistory: [
    { month: '2026-06', balance: 100, theoreticalBalance: 100 },
    { month: '2026-07', balance: 125, theoreticalBalance: 150 },
  ],
  monthlyFlow: [
    { month: '2026-06', income: 50, expense: 10, penalties: 20 },
    { month: '2026-07', income: 40, expense: 15, penalties: 5 },
  ],
  topOutstanding: [{ playerId: 3, playerName: 'Chris Beispiel', balance: 20 }],
};

const emptyStats: TeamOverviewStats = { balanceHistory: [], monthlyFlow: [], topOutstanding: [] };

describe('Overview', () => {
  let routeParams: BehaviorSubject<ParamMap>;
  let httpMock: HttpTestingController;
  let fixture: ComponentFixture<Overview>;

  beforeEach(async () => {
    MockChart.instances.length = 0;
    routeParams = new BehaviorSubject(convertToParamMap({ id: '5' }));
    await TestBed.configureTestingModule({
      imports: [Overview],
      providers: [
        provideHttpClient(),
        provideHttpClientTesting(),
        provideRouter([]),
        {
          provide: TeamStore,
          useValue: {
            team: signal({ id: 5, name: 'Team A', alias: 'a', balance: 125, outstanding: 40 }),
            loading: signal(false),
          },
        },
        {
          provide: ActivatedRoute,
          useValue: {
            parent: {
              snapshot: { paramMap: convertToParamMap({ id: '5' }) },
              paramMap: routeParams,
            },
          },
        },
      ],
    }).compileComponents();
    httpMock = TestBed.inject(HttpTestingController);
    fixture = TestBed.createComponent(Overview);
  });

  function flushTransactions(activities: unknown[], teamId = 5): void {
    httpMock.expectOne(`${environment.apiUrl}teams/${teamId}/transactions`).flush(activities);
  }

  function flushStats(stats: TeamOverviewStats, teamId = 5): void {
    httpMock.expectOne(`${environment.apiUrl}teams/${teamId}/overview/stats`).flush(stats);
  }

  function failStats(teamId = 5): void {
    httpMock
      .expectOne(`${environment.apiUrl}teams/${teamId}/overview/stats`)
      .flush(null, { status: 500, statusText: 'Server Error' });
  }

  it('renders balances and the recent team activity', async () => {
    fixture.detectChanges();
    flushTransactions([
      {
        id: 1,
        date: '2026-07-31',
        amount: 12,
        type: 'payment',
        note: 'Beitrag',
        playerName: 'Alex',
        isTeamWalletTransaction: false,
      },
      {
        id: 2,
        date: '2026-07-30',
        amount: 8,
        type: 'expense',
        note: 'Material',
        isTeamWalletTransaction: true,
      },
      {
        id: 3,
        date: '2026-07-29',
        amount: 5,
        type: 'fine',
        note: 'Training',
        playerName: 'Bea',
        isTeamWalletTransaction: false,
      },
    ]);
    flushStats(sampleStats);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('125,00');
    expect(fixture.nativeElement.textContent).toContain('Alex');
    expect(fixture.nativeElement.textContent).toContain('Beitrag');
    const amounts = [
      ...fixture.nativeElement.querySelectorAll(
        'app-transaction-amount [data-testid="transaction-amount"]',
      ),
    ] as HTMLElement[];
    expect(amounts).toHaveLength(3);
    expect(amounts[0].classList).toContain('inflow');
    expect(amounts[1].classList).toContain('outflow');
    expect(amounts[2].classList).toContain('neutral');
    expect(
      amounts.map((amount) => amount.querySelector('.transaction-amount__sign')?.textContent),
    ).toEqual(['+', '−', '']);

    routeParams.next(convertToParamMap({ id: '6' }));
    flushTransactions(
      [
        {
          id: 2,
          date: '2026-08-01',
          amount: 7,
          type: 'credit',
          note: 'Neues Team',
          playerName: 'Bea',
          isTeamWalletTransaction: false,
        },
      ],
      6,
    );
    flushStats(sampleStats, 6);
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('Neues Team');
    expect(fixture.nativeElement.textContent).not.toContain('Beitrag');
  });

  it('shows a loading spinner in each chart card while stats are loading', () => {
    fixture.detectChanges();

    const spinners = fixture.nativeElement.querySelectorAll('.chart-block mat-spinner');
    expect(spinners.length).toBe(3);

    flushTransactions([]);
    flushStats(sampleStats);
  });

  it('shows an empty state per chart when its dataset is an empty array', async () => {
    fixture.detectChanges();
    flushTransactions([]);
    flushStats(emptyStats);
    await fixture.whenStable();
    fixture.detectChanges();

    expect(fixture.nativeElement.textContent).toContain('Noch keine Kassenstand-Historie');
    expect(fixture.nativeElement.textContent).toContain('Noch keine Bewegungen');
    expect(fixture.nativeElement.textContent).toContain('Keine offenen Beiträge');
    expect(fixture.debugElement.queryAll(By.directive(ChartCanvas))).toHaveLength(0);
  });

  it('passes the loaded stats to each ChartCanvas once the request resolves', async () => {
    fixture.detectChanges();
    flushTransactions([]);
    flushStats(sampleStats);
    await fixture.whenStable();
    fixture.detectChanges();

    const charts = fixture.debugElement.queryAll(By.directive(ChartCanvas));
    expect(charts).toHaveLength(3);

    const [balanceChart, flowChart, outstandingChart] = charts.map(
      (c) => c.componentInstance as ChartCanvas,
    );
    expect(balanceChart.type).toBe('line');
    expect(balanceChart.data.labels).toHaveLength(2);
    expect(balanceChart.data.datasets).toHaveLength(2);
    expect(balanceChart.data.datasets[0].data).toEqual([100, 125]);
    expect(balanceChart.data.datasets[1].label).toBe('Theoretisch (inkl. offene Beiträge)');
    expect(balanceChart.data.datasets[1].data).toEqual([100, 150]);
    expect(balanceChart.options?.plugins?.legend?.position).toBe('bottom');
    expect(flowChart.type).toBe('bar');
    expect(flowChart.data.datasets).toHaveLength(3);
    expect(flowChart.data.datasets[2].label).toBe('Strafen & Umlagen');
    expect(flowChart.data.datasets[2].data).toEqual([20, 5]);
    expect(flowChart.data.datasets[2].backgroundColor).toBe('#9e9e9e');
    expect(outstandingChart.type).toBe('bar');
    expect(outstandingChart.data.labels).toEqual(['Chris Beispiel']);
  });

  it('shows an empty state without throwing when the stats request errors', async () => {
    fixture.detectChanges();
    flushTransactions([]);

    expect(() => failStats()).not.toThrow();
    await fixture.whenStable();
    expect(() => fixture.detectChanges()).not.toThrow();

    expect(fixture.nativeElement.textContent).toContain('Noch keine Kassenstand-Historie');
    expect(fixture.nativeElement.textContent).toContain('Noch keine Bewegungen');
    expect(fixture.nativeElement.textContent).toContain('Keine offenen Beiträge');
  });

  it('links the Top-10 card to the members route', async () => {
    fixture.detectChanges();
    flushTransactions([]);
    flushStats(sampleStats);
    await fixture.whenStable();
    fixture.detectChanges();

    const link: HTMLAnchorElement | null = fixture.nativeElement.querySelector(
      '.chart-block--outstanding a[routerLink]',
    );
    expect(link).toBeTruthy();
    expect(link?.getAttribute('routerLink')).toBe('../members');
  });
});
