import { ExternalGameRatingProfile } from './ExternalGameRatingProfile';
import { UserId } from '../value-objects/UserId';
import { GameKey } from '../value-objects/GameKey';
import { ExternalRating } from '../value-objects/ExternalRating';
import { ExternalRatingProvenance } from '../value-objects/ExternalRatingProvenance';
import { IdentityDomainValidationError } from '../errors/IdentityDomainError';

describe('ExternalGameRatingProfile', () => {
  let userId: UserId;
  let gameKey: GameKey;
  let ratings: Map<string, ExternalRating>;
  let provenance: ExternalRatingProvenance;

  beforeEach(() => {
    userId = UserId.fromString('user-123');
    gameKey = GameKey.create('iracing');
    ratings = new Map([
      ['safety', ExternalRating.create(gameKey, 'safety', 85.5)],
      ['skill', ExternalRating.create(gameKey, 'skill', 92.0)],
    ]);
    provenance = ExternalRatingProvenance.create({
      source: 'iracing',
      lastSyncedAt: new Date('2024-01-01'),
      verified: true,
    });
  });

  describe('create', () => {
    it('should create a valid profile', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      expect(profile.userId).toBe(userId);
      expect(profile.gameKey).toBe(gameKey);
      expect(profile.ratings.size).toBe(2);
      expect(profile.provenance).toBe(provenance);
    });

    it('should allow empty ratings map', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings: new Map(),
        provenance,
      });

      expect(profile.hasRatings()).toBe(false);
      expect(profile.ratings.size).toBe(0);
    });

    it('should throw error if rating gameKey does not match profile gameKey', () => {
      const wrongGameKey = GameKey.create('assetto');
      const wrongRatings = new Map([
        ['safety', ExternalRating.create(wrongGameKey, 'safety', 85.5)],
      ]);

      expect(() =>
        ExternalGameRatingProfile.create({
          userId,
          gameKey,
          ratings: wrongRatings,
          provenance,
        })
      ).toThrow(IdentityDomainValidationError);
    });
  });

  describe('restore', () => {
    it('should restore profile from stored data', () => {
      const profile = ExternalGameRatingProfile.restore({
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: 'safety', gameKey: 'iracing', value: 85.5 },
          { type: 'skill', gameKey: 'iracing', value: 92.0 },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: new Date('2024-01-01'),
          verified: true,
        },
      });

      expect(profile.userId.toString()).toBe('user-123');
      expect(profile.gameKey.toString()).toBe('iracing');
      expect(profile.ratings.size).toBe(2);
      expect(profile.provenance.source).toBe('iracing');
      expect(profile.provenance.verified).toBe(true);
    });

    it('should handle missing verified flag in provenance', () => {
      const profile = ExternalGameRatingProfile.restore({
        userId: 'user-123',
        gameKey: 'iracing',
        ratings: [
          { type: 'safety', gameKey: 'iracing', value: 85.5 },
        ],
        provenance: {
          source: 'iracing',
          lastSyncedAt: new Date('2024-01-01'),
        },
      });

      expect(profile.provenance.verified).toBe(false);
    });
  });

  describe('getRatingByType', () => {
    it('should return rating for existing type', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const rating = profile.getRatingByType('safety');
      expect(rating).toBeDefined();
      expect(rating?.type).toBe('safety');
      expect(rating?.value).toBe(85.5);
    });

    it('should return undefined for non-existing type', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const rating = profile.getRatingByType('nonexistent');
      expect(rating).toBeUndefined();
    });
  });

  describe('getRatingTypes', () => {
    it('should return all rating types', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const types = profile.getRatingTypes();
      expect(types).toHaveLength(2);
      expect(types).toContain('safety');
      expect(types).toContain('skill');
    });
  });

  describe('hasRatings', () => {
    it('should return true when has ratings', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      expect(profile.hasRatings()).toBe(true);
    });

    it('should return false when no ratings', () => {
      const emptyRatings = new Map<string, ExternalRating>();
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings: emptyRatings,
        provenance,
      });

      expect(profile.hasRatings()).toBe(false);
    });
  });

  describe('updateRatings', () => {
    it('should update ratings and provenance', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const newRatings = new Map([
        ['safety', ExternalRating.create(gameKey, 'safety', 90.0)],
        ['newType', ExternalRating.create(gameKey, 'newType', 88.0)],
      ]);
      const newProvenance = ExternalRatingProvenance.create({
        source: 'iracing',
        lastSyncedAt: new Date('2024-01-02'),
        verified: false,
      });

      profile.updateRatings(newRatings, newProvenance);

      expect(profile.ratings.size).toBe(2);
      expect(profile.getRatingByType('safety')?.value).toBe(90.0);
      expect(profile.getRatingByType('newType')?.value).toBe(88.0);
      expect(profile.provenance.lastSyncedAt).toEqual(new Date('2024-01-02'));
    });

    it('should throw error when updating with mismatched gameKey', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const wrongGameKey = GameKey.create('assetto');
      const wrongRatings = new Map([
        ['safety', ExternalRating.create(wrongGameKey, 'safety', 90.0)],
      ]);

      expect(() =>
        profile.updateRatings(
          wrongRatings,
          ExternalRatingProvenance.create({
            source: 'iracing',
            lastSyncedAt: new Date(),
          })
        )
      ).toThrow(IdentityDomainValidationError);
    });
  });

  describe('markVerified', () => {
    it('should mark provenance as verified', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance: ExternalRatingProvenance.create({
          source: 'iracing',
          lastSyncedAt: new Date(),
          verified: false,
        }),
      });

      profile.markVerified();

      expect(profile.provenance.verified).toBe(true);
    });
  });

  describe('updateLastSyncedAt', () => {
    it('should update last synced timestamp', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const newDate = new Date('2024-01-03');
      profile.updateLastSyncedAt(newDate);

      expect(profile.provenance.lastSyncedAt).toEqual(newDate);
    });
  });

  describe('toSummary', () => {
    it('should return summary object', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const summary = profile.toSummary();

      expect(summary.userId).toBe('user-123');
      expect(summary.gameKey).toBe('iracing');
      expect(summary.ratingCount).toBe(2);
      expect(summary.ratingTypes).toEqual(['safety', 'skill']);
      expect(summary.source).toBe('iracing');
      expect(summary.verified).toBe(true);
    });
  });

  describe('toJSON', () => {
    it('should serialize to JSON format', () => {
      const profile = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const json = profile.toJSON();

      expect(json.userId).toBe('user-123');
      expect(json.gameKey).toBe('iracing');
      expect(json.ratings).toHaveLength(2);
      expect(json.provenance.source).toBe('iracing');
      expect(json.provenance.verified).toBe(true);
    });
  });

  describe('equals', () => {
    it('should return true for identical profiles', () => {
      const profile1 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const profile2 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      expect(profile1.equals(profile2)).toBe(true);
    });

    it('should return false for different userId', () => {
      const profile1 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const profile2 = ExternalGameRatingProfile.create({
        userId: UserId.fromString('user-456'),
        gameKey,
        ratings,
        provenance,
      });

      expect(profile1.equals(profile2)).toBe(false);
    });

    it('should return false for different gameKey', () => {
      const profile1 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const differentGameKey = GameKey.create('assetto');
      const differentRatings = new Map([
        ['safety', ExternalRating.create(differentGameKey, 'safety', 85.5)],
      ]);
      
      const profile2 = ExternalGameRatingProfile.create({
        userId,
        gameKey: differentGameKey,
        ratings: differentRatings,
        provenance,
      });

      expect(profile1.equals(profile2)).toBe(false);
    });

    it('should return false for different ratings', () => {
      const profile1 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const differentRatings = new Map([
        ['safety', ExternalRating.create(gameKey, 'safety', 99.0)],
      ]);
      const profile2 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings: differentRatings,
        provenance,
      });

      expect(profile1.equals(profile2)).toBe(false);
    });

    it('should return false for different provenance', () => {
      const profile1 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance,
      });

      const differentProvenance = ExternalRatingProvenance.create({
        source: 'different',
        lastSyncedAt: new Date(),
      });
      const profile2 = ExternalGameRatingProfile.create({
        userId,
        gameKey,
        ratings,
        provenance: differentProvenance,
      });

      expect(profile1.equals(profile2)).toBe(false);
    });
  });
});