import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LeagueService } from './LeagueService';
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import type { LeagueSummaryPresenter } from '../../presenters/LeagueSummaryPresenter';
import type { LeagueStandingsPresenter } from '../../presenters/LeagueStandingsPresenter';
import type {
  AllLeaguesWithCapacityDto,
  LeagueStandingsDto,
  LeagueStatsDto,
  LeagueScheduleDto,
  LeagueMembershipsDto,
  CreateLeagueInputDto,
  CreateLeagueOutputDto,
} from '../../dtos';
import type { LeagueSummaryViewModel, LeagueStandingsViewModel } from '../../view-models';

describe('LeagueService', () => {
  let service: LeagueService;
  let mockApiClient: LeaguesApiClient;
  let mockLeagueSummaryPresenter: LeagueSummaryPresenter;
  let mockLeagueStandingsPresenter: LeagueStandingsPresenter;

  beforeEach(() => {
    mockApiClient = {
      getAllWithCapacity: vi.fn(),
      getTotal: vi.fn(),
      getStandings: vi.fn(),
      getSchedule: vi.fn(),
      getMemberships: vi.fn(),
      create: vi.fn(),
      removeMember: vi.fn(),
    } as unknown as LeaguesApiClient;

    mockLeagueSummaryPresenter = {
      present: vi.fn(),
    } as unknown as LeagueSummaryPresenter;

    mockLeagueStandingsPresenter = {
      present: vi.fn(),
    } as unknown as LeagueStandingsPresenter;

    service = new LeagueService(
      mockApiClient,
      mockLeagueSummaryPresenter,
      mockLeagueStandingsPresenter
    );
  });

  describe('constructor', () => {
    it('should create instance with injected dependencies', () => {
      expect(service).toBeInstanceOf(LeagueService);
    });
  });

  describe('getAllLeagues', () => {
    it('should fetch all leagues from API and transform via presenter', async () => {
      // Arrange
      const mockDto: AllLeaguesWithCapacityDto = {
        leagues: [
          {
            id: 'league-1',
            name: 'Championship League',
            description: 'Top tier racing',
            memberCount: 10,
            maxMembers: 20,
            isPublic: true,
            ownerId: 'owner-1',
          },
          {
            id: 'league-2',
            name: 'Rookie League',
            description: 'Entry level racing',
            memberCount: 5,
            maxMembers: 15,
            isPublic: true,
            ownerId: 'owner-2',
          },
        ],
      };

      const mockViewModels: LeagueSummaryViewModel[] = [
        {
          id: 'league-1',
          name: 'Championship League',
          description: 'Top tier racing',
          memberCount: 10,
          maxMembers: 20,
          isPublic: true,
          ownerId: 'owner-1',
        } as LeagueSummaryViewModel,
        {
          id: 'league-2',
          name: 'Rookie League',
          description: 'Entry level racing',
          memberCount: 5,
          maxMembers: 15,
          isPublic: true,
          ownerId: 'owner-2',
        } as LeagueSummaryViewModel,
      ];

      vi.mocked(mockApiClient.getAllWithCapacity).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueSummaryPresenter.present).mockReturnValue(mockViewModels);

      // Act
      const result = await service.getAllLeagues();

      // Assert
      expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
      expect(mockLeagueSummaryPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual(mockViewModels);
    });

    it('should handle empty leagues list', async () => {
      // Arrange
      const mockDto: AllLeaguesWithCapacityDto = {
        leagues: [],
      };

      const mockViewModels: LeagueSummaryViewModel[] = [];

      vi.mocked(mockApiClient.getAllWithCapacity).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueSummaryPresenter.present).mockReturnValue(mockViewModels);

      // Act
      const result = await service.getAllLeagues();

      // Assert
      expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
      expect(mockLeagueSummaryPresenter.present).toHaveBeenCalledWith(mockDto);
      expect(result).toEqual([]);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch leagues');
      vi.mocked(mockApiClient.getAllWithCapacity).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getAllLeagues()).rejects.toThrow('Failed to fetch leagues');
      expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
      expect(mockLeagueSummaryPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('getLeagueStandings', () => {
    it('should fetch league standings and transform via presenter', async () => {
      // Arrange
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      const mockDto: LeagueStandingsDto = {
        standings: [
          {
            position: 1,
            driverId: 'driver-1',
            points: 100,
          },
          {
            position: 2,
            driverId: 'driver-2',
            points: 85,
          },
        ],
        drivers: [],
        memberships: [],
      };

      const mockViewModel: LeagueStandingsViewModel = {
        standings: [
          {
            position: 1,
            driverId: 'driver-1',
            points: 100,
          },
          {
            position: 2,
            driverId: 'driver-2',
            points: 85,
          },
        ],
        drivers: [],
        memberships: [],
      } as LeagueStandingsViewModel;

      vi.mocked(mockApiClient.getStandings).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueStandingsPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getLeagueStandings(leagueId, currentUserId);

      // Assert
      expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
      expect(mockLeagueStandingsPresenter.present).toHaveBeenCalledWith(
        expect.objectContaining(mockDto),
        currentUserId
      );
      expect(result).toEqual(mockViewModel);
    });

    it('should handle empty standings', async () => {
      // Arrange
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      const mockDto: LeagueStandingsDto = {
        standings: [],
        drivers: [],
        memberships: [],
      };

      const mockViewModel: LeagueStandingsViewModel = {
        standings: [],
        drivers: [],
        memberships: [],
      } as LeagueStandingsViewModel;

      vi.mocked(mockApiClient.getStandings).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueStandingsPresenter.present).mockReturnValue(mockViewModel);

      // Act
      const result = await service.getLeagueStandings(leagueId, currentUserId);

      // Assert
      expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
      expect(mockLeagueStandingsPresenter.present).toHaveBeenCalled();
      expect(result).toEqual(mockViewModel);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const leagueId = 'league-123';
      const currentUserId = 'user-456';
      const error = new Error('Failed to fetch standings');
      vi.mocked(mockApiClient.getStandings).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('Failed to fetch standings');
      expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
      expect(mockLeagueStandingsPresenter.present).not.toHaveBeenCalled();
    });
  });

  describe('getLeagueStats', () => {
    it('should fetch league statistics', async () => {
      // Arrange
      const mockStats: LeagueStatsDto = {
        totalLeagues: 42,
      };

      vi.mocked(mockApiClient.getTotal).mockResolvedValue(mockStats);

      // Act
      const result = await service.getLeagueStats();

      // Assert
      expect(mockApiClient.getTotal).toHaveBeenCalled();
      expect(result).toEqual(mockStats);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const error = new Error('Failed to fetch stats');
      vi.mocked(mockApiClient.getTotal).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getLeagueStats()).rejects.toThrow('Failed to fetch stats');
      expect(mockApiClient.getTotal).toHaveBeenCalled();
    });
  });

  describe('getLeagueSchedule', () => {
    it('should fetch league schedule', async () => {
      // Arrange
      const leagueId = 'league-123';
      const mockSchedule: LeagueScheduleDto = {
        races: [
          {
            id: 'race-1',
            name: 'Race 1',
            date: '2024-01-01',
          },
          {
            id: 'race-2',
            name: 'Race 2',
            date: '2024-01-08',
          },
        ],
      };

      vi.mocked(mockApiClient.getSchedule).mockResolvedValue(mockSchedule);

      // Act
      const result = await service.getLeagueSchedule(leagueId);

      // Assert
      expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
      expect(result).toEqual(mockSchedule);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const leagueId = 'league-123';
      const error = new Error('Failed to fetch schedule');
      vi.mocked(mockApiClient.getSchedule).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('Failed to fetch schedule');
      expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
    });
  });

  describe('getLeagueMemberships', () => {
    it('should fetch league memberships', async () => {
      // Arrange
      const leagueId = 'league-123';
      const mockMemberships: LeagueMembershipsDto = {
        memberships: [
          {
            driverId: 'driver-1',
            role: 'admin',
            joinedAt: '2024-01-01',
          },
          {
            driverId: 'driver-2',
            role: 'member',
            joinedAt: '2024-01-02',
          },
        ],
      };

      vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockMemberships);

      // Act
      const result = await service.getLeagueMemberships(leagueId);

      // Assert
      expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
      expect(result).toEqual(mockMemberships);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const leagueId = 'league-123';
      const error = new Error('Failed to fetch memberships');
      vi.mocked(mockApiClient.getMemberships).mockRejectedValue(error);

      // Act & Assert
      await expect(service.getLeagueMemberships(leagueId)).rejects.toThrow('Failed to fetch memberships');
      expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
    });
  });

  describe('createLeague', () => {
    it('should create a new league', async () => {
      // Arrange
      const input: CreateLeagueInputDto = {
        name: 'New League',
        description: 'A brand new league',
        maxMembers: 25,
        isPublic: true,
      };

      const mockOutput: CreateLeagueOutputDto = {
        id: 'league-new',
        name: 'New League',
        success: true,
      };

      vi.mocked(mockApiClient.create).mockResolvedValue(mockOutput);

      // Act
      const result = await service.createLeague(input);

      // Assert
      expect(mockApiClient.create).toHaveBeenCalledWith(input);
      expect(result).toEqual(mockOutput);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const input: CreateLeagueInputDto = {
        name: 'New League',
        description: 'A brand new league',
        maxMembers: 25,
        isPublic: true,
      };
      const error = new Error('Failed to create league');
      vi.mocked(mockApiClient.create).mockRejectedValue(error);

      // Act & Assert
      await expect(service.createLeague(input)).rejects.toThrow('Failed to create league');
      expect(mockApiClient.create).toHaveBeenCalledWith(input);
    });
  });

  describe('removeMember', () => {
    it('should remove a member from league', async () => {
      // Arrange
      const leagueId = 'league-123';
      const performerDriverId = 'driver-admin';
      const targetDriverId = 'driver-remove';

      const mockOutput = { success: true };

      vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput);

      // Act
      const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);

      // Assert
      expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
      expect(result).toEqual(mockOutput);
    });

    it('should handle removal failure', async () => {
      // Arrange
      const leagueId = 'league-123';
      const performerDriverId = 'driver-admin';
      const targetDriverId = 'driver-remove';

      const mockOutput = { success: false };

      vi.mocked(mockApiClient.removeMember).mockResolvedValue(mockOutput);

      // Act
      const result = await service.removeMember(leagueId, performerDriverId, targetDriverId);

      // Assert
      expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
      expect(result).toEqual(mockOutput);
      expect(result.success).toBe(false);
    });

    it('should propagate errors from API client', async () => {
      // Arrange
      const leagueId = 'league-123';
      const performerDriverId = 'driver-admin';
      const targetDriverId = 'driver-remove';
      const error = new Error('Failed to remove member');
      vi.mocked(mockApiClient.removeMember).mockRejectedValue(error);

      // Act & Assert
      await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Failed to remove member');
      expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
    });
  });
});