import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import {
  GetRaceDetailUseCase,
  type GetRaceDetailInput,
  type GetRaceDetailResult,
  type GetRaceDetailErrorCode,
} from './GetRaceDetailUseCase';
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import { Race } from '../../domain/entities/Race';

describe('GetRaceDetailUseCase', () => {
  let useCase: GetRaceDetailUseCase;
  let raceRepository: { findById: Mock };
  let leagueRepository: { findById: Mock };
  let driverRepository: { findById: Mock };
  let raceRegistrationRepository: { findByRaceId: Mock };
  let resultRepository: { findByRaceId: Mock };
  let leagueMembershipRepository: { getMembership: Mock };
  let output: UseCaseOutputPort<GetRaceDetailResult> & { present: Mock };

  beforeEach(() => {
    raceRepository = { findById: vi.fn() };
    leagueRepository = { findById: vi.fn() };
    driverRepository = { findById: vi.fn() };
    raceRegistrationRepository = { findByRaceId: vi.fn() };
    resultRepository = { findByRaceId: vi.fn() };
    leagueMembershipRepository = { getMembership: vi.fn() };
    output = { present: vi.fn() } as UseCaseOutputPort<GetRaceDetailResult> & { present: Mock };

    useCase = new GetRaceDetailUseCase(
      raceRepository as unknown as IRaceRepository,
      leagueRepository as unknown as ILeagueRepository,
      driverRepository as unknown as IDriverRepository,
      raceRegistrationRepository as unknown as IRaceRegistrationRepository,
      resultRepository as unknown as IResultRepository,
      leagueMembershipRepository as unknown as ILeagueMembershipRepository,
      output,
    );
  });

  it('should present race detail when race exists', async () => {
    const raceId = 'race-1';
    const driverId = 'driver-1';
    const race = Race.create({
      id: raceId,
      leagueId: 'league-1',
      track: 'Track 1',
      car: 'Car 1',
      scheduledAt: new Date('2099-01-01T10:00:00Z'),
      status: 'scheduled',
      strengthOfField: 1500,
      registeredCount: 10,
      maxParticipants: 20,
    });
    const league = {
      id: 'league-1',
      name: 'League 1',
      description: 'Description',
      settings: { maxDrivers: 20, qualifyingFormat: 'ladder' },
    };
    const registrations = [
      { driverId: { toString: () => 'driver-1' } },
      { driverId: { toString: () => 'driver-2' } },
    ];
    const membership = { status: { toString: () => 'active' } };
    const drivers = [
      { id: 'driver-1', name: 'Driver 1', country: 'US' },
      { id: 'driver-2', name: 'Driver 2', country: 'UK' },
    ];

    raceRepository.findById.mockResolvedValue(race);
    leagueRepository.findById.mockResolvedValue(league);
    raceRegistrationRepository.findByRaceId.mockResolvedValue(registrations);
    leagueMembershipRepository.getMembership.mockResolvedValue(membership);
    driverRepository.findById.mockImplementation((id: string) =>
      Promise.resolve(drivers.find(d => d.id === id) || null),
    );
    resultRepository.findByRaceId.mockResolvedValue([]);

    const input: GetRaceDetailInput = { raceId, driverId };
    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 GetRaceDetailResult;
    expect(presented.race).toEqual(race);
    expect(presented.league).toEqual(league);
    expect(presented.registrations).toEqual(registrations);
    expect(presented.drivers).toHaveLength(2);
    expect(presented.isUserRegistered).toBe(true);
    expect(presented.canRegister).toBe(true);
    expect(presented.userResult).toBeNull();
  });

  it('should return error when race not found', async () => {
    raceRepository.findById.mockResolvedValue(null);

    const input: GetRaceDetailInput = { raceId: 'race-1', driverId: 'driver-1' };
    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr() as ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>;
    expect(err.code).toBe('RACE_NOT_FOUND');
    expect(err.details?.message).toBe('Race not found');
    expect(output.present).not.toHaveBeenCalled();
  });

  it('should include user result when race is completed', async () => {
    const raceId = 'race-1';
    const driverId = 'driver-1';
    const race = Race.create({
      id: raceId,
      leagueId: 'league-1',
      track: 'Track 1',
      car: 'Car 1',
      scheduledAt: new Date('2023-01-01T10:00:00Z'),
      status: 'completed',
    });
    const registrations: Array<{ driverId: { toString: () => string } }> = [];
    const userDomainResult = {
      driverId: { toString: () => driverId },
    } as unknown as { driverId: { toString: () => string } };

    raceRepository.findById.mockResolvedValue(race);
    leagueRepository.findById.mockResolvedValue(null);
    raceRegistrationRepository.findByRaceId.mockResolvedValue(registrations);
    leagueMembershipRepository.getMembership.mockResolvedValue(null);
    driverRepository.findById.mockResolvedValue(null);
    resultRepository.findByRaceId.mockResolvedValue([userDomainResult]);

    const input: GetRaceDetailInput = { raceId, driverId };
    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 GetRaceDetailResult;
    expect(presented.userResult).toBe(userDomainResult);
    expect(presented.race).toEqual(race);
    expect(presented.league).toBeNull();
    expect(presented.registrations).toEqual(registrations);
  });

  it('should wrap repository errors', async () => {
    const error = new Error('db down');
    raceRepository.findById.mockRejectedValue(error);

    const input: GetRaceDetailInput = { raceId: 'race-1', driverId: 'driver-1' };
    const result = await useCase.execute(input);

    expect(result.isErr()).toBe(true);
    const err = result.unwrapErr() as ApplicationErrorCode<GetRaceDetailErrorCode, { message: string }>;
    expect(err.code).toBe('REPOSITORY_ERROR');
    expect(err.details.message).toBe('db down');
    expect(output.present).not.toHaveBeenCalled();
  });
});