import 'reflect-metadata';

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

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

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

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

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

describe('League roster join request mutations (HTTP)', () => {
  let app: import("@nestjs/common").INestApplication;

  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 seedLeagueWithJoinRequest(params: {
    leagueId: string;
    adminId: string;
    requesterId: string;
    joinRequestId: string;
    maxDrivers?: number;
    extraActiveMemberId?: 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', ...(params.maxDrivers !== undefined ? { maxDrivers: params.maxDrivers } : {}) },
      }),
    );

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

    await driverRepo.create(
      Driver.create({
        id: params.requesterId,
        iracingId: '1002',
        name: 'Requester Driver',
        country: 'DE',
      }),
    );

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

    if (params.extraActiveMemberId) {
      await driverRepo.create(
        Driver.create({
          id: params.extraActiveMemberId,
          iracingId: '1003',
          name: 'Extra Member',
          country: 'DE',
        }),
      );

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

    await membershipRepo.saveJoinRequest(
      JoinRequest.create({
        id: params.joinRequestId,
        leagueId: params.leagueId,
        driverId: params.requesterId,
        requestedAt: new Date('2025-01-01T12:00:00Z'),
        message: 'please',
      }),
    );
  }

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

    app = module.createNestApplication();

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

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

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

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

    await app.init();
  });

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

  it('returns 401 when unauthenticated', async () => {
    await seedLeagueWithJoinRequest({
      leagueId: 'league-1',
      adminId: 'admin-1',
      requesterId: 'driver-2',
      joinRequestId: 'jr-1',
    });

    await request(app.getHttpServer())
      .post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
      .expect(401);
  });

  it('returns 403 when authenticated but not admin/owner', async () => {
    await seedLeagueWithJoinRequest({
      leagueId: 'league-1',
      adminId: 'admin-1',
      requesterId: 'driver-2',
      joinRequestId: 'jr-1',
    });

    await request(app.getHttpServer())
      .post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
      .set('x-test-user-id', 'user-2')
      .expect(403);
  });

  it('approve removes request and adds member; roster reads reflect changes', async () => {
    await seedLeagueWithJoinRequest({
      leagueId: 'league-1',
      adminId: 'admin-1',
      requesterId: 'driver-2',
      joinRequestId: 'jr-1',
    });

    await request(app.getHttpServer())
      .post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
      .set('x-test-user-id', 'admin-1')
      .expect(200);

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

    expect(Array.isArray(joinRequests.body)).toBe(true);
    expect(joinRequests.body.find((r: unknown) => (r as { id: string }).id === 'jr-1')).toBeUndefined();

    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.some((m: unknown) => (m as { driverId: string }).driverId === 'driver-2')).toBe(true);
  });

  it('reject removes request only; roster reads reflect changes', async () => {
    await seedLeagueWithJoinRequest({
      leagueId: 'league-1',
      adminId: 'admin-1',
      requesterId: 'driver-2',
      joinRequestId: 'jr-1',
    });

    await request(app.getHttpServer())
      .post('/leagues/league-1/admin/roster/join-requests/jr-1/reject')
      .set('x-test-user-id', 'admin-1')
      .expect(200);

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

    expect(Array.isArray(joinRequests.body)).toBe(true);
    expect(joinRequests.body.find((r: unknown) => (r as { id: string }).id === 'jr-1')).toBeUndefined();

    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.some((m: unknown) => (m as { driverId: string }).driverId === 'driver-2')).toBe(false);
  });

  it('approve returns error when league is full and keeps request pending', async () => {
    await seedLeagueWithJoinRequest({
      leagueId: 'league-1',
      adminId: 'admin-1',
      requesterId: 'driver-2',
      joinRequestId: 'jr-1',
      maxDrivers: 2,
      extraActiveMemberId: 'driver-3',
    });

    await request(app.getHttpServer())
      .post('/leagues/league-1/admin/roster/join-requests/jr-1/approve')
      .set('x-test-user-id', 'admin-1')
      .expect(409);

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

    expect(Array.isArray(joinRequests.body)).toBe(true);
    expect(joinRequests.body.find((r: unknown) => (r as { id: string }).id === 'jr-1')).toBeDefined();
  });
});