import { beforeEach, describe, expect, it } from 'vitest';
import { ExternalGameRatingProfile } from '../entities/ExternalGameRatingProfile';
import { ExternalRating } from '../value-objects/ExternalRating';
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
import { GameKey } from '../value-objects/GameKey';
import { UserId } from '../value-objects/UserId';
import { ExternalGameRatingRepository } from './ExternalGameRatingRepository';

/**
 * Test suite for ExternalGameRatingRepository interface
 * This tests the contract that all implementations must satisfy
 */
describe('ExternalGameRatingRepository', () => {
  // Mock implementation for testing
  class MockExternalGameRatingRepository implements ExternalGameRatingRepository {
    private profiles: Map<string, ExternalGameRatingProfile> = new Map();

    private getKey(userId: string, gameKey: string): string {
      return `${userId}|${gameKey}`;
    }

    async findByUserIdAndGameKey(
      userId: string,
      gameKey: string
    ): Promise<ExternalGameRatingProfile | null> {
      const key = this.getKey(userId, gameKey);
      return this.profiles.get(key) || null;
    }

    async findByUserId(userId: string): Promise<ExternalGameRatingProfile[]> {
      return Array.from(this.profiles.values()).filter(
        p => p.userId.toString() === userId
      );
    }

    async findByGameKey(gameKey: string): Promise<ExternalGameRatingProfile[]> {
      return Array.from(this.profiles.values()).filter(
        p => p.gameKey.toString() === gameKey
      );
    }

    async save(profile: ExternalGameRatingProfile): Promise<ExternalGameRatingProfile> {
      const key = this.getKey(profile.userId.toString(), profile.gameKey.toString());
      this.profiles.set(key, profile);
      return profile;
    }

    async saveMany(profiles: ExternalGameRatingProfile[]): Promise<ExternalGameRatingProfile[]> {
      for (const profile of profiles) {
        await this.save(profile);
      }
      return profiles;
    }

    async delete(userId: string, gameKey: string): Promise<boolean> {
      const key = this.getKey(userId, gameKey);
      return this.profiles.delete(key);
    }

    async exists(userId: string, gameKey: string): Promise<boolean> {
      const key = this.getKey(userId, gameKey);
      return this.profiles.has(key);
    }

    async findProfilesPaginated(userId: string, options?: import('./ExternalGameRatingRepository').PaginatedQueryOptions): Promise<import('./ExternalGameRatingRepository').PaginatedResult<ExternalGameRatingProfile>> {
      const allProfiles = await this.findByUserId(userId);
      
      // Apply filters
      let filtered = allProfiles;
      if (options?.filter) {
        const filter = options.filter;
        if (filter.gameKeys) {
          filtered = filtered.filter(p => filter.gameKeys!.includes(p.gameKey.toString()));
        }
        if (filter.sources) {
          filtered = filtered.filter(p => filter.sources!.includes(p.provenance.source));
        }
        if (filter.verified !== undefined) {
          filtered = filtered.filter(p => p.provenance.verified === filter.verified);
        }
        if (filter.lastSyncedAfter) {
          filtered = filtered.filter(p => p.provenance.lastSyncedAt >= filter.lastSyncedAfter!);
        }
      }

      const total = filtered.length;
      const limit = options?.limit ?? 10;
      const offset = options?.offset ?? 0;
      const items = filtered.slice(offset, offset + limit);
      const hasMore = offset + limit < total;
      const nextOffset = hasMore ? offset + limit : undefined;

      const result: import('./ExternalGameRatingRepository').PaginatedResult<ExternalGameRatingProfile> = {
        items,
        total,
        limit,
        offset,
        hasMore
      };
      
      if (nextOffset !== undefined) {
        result.nextOffset = nextOffset;
      }

      return result;
    }
  }

  let repository: ExternalGameRatingRepository;

  beforeEach(() => {
    repository = new MockExternalGameRatingRepository();
  });

  describe('findByUserIdAndGameKey', () => {
    it('should return null when profile does not exist', async () => {
      const result = await repository.findByUserIdAndGameKey('user-123', 'iracing');
      expect(result).toBeNull();
    });

    it('should return profile when it exists', async () => {
      const profile = createTestProfile('user-123', 'iracing');
      await repository.save(profile);

      const result = await repository.findByUserIdAndGameKey('user-123', 'iracing');
      expect(result).not.toBeNull();
      expect(result?.userId.toString()).toBe('user-123');
      expect(result?.gameKey.toString()).toBe('iracing');
    });
  });

  describe('findByUserId', () => {
    it('should return empty array when no profiles exist for user', async () => {
      const results = await repository.findByUserId('user-123');
      expect(results).toEqual([]);
    });

    it('should return all profiles for a user', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-123', 'assetto');
      const profile3 = createTestProfile('user-456', 'iracing');

      await repository.saveMany([profile1, profile2, profile3]);

      const results = await repository.findByUserId('user-123');
      expect(results).toHaveLength(2);
      expect(results.map(p => p.gameKey.toString()).sort()).toEqual(['assetto', 'iracing']);
    });
  });

  describe('findByGameKey', () => {
    it('should return empty array when no profiles exist for game', async () => {
      const results = await repository.findByGameKey('iracing');
      expect(results).toEqual([]);
    });

    it('should return all profiles for a game', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-456', 'iracing');
      const profile3 = createTestProfile('user-123', 'assetto');

      await repository.saveMany([profile1, profile2, profile3]);

      const results = await repository.findByGameKey('iracing');
      expect(results).toHaveLength(2);
      expect(results.map(p => p.userId.toString()).sort()).toEqual(['user-123', 'user-456']);
    });
  });

  describe('save', () => {
    it('should save a new profile', async () => {
      const profile = createTestProfile('user-123', 'iracing');
      const saved = await repository.save(profile);

      expect(saved).toBe(profile);

      const retrieved = await repository.findByUserIdAndGameKey('user-123', 'iracing');
      expect(retrieved).toBe(profile);
    });

    it('should update an existing profile', async () => {
      const profile = createTestProfile('user-123', 'iracing');
      await repository.save(profile);

      // Update the profile
      profile.updateLastSyncedAt(new Date('2024-01-02'));
      profile.markVerified();

      await repository.save(profile);

      const retrieved = await repository.findByUserIdAndGameKey('user-123', 'iracing');
      expect(retrieved?.provenance.verified).toBe(true);
      expect(retrieved?.provenance.lastSyncedAt).toEqual(new Date('2024-01-02'));
    });
  });

  describe('saveMany', () => {
    it('should save multiple profiles', async () => {
      const profiles = [
        createTestProfile('user-123', 'iracing'),
        createTestProfile('user-456', 'assetto'),
        createTestProfile('user-789', 'iracing'),
      ];

      const saved = await repository.saveMany(profiles);
      expect(saved).toHaveLength(3);

      const iracingProfiles = await repository.findByGameKey('iracing');
      expect(iracingProfiles).toHaveLength(2);
    });
  });

  describe('delete', () => {
    it('should delete existing profile', async () => {
      const profile = createTestProfile('user-123', 'iracing');
      await repository.save(profile);

      const deleted = await repository.delete('user-123', 'iracing');
      expect(deleted).toBe(true);

      const retrieved = await repository.findByUserIdAndGameKey('user-123', 'iracing');
      expect(retrieved).toBeNull();
    });

    it('should return false when deleting non-existent profile', async () => {
      const deleted = await repository.delete('user-123', 'iracing');
      expect(deleted).toBe(false);
    });
  });

  describe('exists', () => {
    it('should return true when profile exists', async () => {
      const profile = createTestProfile('user-123', 'iracing');
      await repository.save(profile);

      const exists = await repository.exists('user-123', 'iracing');
      expect(exists).toBe(true);
    });

    it('should return false when profile does not exist', async () => {
      const exists = await repository.exists('user-123', 'iracing');
      expect(exists).toBe(false);
    });
  });

  describe('findProfilesPaginated', () => {
    it('should return paginated results', async () => {
      // Create 15 profiles
      for (let i = 0; i < 15; i++) {
        const profile = createTestProfile('user-123', `game-${i}`);
        await repository.save(profile);
      }

      const result = await repository.findProfilesPaginated('user-123', { limit: 5, offset: 0 });
      
      expect(result.items).toHaveLength(5);
      expect(result.total).toBe(15);
      expect(result.limit).toBe(5);
      expect(result.offset).toBe(0);
      expect(result.hasMore).toBe(true);
      expect(result.nextOffset).toBe(5);
    });

    it('should filter by game keys', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-123', 'assetto');
      const profile3 = createTestProfile('user-123', 'rfactor');

      await repository.saveMany([profile1, profile2, profile3]);

      const result = await repository.findProfilesPaginated('user-123', {
        filter: { gameKeys: ['iracing', 'rfactor'] }
      });

      expect(result.items).toHaveLength(2);
      expect(result.items.map(p => p.gameKey.toString()).sort()).toEqual(['iracing', 'rfactor']);
    });

    it('should filter by sources', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-123', 'assetto');
      
      // Manually update provenance for testing
      const profile2Provenance = ExternalRatingProvenance.create({
        source: 'manual',
        lastSyncedAt: new Date('2024-01-01'),
        verified: false,
      });
      profile2.updateRatings(new Map(profile2.ratings), profile2Provenance);

      await repository.saveMany([profile1, profile2]);

      const result = await repository.findProfilesPaginated('user-123', {
        filter: { sources: ['iracing'] }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0].gameKey.toString()).toBe('iracing');
    });

    it('should filter by verified status', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-123', 'assetto');
      
      profile1.markVerified();
      await repository.saveMany([profile1, profile2]);

      const result = await repository.findProfilesPaginated('user-123', {
        filter: { verified: true }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0].gameKey.toString()).toBe('iracing');
    });

    it('should filter by last synced date', async () => {
      const profile1 = createTestProfile('user-123', 'iracing');
      const profile2 = createTestProfile('user-123', 'assetto');
      
      profile1.updateLastSyncedAt(new Date('2024-01-02'));
      profile2.updateLastSyncedAt(new Date('2024-01-01'));
      await repository.saveMany([profile1, profile2]);

      const result = await repository.findProfilesPaginated('user-123', {
        filter: { lastSyncedAfter: new Date('2024-01-01T12:00:00Z') }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0].gameKey.toString()).toBe('iracing');
    });

    it('should return empty result when no profiles match', async () => {
      const result = await repository.findProfilesPaginated('non-existent', {
        filter: { gameKeys: ['iracing'] }
      });

      expect(result.items).toHaveLength(0);
      expect(result.total).toBe(0);
      expect(result.hasMore).toBe(false);
    });
  });

  // Helper function to create test profiles
  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)],
      ['skill', ExternalRating.create(game, 'skill', 92.0)],
    ]);
    const provenance = ExternalRatingProvenance.create({
      source: gameKey,
      lastSyncedAt: new Date('2024-01-01'),
      verified: false,
    });

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