import 'reflect-metadata';

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Test } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import request from 'supertest';
import { ValidationPipe } from '@nestjs/common';

import { LeagueModule } from './LeagueModule';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';

import { requestContextMiddleware } from '@adapters/http/RequestContext';

import {
  DRIVER_REPOSITORY_TOKEN,
  LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
  LEAGUE_REPOSITORY_TOKEN,
} from '../../persistence/inmemory/InMemoryRacingPersistenceModule';

import { League } from '@core/racing/domain/entities/League';
import { Driver } from '@core/racing/domain/entities/Driver';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';

describe('League roster member mutations (HTTP)', () => {
  let app: any;

  const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
    getCurrentSession: vi.fn(async () => null),
  };

  const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
    getRolesForUser: vi.fn(() => []),
  };

  async function seedLeagueWithMembers(params: {
    leagueId: string;
    adminId: string;
    memberId: string;
  }): Promise<void> {
    const leagueRepo = app.get(LEAGUE_REPOSITORY_TOKEN);
    const driverRepo = app.get(DRIVER_REPOSITORY_TOKEN);
    const membershipRepo = app.get(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN);

    await leagueRepo.create(
      League.create({
        id: params.leagueId,
        name: 'Test League',
        description: 'Test league',
        ownerId: params.adminId,
        settings: { visibility: 'unranked' },
      }),
    );

    await driverRepo.create(
      Driver.create({
        id: params.adminId,
        iracingId: '2001',
        name: 'Admin Driver',
        country: 'DE',
      }),
    );

    await driverRepo.create(
      Driver.create({
        id: params.memberId,
        iracingId: '2002',
        name: 'Member Driver',
        country: 'DE',
      }),
    );

    await membershipRepo.saveMembership(
      LeagueMembership.create({
        leagueId: params.leagueId,
        driverId: params.adminId,
        role: 'admin',
        status: 'active',
      }),
    );

    await membershipRepo.saveMembership(
      LeagueMembership.create({
        leagueId: params.leagueId,
        driverId: params.memberId,
        role: 'member',
        status: 'active',
      }),
    );
  }

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [LeagueModule],
    }).compile();

    app = module.createNestApplication();

    // Required for getActorFromRequestContext() used by requireLeagueAdminOrOwner().
    app.use(requestContextMiddleware as any);

    // Test-only auth injection: emulate an authenticated session by setting request.user.
    app.use((req: any, _res: any, next: any) => {
      const userId = req.headers['x-test-user-id'];
      if (typeof userId === 'string' && userId.length > 0) {
        req.user = { userId };
      }
      next();
    });

    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );

    const reflector = new Reflector();
    app.useGlobalGuards(
      new AuthenticationGuard(sessionPort as any),
      new AuthorizationGuard(reflector, authorizationService as any),
    );

    await app.init();
  });

  afterEach(async () => {
    await app?.close();
    vi.clearAllMocks();
  });

  it('returns 401 when unauthenticated (role change + removal)', async () => {
    await seedLeagueWithMembers({ leagueId: 'league-1', adminId: 'admin-1', memberId: 'driver-2' });

    await request(app.getHttpServer())
      .patch('/leagues/league-1/admin/roster/members/driver-2/role')
      .send({ newRole: 'steward' })
      .expect(401);

    await request(app.getHttpServer()).patch('/leagues/league-1/admin/roster/members/driver-2/remove').expect(401);
  });

  it('returns 403 when authenticated but not admin/owner (role change + removal)', async () => {
    await seedLeagueWithMembers({ leagueId: 'league-1', adminId: 'admin-1', memberId: 'driver-2' });

    await request(app.getHttpServer())
      .patch('/leagues/league-1/admin/roster/members/driver-2/role')
      .set('x-test-user-id', 'user-2')
      .send({ newRole: 'steward' })
      .expect(403);

    await request(app.getHttpServer())
      .patch('/leagues/league-1/admin/roster/members/driver-2/remove')
      .set('x-test-user-id', 'user-2')
      .expect(403);
  });

  it('role change is reflected in roster members read', async () => {
    await seedLeagueWithMembers({ leagueId: 'league-1', adminId: 'admin-1', memberId: 'driver-2' });

    await request(app.getHttpServer())
      .patch('/leagues/league-1/admin/roster/members/driver-2/role')
      .set('x-test-user-id', 'admin-1')
      .send({ newRole: 'steward' })
      .expect(200);

    const members = await request(app.getHttpServer())
      .get('/leagues/league-1/admin/roster/members')
      .set('x-test-user-id', 'admin-1')
      .expect(200);

    expect(Array.isArray(members.body)).toBe(true);

    const updated = (members.body as any[]).find(m => m.driverId === 'driver-2');
    expect(updated).toBeDefined();
    expect(updated.role).toBe('steward');
  });

  it('member removal is reflected in roster members read', async () => {
    await seedLeagueWithMembers({ leagueId: 'league-1', adminId: 'admin-1', memberId: 'driver-2' });

    await request(app.getHttpServer())
      .patch('/leagues/league-1/admin/roster/members/driver-2/remove')
      .set('x-test-user-id', 'admin-1')
      .expect(200);

    const members = await request(app.getHttpServer())
      .get('/leagues/league-1/admin/roster/members')
      .set('x-test-user-id', 'admin-1')
      .expect(200);

    expect(Array.isArray(members.body)).toBe(true);
    expect((members.body as any[]).some(m => m.driverId === 'driver-2')).toBe(false);
  });
});