import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
import { ExternalGameRatingProfile } from '../../domain/entities/ExternalGameRatingProfile';
import { ExternalGameRatingRepository } from '../../domain/repositories/ExternalGameRatingRepository';
import { ExternalRating } from '../../domain/value-objects/ExternalRating';
import { ExternalRatingProvenance } from '../../domain/value-objects/ExternalRatingProvenance';
import { GameKey } from '../../domain/value-objects/GameKey';
import { UserId } from '../../domain/value-objects/UserId';
import { UpsertExternalGameRatingInput } from '../dtos/UpsertExternalGameRatingDto';
import { UpsertExternalGameRatingUseCase } from './UpsertExternalGameRatingUseCase';

describe('UpsertExternalGameRatingUseCase', () => {
  let useCase: UpsertExternalGameRatingUseCase;
  let mockRepository: {
    findByUserIdAndGameKey: Mock;
    findByUserId: Mock;
    findByGameKey: Mock;
    save: Mock;
    saveMany: Mock;
    delete: Mock;
    exists: Mock;
  };

  beforeEach(() => {
    mockRepository = {
      findByUserIdAndGameKey: vi.fn(),
      findByUserId: vi.fn(),
      findByGameKey: vi.fn(),
      save: vi.fn(),
      saveMany: vi.fn(),
      delete: vi.fn(),
      exists: vi.fn(),
    } as unknown as {
      findByUserIdAndGameKey: Mock;
      findByUserId: Mock;
      findByGameKey: Mock;
      save: Mock;
      saveMany: Mock;
      delete: Mock;
      exists: Mock;
    };

    useCase = new UpsertExternalGameRatingUseCase(mockRepository as unknown as ExternalGameRatingRepository);
  });

  describe('execute', () => {
    it('should create a new profile when it does not exist', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: 'safety', value: 85.5 },
          { type: 'skill', value: 92.0 },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
          verified: true,
        },
      };

      mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
      mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);

      const result = await useCase.execute(input);

      expect(result.success).toBe(true);
      expect(result.action).toBe('created');
      expect(result.profile.userId).toBe('user-123');
      expect(result.profile.gameKey).toBe('iracing');
      expect(result.profile.ratingCount).toBe(2);
      expect(result.profile.ratingTypes).toEqual(['safety', 'skill']);
      expect(result.profile.verified).toBe(true);

      expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing');
      expect(mockRepository.save).toHaveBeenCalledTimes(1);
    });

    it('should update existing profile', async () => {
      const existingProfile = createTestProfile('user-123', 'iracing');
      mockRepository.findByUserIdAndGameKey.mockResolvedValue(existingProfile);
      mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);

      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: 'safety', value: 90.0 },
          { type: 'newType', value: 88.0 },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-02T00:00:00Z',
          verified: false,
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(true);
      expect(result.action).toBe('updated');
      expect(result.profile.ratingCount).toBe(2);
      expect(result.profile.ratingTypes).toEqual(['safety', 'newType']);
      expect(result.profile.verified).toBe(false);

      expect(mockRepository.findByUserIdAndGameKey).toHaveBeenCalledWith('user-123', 'iracing');
      expect(mockRepository.save).toHaveBeenCalledTimes(1);
    });

    it('should handle validation errors - missing userId', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: '',
        gameKey: 'iracing',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('User ID is required');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle validation errors - missing gameKey', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: '',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Game key is required');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle validation errors - empty ratings', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Ratings are required');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle validation errors - invalid rating value', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: 'safety', value: NaN },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Rating value for type safety must be a valid number');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle validation errors - missing provenance source', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: '',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Provenance source is required');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle validation errors - invalid date', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: 'iracing',
          lastSyncedAt: 'invalid-date',
        },
      };

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Provenance lastSyncedAt must be a valid date');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should handle repository errors gracefully', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      mockRepository.findByUserIdAndGameKey.mockRejectedValue(new Error('Database connection failed'));

      const result = await useCase.execute(input);

      expect(result.success).toBe(false);
      expect(result.errors).toContain('Database connection failed');
      expect(mockRepository.save).not.toHaveBeenCalled();
    });

    it('should default verified to false when not provided', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [{ type: 'safety', value: 85.5 }],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
          // verified not provided
        },
      };

      mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
      mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);

      const result = await useCase.execute(input);

      expect(result.success).toBe(true);
      expect(result.profile.verified).toBe(false);
    });

    it('should trim rating types', async () => {
      const input: UpsertExternalGameRatingInput = {
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: '  safety  ', value: 85.5 },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: '2024-01-01T00:00:00Z',
        },
      };

      mockRepository.findByUserIdAndGameKey.mockResolvedValue(null);
      mockRepository.save.mockImplementation(async (profile: ExternalGameRatingProfile) => profile);

      const result = await useCase.execute(input);

      expect(result.success).toBe(true);
      expect(result.profile.ratingTypes).toEqual(['safety']);
    });
  });

  function createTestProfile(userId: string, gameKey: string): ExternalGameRatingProfile {
    const user = UserId.fromString(userId);
    const game = GameKey.create(gameKey);
    const ratings = new Map([
      ['safety', ExternalRating.create(game, 'safety', 85.5)],
    ]);
    const provenance = ExternalRatingProvenance.create({
      source: gameKey,
      lastSyncedAt: new Date('2024-01-01'),
      verified: false,
    });

    return ExternalGameRatingProfile.create({
      userId: user,
      gameKey: game,
      ratings,
      provenance,
    });
  }
});