import 'reflect-metadata';

import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { vi } from 'vitest';
import { DriverController } from './DriverController';
import { DriverService } from './DriverService';
import type { Request } from 'express';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';

interface AuthenticatedRequest extends Request {
  user?: { userId: string };
}
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
import { DriverStatsDTO } from './dtos/DriverStatsDTO';
import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO';
import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO';
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';

describe('DriverController', () => {
  let controller: DriverController;
  let service: ReturnType<typeof vi.mocked<DriverService>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [DriverController],
      providers: [
        {
          provide: DriverService,
          useValue: {
            getDriversLeaderboard: vi.fn(),
            getTotalDrivers: vi.fn(),
            getCurrentDriver: vi.fn(),
            completeOnboarding: vi.fn(),
            getDriverRegistrationStatus: vi.fn(),
            getDriver: vi.fn(),
            getDriverProfile: vi.fn(),
            updateDriverProfile: vi.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<DriverController>(DriverController);
    service = vi.mocked(module.get(DriverService));
  });

  describe('getDriversLeaderboard', () => {
    it('should return drivers leaderboard', async () => {
      const leaderboard: DriversLeaderboardDTO = { drivers: [], totalRaces: 0, totalWins: 0, activeCount: 0 };
      service.getDriversLeaderboard.mockResolvedValue(leaderboard);

      const result = await controller.getDriversLeaderboard();

      expect(service.getDriversLeaderboard).toHaveBeenCalled();
      expect(result).toEqual(leaderboard);
    });
  });

  describe('getTotalDrivers', () => {
    it('should return total drivers stats', async () => {
      const stats: DriverStatsDTO = { totalDrivers: 100 };
      service.getTotalDrivers.mockResolvedValue(stats);

      const result = await controller.getTotalDrivers();

      expect(service.getTotalDrivers).toHaveBeenCalled();
      expect(result).toEqual(stats);
    });
  });

  describe('getCurrentDriver', () => {
    it('should return current driver if userId exists', async () => {
      const userId = 'user-123';
      const driver: GetDriverOutputDTO = { id: 'driver-123', name: 'Driver' } as GetDriverOutputDTO;
      service.getCurrentDriver.mockResolvedValue(driver);

      const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };

      const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);

      expect(service.getCurrentDriver).toHaveBeenCalledWith(userId);
      expect(result).toEqual(driver);
    });

    it('should return null if no userId', async () => {
      const mockReq: Partial<AuthenticatedRequest> = {};

      const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);

      expect(service.getCurrentDriver).not.toHaveBeenCalled();
      expect(result).toBeNull();
    });
  });

  describe('completeOnboarding', () => {
    it('should complete onboarding', async () => {
      const userId = 'user-123';
      const input: CompleteOnboardingInputDTO = { someField: 'value' } as unknown as CompleteOnboardingInputDTO;
      const output: CompleteOnboardingOutputDTO = { success: true };
      service.completeOnboarding.mockResolvedValue(output);

      const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };

      const result = await controller.completeOnboarding(input, mockReq as AuthenticatedRequest);

      expect(service.completeOnboarding).toHaveBeenCalledWith(userId, input);
      expect(result).toEqual(output);
    });
  });

  describe('getDriverRegistrationStatus', () => {
    it('should return registration status', async () => {
      const driverId = 'driver-123';
      const raceId = 'race-456';
      const status: DriverRegistrationStatusDTO = { registered: true } as unknown as DriverRegistrationStatusDTO;
      service.getDriverRegistrationStatus.mockResolvedValue(status);

      const result = await controller.getDriverRegistrationStatus(driverId, raceId);

      expect(service.getDriverRegistrationStatus).toHaveBeenCalledWith({ driverId, raceId });
      expect(result).toEqual(status);
    });
  });

  describe('getDriver', () => {
    it('should return driver by id', async () => {
      const driverId = 'driver-123';
      const driver: GetDriverOutputDTO = { id: driverId, name: 'Driver' } as GetDriverOutputDTO;
      service.getDriver.mockResolvedValue(driver);

      const result = await controller.getDriver(driverId);

      expect(service.getDriver).toHaveBeenCalledWith(driverId);
      expect(result).toEqual(driver);
    });
  });

  describe('getDriverProfile', () => {
    it('should return driver profile', async () => {
      const driverId = 'driver-123';
      const profile: GetDriverProfileOutputDTO = { id: driverId, bio: 'Bio' } as unknown as GetDriverProfileOutputDTO;
      service.getDriverProfile.mockResolvedValue(profile);

      const result = await controller.getDriverProfile(driverId);

      expect(service.getDriverProfile).toHaveBeenCalledWith(driverId);
      expect(result).toEqual(profile);
    });
  });

  describe('updateDriverProfile', () => {
    it('should update driver profile', async () => {
      const driverId = 'driver-123';
      const body = { bio: 'New bio', country: 'US' };
      const updated: GetDriverOutputDTO = { id: driverId, name: 'Driver' } as unknown as GetDriverOutputDTO;
      service.updateDriverProfile.mockResolvedValue(updated);

      const result = await controller.updateDriverProfile(driverId, body);

      expect(service.updateDriverProfile).toHaveBeenCalledWith(driverId, body.bio, body.country);
      expect(result).toEqual(updated);
    });
  });

  describe('auth guards (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(() => []),
    };

    const policyService: Pick<PolicyService, 'getSnapshot'> = {
      getSnapshot: vi.fn(async (): Promise<PolicySnapshot> => ({
        policyVersion: 1,
        operationalMode: 'normal',
        maintenanceAllowlist: { view: [], mutate: [] },
        capabilities: {},
        loadedFrom: 'defaults',
        loadedAtIso: new Date(0).toISOString(),
      })),
    };

    beforeEach(async () => {
      const module = await Test.createTestingModule({
        controllers: [DriverController],
        providers: [
          {
            provide: DriverService,
            useValue: {
              getDriversLeaderboard: vi.fn(async () => ({ drivers: [], totalRaces: 0, totalWins: 0, activeCount: 0 })),
              getCurrentDriver: vi.fn(async () => ({ id: 'd1' })),
            },
          },
        ],
      }).compile();

      app = module.createNestApplication();

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

      await app.init();
    });

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

    it('allows @Public() endpoint without a session', async () => {
      await request(app.getHttpServer()).get('/drivers/leaderboard').expect(200);
    });

    it('denies non-public endpoint by default when not authenticated (401)', async () => {
      await request(app.getHttpServer()).get('/drivers/current').expect(401);
    });

    it('allows non-public endpoint when authenticated via session port', async () => {
      vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
        token: 't',
        user: { id: 'user-1' },
      });

      await request(app.getHttpServer()).get('/drivers/current').expect(200);
    });
  });
});