import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import type { DriverRepository } from '../../domain/repositories/DriverRepository';
import type { PenaltyRepository } from '../../domain/repositories/PenaltyRepository';
import type { RaceRepository } from '../../domain/repositories/RaceRepository';
import type { ResultRepository } from '../../domain/repositories/ResultRepository';
import type { StandingRepository } from '../../domain/repositories/StandingRepository';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
import {
    GetLeagueDriverSeasonStatsUseCase,
    type GetLeagueDriverSeasonStatsErrorCode,
    type GetLeagueDriverSeasonStatsInput,
} from './GetLeagueDriverSeasonStatsUseCase';

describe('GetLeagueDriverSeasonStatsUseCase', () => {
  const mockStandingFindByLeagueId = vi.fn();
  const mockResultFindByDriverIdAndLeagueId = vi.fn();
  const mockPenaltyFindByRaceId = vi.fn();
  const mockRaceFindByLeagueId = vi.fn();
  const mockDriverRatingGetRating = vi.fn();
  const mockDriverFindById = vi.fn();

  let useCase: GetLeagueDriverSeasonStatsUseCase;
  let standingRepository: {
    findByLeagueId: Mock;
    findByDriverIdAndLeagueId: Mock;
    findAll: Mock;
    save: Mock;
    saveMany: Mock;
    delete: Mock;
    deleteByLeagueId: Mock;
    exists: Mock;
    recalculate: Mock;
  };
  let resultRepository: {
    findById: Mock;
    findAll: Mock;
    findByRaceId: Mock;
    findByDriverId: Mock;
    findByDriverIdAndLeagueId: Mock;
    create: Mock;
    createMany: Mock;
    update: Mock;
    delete: Mock;
    deleteByRaceId: Mock;
    exists: Mock;
    existsByRaceId: Mock;
  };
  let penaltyRepository: {
    findById: Mock;
    findByDriverId: Mock;
    findByProtestId: Mock;
    findPending: Mock;
    findByRaceId: Mock;
    findIssuedBy: Mock;
    create: Mock;
    update: Mock;
    exists: Mock;
  };
  let raceRepository: {
    findById: Mock;
    findAll: Mock;
    findByLeagueId: Mock;
    findUpcomingByLeagueId: Mock;
    findCompletedByLeagueId: Mock;
    findByStatus: Mock;
    findByDateRange: Mock;
    create: Mock;
    update: Mock;
    delete: Mock;
    exists: Mock;
  };
  let driverRepository: {
    findById: Mock;
    findByIRacingId: Mock;
    findAll: Mock;
    create: Mock;
    update: Mock;
    delete: Mock;
    exists: Mock;
    existsByIRacingId: Mock;
  };
  let driverRatingPort: {
    getDriverRating: Mock;
    calculateRatingChange: Mock;
    updateDriverRating: Mock;
  };

  beforeEach(() => {
    mockStandingFindByLeagueId.mockReset();
    mockResultFindByDriverIdAndLeagueId.mockReset();
    mockPenaltyFindByRaceId.mockReset();
    mockRaceFindByLeagueId.mockReset();
    mockDriverRatingGetRating.mockReset();
    mockDriverFindById.mockReset();

    standingRepository = {
      findByLeagueId: mockStandingFindByLeagueId,
      findByDriverIdAndLeagueId: vi.fn(),
      findAll: vi.fn(),
      save: vi.fn(),
      saveMany: vi.fn(),
      delete: vi.fn(),
      deleteByLeagueId: vi.fn(),
      exists: vi.fn(),
      recalculate: vi.fn(),
    };
    resultRepository = {
      findById: vi.fn(),
      findAll: vi.fn(),
      findByRaceId: vi.fn(),
      findByDriverId: vi.fn(),
      findByDriverIdAndLeagueId: mockResultFindByDriverIdAndLeagueId,
      create: vi.fn(),
      createMany: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      deleteByRaceId: vi.fn(),
      exists: vi.fn(),
      existsByRaceId: vi.fn(),
    };
    penaltyRepository = {
      findById: vi.fn(),
      findByDriverId: vi.fn(),
      findByProtestId: vi.fn(),
      findPending: vi.fn(),
      findByRaceId: mockPenaltyFindByRaceId,
      findIssuedBy: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      exists: vi.fn(),
    };
    raceRepository = {
      findById: vi.fn(),
      findAll: vi.fn(),
      findByLeagueId: mockRaceFindByLeagueId,
      findUpcomingByLeagueId: vi.fn(),
      findCompletedByLeagueId: vi.fn(),
      findByStatus: vi.fn(),
      findByDateRange: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      exists: vi.fn(),
    };
    driverRepository = {
      findById: mockDriverFindById,
      findByIRacingId: vi.fn(),
      findAll: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
      delete: vi.fn(),
      exists: vi.fn(),
      existsByIRacingId: vi.fn(),
    };
    driverRatingPort = {
      getDriverRating: mockDriverRatingGetRating,
      calculateRatingChange: vi.fn(),
      updateDriverRating: vi.fn(),
    };

    useCase = new GetLeagueDriverSeasonStatsUseCase(
      standingRepository as unknown as StandingRepository,
      resultRepository as unknown as ResultRepository,
      penaltyRepository as unknown as PenaltyRepository,
      raceRepository as unknown as RaceRepository,
      driverRepository as unknown as DriverRepository,
      driverRatingPort as unknown as DriverRatingPort
    );
  });

  it('should return league driver season stats for given league id', async () => {
    const input: GetLeagueDriverSeasonStatsInput = { leagueId: 'league-1' };

    const mockStandings = [
      {
        driverId: { toString: () => 'driver-1' },
        position: { toNumber: () => 1 },
        points: { toNumber: () => 100 },
      },
      {
        driverId: { toString: () => 'driver-2' },
        position: { toNumber: () => 2 },
        points: { toNumber: () => 80 },
      },
    ];
    const mockRaces = [
      { id: 'race-1' },
      { id: 'race-2' },
      { id: 'race-3' },
      { id: 'race-4' },
      { id: 'race-5' },
    ];
    const mockPenalties = [
      { driverId: 'driver-1', status: 'applied', type: 'points_deduction', value: 10 },
    ];

    const mockDriver1Results = [
      { position: { toNumber: () => 1 } },
      { position: { toNumber: () => 1 } },
      { position: { toNumber: () => 1 } },
      { position: { toNumber: () => 1 } },
      { position: { toNumber: () => 1 } },
    ];
    const mockDriver2Results = [
      { position: { toNumber: () => 2 } },
      { position: { toNumber: () => 2 } },
      { position: { toNumber: () => 2 } },
      { position: { toNumber: () => 2 } },
      { position: { toNumber: () => 2 } },
    ];

    mockStandingFindByLeagueId.mockResolvedValue(mockStandings);
    mockRaceFindByLeagueId.mockResolvedValue(mockRaces);
    mockPenaltyFindByRaceId.mockImplementation((raceId: string) => {
      if (raceId === 'race-1') return Promise.resolve(mockPenalties);
      return Promise.resolve([]);
    });

    mockDriverRatingGetRating.mockImplementation((driverId: string) => {
      if (driverId === 'driver-1') return Promise.resolve(1500);
      if (driverId === 'driver-2') return Promise.resolve(1400);
      return Promise.resolve(null);
    });

    mockResultFindByDriverIdAndLeagueId.mockImplementation((driverId: string) => {
      if (driverId === 'driver-1') return Promise.resolve(mockDriver1Results);
      if (driverId === 'driver-2') return Promise.resolve(mockDriver2Results);
      return Promise.resolve([]);
    });

    mockDriverFindById.mockImplementation((id: string) => {
      if (id === 'driver-1') return Promise.resolve({ id: 'driver-1', name: { toString: () => 'Driver One' } });
      if (id === 'driver-2') return Promise.resolve({ id: 'driver-2', name: { toString: () => 'Driver Two' } });
      return Promise.resolve(null);
    });

    const result = await useCase.execute(input);

    expect(result.isOk()).toBe(true);
    const presented = result.unwrap();
    expect(presented.leagueId).toBe('league-1');
    expect(presented.stats).toHaveLength(2);

    expect(presented.stats[0]).toEqual({
      leagueId: 'league-1',
      driverId: 'driver-1',
      position: 1,
      driverName: 'Driver One',
      teamId: undefined,
      teamName: undefined,
      totalPoints: 100,
      basePoints: 110,
      penaltyPoints: -10,
      bonusPoints: 0,
      pointsPerRace: 20,
      racesStarted: 5,
      racesFinished: 5,
      dnfs: 0,
      noShows: 0,
      avgFinish: 1,
      rating: 1500,
      ratingChange: null,
    });
  });

  it('should handle no penalties', async () => {
    const input: GetLeagueDriverSeasonStatsInput = { leagueId: 'league-1' };

    const mockStandings = [
      {
        driverId: { toString: () => 'driver-1' },
        position: { toNumber: () => 1 },
        points: { toNumber: () => 100 },
      },
    ];
    const mockRaces = [{ id: 'race-1' }];
    const mockResults = [{ position: { toNumber: () => 1 } }];

    mockStandingFindByLeagueId.mockResolvedValue(mockStandings);
    mockRaceFindByLeagueId.mockResolvedValue(mockRaces);
    mockPenaltyFindByRaceId.mockResolvedValue([]);
    mockDriverRatingGetRating.mockResolvedValue(null);
    mockResultFindByDriverIdAndLeagueId.mockResolvedValue(mockResults);
    mockDriverFindById.mockResolvedValue({ id: 'driver-1', name: { toString: () => 'Driver One' } });

    const result = await useCase.execute(input);

    expect(result.isOk()).toBe(true);
    const presented = result.unwrap();
    expect(presented.stats[0]?.penaltyPoints).toBe(0);
  });

  it('should return LEAGUE_NOT_FOUND when no standings are found', async () => {
    const input: GetLeagueDriverSeasonStatsInput = { leagueId: 'missing-league' };

    mockStandingFindByLeagueId.mockResolvedValue([]);
    mockRaceFindByLeagueId.mockResolvedValue([]);

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr() as ApplicationErrorCode<
      GetLeagueDriverSeasonStatsErrorCode,
      { message: string }
    >;
    expect(err.code).toBe('LEAGUE_NOT_FOUND');
    expect(err.details.message).toBe('League not found');
  });

  it('should return REPOSITORY_ERROR when an unexpected error occurs', async () => {
    const input: GetLeagueDriverSeasonStatsInput = { leagueId: 'league-1' };
    const thrown = new Error('repository failure');

    mockStandingFindByLeagueId.mockRejectedValue(thrown);

    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr() as ApplicationErrorCode<
      GetLeagueDriverSeasonStatsErrorCode,
      { message: string }
    >;
    expect(err.code).toBe('REPOSITORY_ERROR');
    expect(err.details.message).toBe('repository failure');
  });
});
