import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
import type { DriverRatingPort } from '../ports/DriverRatingPort';
import {
  GetLeagueDriverSeasonStatsUseCase,
  type GetLeagueDriverSeasonStatsErrorCode,
  type GetLeagueDriverSeasonStatsInput,
  type GetLeagueDriverSeasonStatsResult,
} 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();
  const mockTeamFindById = vi.fn();

  let useCase: GetLeagueDriverSeasonStatsUseCase;
  let standingRepository: IStandingRepository;
  let resultRepository: IResultRepository;
  let penaltyRepository: IPenaltyRepository;
  let raceRepository: IRaceRepository;
  let driverRepository: IDriverRepository;
  let driverRatingPort: DriverRatingPort;
  let output: UseCaseOutputPort<GetLeagueDriverSeasonStatsResult> & { present: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockStandingFindByLeagueId.mockReset();
    mockResultFindByDriverIdAndLeagueId.mockReset();
    mockPenaltyFindByRaceId.mockReset();
    mockRaceFindByLeagueId.mockReset();
    mockDriverRatingGetRating.mockReset();
    mockDriverFindById.mockReset();
    mockTeamFindById.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(),
    };

    output = {
      present: vi.fn(),
    } as unknown as UseCaseOutputPort<GetLeagueDriverSeasonStatsResult> & {
      present: ReturnType<typeof vi.fn>;
    };

    useCase = new GetLeagueDriverSeasonStatsUseCase(
      standingRepository,
      resultRepository,
      penaltyRepository,
      raceRepository,
      driverRepository,
      driverRatingPort,
      output,
    );
  });

  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);
    expect(result.unwrap()).toBeUndefined();
    expect(output.present).toHaveBeenCalledTimes(1);

    const presented = output.present.mock.calls[0]?.[0] as GetLeagueDriverSeasonStatsResult;
    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);
    expect(result.unwrap()).toBeUndefined();
    expect(output.present).toHaveBeenCalledTimes(1);

    const presented = output.present.mock.calls[0]?.[0] as GetLeagueDriverSeasonStatsResult;
    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');
    expect(output.present).not.toHaveBeenCalled();
  });

  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');
    expect(output.present).not.toHaveBeenCalled();
  });
});
