import { describe, it, expect, vi, Mocked, beforeEach, afterEach } from 'vitest';
import { LeagueService } from './LeagueService';
import { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import { LeagueStandingsViewModel } from '../../view-models/LeagueStandingsViewModel';
import { LeagueStatsViewModel } from '../../view-models/LeagueStatsViewModel';
import { LeagueScheduleViewModel } from '../../view-models/LeagueScheduleViewModel';
import { LeagueMembershipsViewModel } from '../../view-models/LeagueMembershipsViewModel';
import { RemoveMemberViewModel } from '../../view-models/RemoveMemberViewModel';
import { LeagueMemberViewModel } from '../../view-models/LeagueMemberViewModel';
import type { CreateLeagueInputDTO } from '../../types/generated/CreateLeagueInputDTO';
import type { CreateLeagueOutputDTO } from '../../types/generated/CreateLeagueOutputDTO';
import type { RemoveLeagueMemberOutputDTO } from '../../types/generated/RemoveLeagueMemberOutputDTO';

describe('LeagueService', () => {
  let mockApiClient: Mocked<LeaguesApiClient>;
  let service: LeagueService;

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

    service = new LeagueService(mockApiClient);
  });

  describe('getAllLeagues', () => {
    it('should call apiClient.getAllWithCapacityAndScoring and return array of LeagueSummaryViewModel', async () => {
      const mockDto = {
        totalCount: 2,
        leagues: [
          { id: 'league-1', name: 'League One' },
          { id: 'league-2', name: 'League Two' },
        ],
      } as any;

      mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);

      const result = await service.getAllLeagues();

      expect(mockApiClient.getAllWithCapacityAndScoring).toHaveBeenCalled();
      expect(result).toHaveLength(2);
      expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
    });

    it('should handle empty leagues array', async () => {
      const mockDto = { totalCount: 0, leagues: [] } as any;

      mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);

      const result = await service.getAllLeagues();

      expect(result).toHaveLength(0);
    });

    it('should throw error when apiClient.getAllWithCapacityAndScoring fails', async () => {
      const error = new Error('API call failed');
      mockApiClient.getAllWithCapacityAndScoring.mockRejectedValue(error);

      await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
    });
  });

  describe('getLeagueStandings', () => {
    it('should call apiClient.getStandings and return LeagueStandingsViewModel', async () => {
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      mockApiClient.getStandings.mockResolvedValue({ standings: [] } as any);
      mockApiClient.getMemberships.mockResolvedValue({ members: [] } as any);

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

      expect(mockApiClient.getStandings).toHaveBeenCalledWith(leagueId);
      expect(result).toBeInstanceOf(LeagueStandingsViewModel);
    });

    it('should throw error when apiClient.getStandings fails', async () => {
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      const error = new Error('API call failed');
      mockApiClient.getStandings.mockRejectedValue(error);

      await expect(service.getLeagueStandings(leagueId, currentUserId)).rejects.toThrow('API call failed');
    });
  });

  describe('getLeagueStats', () => {
    it('should call apiClient.getTotal and return LeagueStatsViewModel', async () => {
      const mockDto = { totalLeagues: 42 };

      mockApiClient.getTotal.mockResolvedValue(mockDto);

      const result = await service.getLeagueStats();

      expect(mockApiClient.getTotal).toHaveBeenCalled();
      expect(result).toBeInstanceOf(LeagueStatsViewModel);
      expect(result.totalLeagues).toBe(42);
    });

    it('should throw error when apiClient.getTotal fails', async () => {
      const error = new Error('API call failed');
      mockApiClient.getTotal.mockRejectedValue(error);

      await expect(service.getLeagueStats()).rejects.toThrow('API call failed');
    });
  });

  describe('getLeagueSchedule', () => {
    afterEach(() => {
      vi.useRealTimers();
    });

    it('should call apiClient.getSchedule and return LeagueScheduleViewModel with Date parsing', async () => {
      vi.useFakeTimers();
      vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));

      const leagueId = 'league-123';
      const mockDto = {
        races: [
          { id: 'race-1', name: 'Race One', date: '2024-12-31T20:00:00Z' },
          { id: 'race-2', name: 'Race Two', date: '2025-01-02T20:00:00Z' },
        ],
      } as any;

      mockApiClient.getSchedule.mockResolvedValue(mockDto);

      const result = await service.getLeagueSchedule(leagueId);

      expect(mockApiClient.getSchedule).toHaveBeenCalledWith(leagueId);
      expect(result).toBeInstanceOf(LeagueScheduleViewModel);

      expect(result.raceCount).toBe(2);
      expect(result.races[0]!.scheduledAt).toBeInstanceOf(Date);
      expect(result.races[0]!.isPast).toBe(true);
      expect(result.races[1]!.isUpcoming).toBe(true);
    });

    it('should prefer scheduledAt over date and map optional fields/status', async () => {
      vi.useFakeTimers();
      vi.setSystemTime(new Date('2025-01-01T00:00:00Z'));

      const leagueId = 'league-123';
      const mockDto = {
        races: [
          {
            id: 'race-1',
            name: 'Round 1',
            date: '2025-01-02T20:00:00Z',
            scheduledAt: '2025-01-03T20:00:00Z',
            track: 'Monza',
            car: 'GT3',
            sessionType: 'race',
            isRegistered: true,
            status: 'scheduled',
          },
        ],
      } as any;

      mockApiClient.getSchedule.mockResolvedValue(mockDto);

      const result = await service.getLeagueSchedule(leagueId);

      expect(result.races[0]!.scheduledAt.toISOString()).toBe('2025-01-03T20:00:00.000Z');
      expect(result.races[0]!.track).toBe('Monza');
      expect(result.races[0]!.car).toBe('GT3');
      expect(result.races[0]!.sessionType).toBe('race');
      expect(result.races[0]!.isRegistered).toBe(true);
      expect(result.races[0]!.status).toBe('scheduled');
    });

    it('should handle empty races array', async () => {
      const leagueId = 'league-123';
      const mockDto = { races: [] };

      mockApiClient.getSchedule.mockResolvedValue(mockDto as any);

      const result = await service.getLeagueSchedule(leagueId);

      expect(result.races).toEqual([]);
      expect(result.hasRaces).toBe(false);
    });

    it('should throw error when apiClient.getSchedule fails', async () => {
      const leagueId = 'league-123';

      const error = new Error('API call failed');
      mockApiClient.getSchedule.mockRejectedValue(error);

      await expect(service.getLeagueSchedule(leagueId)).rejects.toThrow('API call failed');
    });
  });

  describe('getLeagueMemberships', () => {
    it('should call apiClient.getMemberships and return LeagueMembershipsViewModel', async () => {
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      const mockDto = {
        members: [{ driverId: 'driver-1' }, { driverId: 'driver-2' }],
      } as any;

      mockApiClient.getMemberships.mockResolvedValue(mockDto);

      const result = await service.getLeagueMemberships(leagueId, currentUserId);

      expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
      expect(result).toBeInstanceOf(LeagueMembershipsViewModel);
      expect(result.memberships).toHaveLength(2);

      const first = result.memberships[0]!;
      expect(first).toBeInstanceOf(LeagueMemberViewModel);
      expect(first.driverId).toBe('driver-1');
    });

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

      const mockDto = { members: [] } as any;

      mockApiClient.getMemberships.mockResolvedValue(mockDto);

      const result = await service.getLeagueMemberships(leagueId, currentUserId);

      expect(result.memberships).toHaveLength(0);
      expect(result.hasMembers).toBe(false);
    });

    it('should throw error when apiClient.getMemberships fails', async () => {
      const leagueId = 'league-123';
      const currentUserId = 'user-456';

      const error = new Error('API call failed');
      mockApiClient.getMemberships.mockRejectedValue(error);

      await expect(service.getLeagueMemberships(leagueId, currentUserId)).rejects.toThrow('API call failed');
    });
  });

  describe('createLeague', () => {
    it('should call apiClient.create', async () => {
      const input: CreateLeagueInputDTO = {
        name: 'New League',
        description: 'A new league',
        visibility: 'public',
        ownerId: 'owner-1',
      };

      const mockDto: CreateLeagueOutputDTO = {
        leagueId: 'new-league-id',
        success: true,
      };

      mockApiClient.create.mockResolvedValue(mockDto);

      await service.createLeague(input);

      expect(mockApiClient.create).toHaveBeenCalledWith(input);
    });

    it('should throw error when apiClient.create fails', async () => {
      const input: CreateLeagueInputDTO = {
        name: 'New League',
        description: 'A new league',
        visibility: 'public',
        ownerId: 'owner-1',
      };

      const error = new Error('API call failed');
      mockApiClient.create.mockRejectedValue(error);

      await expect(service.createLeague(input)).rejects.toThrow('API call failed');
    });

    it('should not call apiClient.create when submitBlocker is blocked', async () => {
      const input: CreateLeagueInputDTO = {
        name: 'New League',
        description: 'A new league',
        visibility: 'public',
        ownerId: 'owner-1',
      };

      // First call should succeed
      const mockDto: CreateLeagueOutputDTO = {
        leagueId: 'new-league-id',
        success: true,
      };
      mockApiClient.create.mockResolvedValue(mockDto);

      await service.createLeague(input); // This should block the submitBlocker

      // Reset mock to check calls
      mockApiClient.create.mockClear();

      // Second call should not call API
      await service.createLeague(input);
      expect(mockApiClient.create).not.toHaveBeenCalled();
    });

    it('should not call apiClient.create when throttle is active', async () => {
      const input: CreateLeagueInputDTO = {
        name: 'New League',
        description: 'A new league',
        visibility: 'public',
        ownerId: 'owner-1',
      };

      // First call
      const mockDto: CreateLeagueOutputDTO = {
        leagueId: 'new-league-id',
        success: true,
      };
      mockApiClient.create.mockResolvedValue(mockDto);

      await service.createLeague(input); // This blocks throttle for 500ms

      // Reset mock
      mockApiClient.create.mockClear();

      // Immediate second call should not call API due to throttle
      await service.createLeague(input);
      expect(mockApiClient.create).not.toHaveBeenCalled();
    });
  });

  describe('removeMember', () => {
    it('should call apiClient.removeMember and return RemoveMemberViewModel', async () => {
      const leagueId = 'league-123';
      const performerDriverId = 'performer-456';
      const targetDriverId = 'target-789';

      const mockDto: RemoveLeagueMemberOutputDTO = { success: true };

      mockApiClient.removeMember.mockResolvedValue(mockDto);

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

      expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
      expect(result).toBeInstanceOf(RemoveMemberViewModel);
      expect(result.success).toBe(true);
    });

    it('should handle unsuccessful removal', async () => {
      const leagueId = 'league-123';
      const performerDriverId = 'performer-456';
      const targetDriverId = 'target-789';

      const mockDto: RemoveLeagueMemberOutputDTO = { success: false };

      mockApiClient.removeMember.mockResolvedValue(mockDto);

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

      expect(result.success).toBe(false);
      expect(result.successMessage).toBe('Failed to remove member.');
    });

    it('should throw error when apiClient.removeMember fails', async () => {
      const leagueId = 'league-123';
      const performerDriverId = 'performer-456';
      const targetDriverId = 'target-789';

      const error = new Error('API call failed');
      mockApiClient.removeMember.mockRejectedValue(error);

      await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('API call failed');
    });
  });
});