import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LeagueMembershipService } from './LeagueMembershipService';
import type { LeaguesApiClient } from '../../api/leagues/LeaguesApiClient';
import type { LeagueMembersPresenter } from '../../presenters/LeagueMembersPresenter';
import type { LeagueMembershipsDto } from '../../dtos';
import type { LeagueMemberViewModel } from '../../view-models';

describe('LeagueMembershipService', () => {
  let service: LeagueMembershipService;
  let mockApiClient: LeaguesApiClient;
  let mockLeagueMembersPresenter: LeagueMembersPresenter;

  beforeEach(() => {
    mockApiClient = {
      getMemberships: vi.fn(),
      removeMember: vi.fn(),
    } as unknown as LeaguesApiClient;

    mockLeagueMembersPresenter = {
      present: vi.fn(),
    } as unknown as LeagueMembersPresenter;

    service = new LeagueMembershipService(
      mockApiClient,
      mockLeagueMembersPresenter
    );
  });

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

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

      const mockDto: LeagueMembershipsDto = {
        members: [
          {
            driverId: 'driver-1',
            role: 'owner',
            joinedAt: '2024-01-01',
          },
          {
            driverId: 'driver-2',
            role: 'member',
            joinedAt: '2024-01-02',
          },
        ],
      };

      const mockViewModels: LeagueMemberViewModel[] = [
        {
          driverId: 'driver-1',
          role: 'owner',
          joinedAt: '2024-01-01',
        } as LeagueMemberViewModel,
        {
          driverId: 'driver-2',
          role: 'member',
          joinedAt: '2024-01-02',
        } as LeagueMemberViewModel,
      ];

      vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels);

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

      // Assert
      expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
      expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId);
      expect(result).toEqual(mockViewModels);
    });

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

      const mockDto: LeagueMembershipsDto = {
        members: [],
      };

      const mockViewModels: LeagueMemberViewModel[] = [];

      vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels);

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

      // Assert
      expect(mockApiClient.getMemberships).toHaveBeenCalledWith(leagueId);
      expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId);
      expect(result).toEqual([]);
    });

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

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

    it('should pass correct currentUserId to presenter', async () => {
      // Arrange
      const leagueId = 'league-123';
      const currentUserId = 'current-user-789';

      const mockDto: LeagueMembershipsDto = {
        members: [
          {
            driverId: 'driver-1',
            role: 'member',
            joinedAt: '2024-01-01',
          },
        ],
      };

      const mockViewModels: LeagueMemberViewModel[] = [
        {
          driverId: 'driver-1',
          role: 'member',
          joinedAt: '2024-01-01',
        } as LeagueMemberViewModel,
      ];

      vi.mocked(mockApiClient.getMemberships).mockResolvedValue(mockDto);
      vi.mocked(mockLeagueMembersPresenter.present).mockReturnValue(mockViewModels);

      // Act
      await service.getLeagueMemberships(leagueId, currentUserId);

      // Assert
      expect(mockLeagueMembersPresenter.present).toHaveBeenCalledWith(mockDto, currentUserId);
    });
  });

  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);
    });

    it('should handle unauthorized removal attempt', async () => {
      // Arrange
      const leagueId = 'league-123';
      const performerDriverId = 'non-admin-driver';
      const targetDriverId = 'driver-remove';
      const error = new Error('Unauthorized');
      vi.mocked(mockApiClient.removeMember).mockRejectedValue(error);

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

    it('should handle removing non-existent member', async () => {
      // Arrange
      const leagueId = 'league-123';
      const performerDriverId = 'driver-admin';
      const targetDriverId = 'non-existent-driver';
      const error = new Error('Member not found');
      vi.mocked(mockApiClient.removeMember).mockRejectedValue(error);

      // Act & Assert
      await expect(service.removeMember(leagueId, performerDriverId, targetDriverId)).rejects.toThrow('Member not found');
      expect(mockApiClient.removeMember).toHaveBeenCalledWith(leagueId, performerDriverId, targetDriverId);
    });
  });
});