/**
 * Unit tests for RatingEventRepository
 */

import { RatingEvent } from '../entities/RatingEvent';
import { RatingDelta } from '../value-objects/RatingDelta';
import { RatingDimensionKey } from '../value-objects/RatingDimensionKey';
import { RatingEventId } from '../value-objects/RatingEventId';
import { FindByUserIdOptions, PaginatedQueryOptions, PaginatedResult, RatingEventRepository } from './RatingEventRepository';

// In-memory test implementation
class InMemoryRatingEventRepository implements RatingEventRepository {
  private events: RatingEvent[] = [];

  async save(event: RatingEvent): Promise<RatingEvent> {
    const existingIndex = this.events.findIndex(e => e.id.equals(event.id));
    if (existingIndex >= 0) {
      this.events[existingIndex] = event;
    } else {
      this.events.push(event);
    }
    return event;
  }

  async findByUserId(userId: string, options?: FindByUserIdOptions): Promise<RatingEvent[]> {
    let filtered = this.events.filter(e => e.userId === userId);
    
    // Sort by occurredAt, then createdAt, then id for deterministic ordering
    filtered.sort((a, b) => {
      const timeCompare = a.occurredAt.getTime() - b.occurredAt.getTime();
      if (timeCompare !== 0) return timeCompare;
      
      const createdCompare = a.createdAt.getTime() - b.createdAt.getTime();
      if (createdCompare !== 0) return createdCompare;
      
      return a.id.value.localeCompare(b.id.value);
    });

    // Apply afterId filter
    if (options?.afterId) {
      const afterIndex = filtered.findIndex(e => e.id.equals(options.afterId!));
      if (afterIndex >= 0) {
        filtered = filtered.slice(afterIndex + 1);
      }
    }

    // Apply limit
    if (options?.limit) {
      filtered = filtered.slice(0, options.limit);
    }

    return filtered;
  }

  async findByIds(ids: RatingEventId[]): Promise<RatingEvent[]> {
    return this.events.filter(e => ids.some(id => e.id.equals(id)));
  }

  async getAllByUserId(userId: string): Promise<RatingEvent[]> {
    return this.findByUserId(userId);
  }

  async findEventsPaginated(userId: string, options?: PaginatedQueryOptions): Promise<PaginatedResult<RatingEvent>> {
    const allEvents = await this.findByUserId(userId);
    
    // Apply filters
    let filtered = allEvents;
    if (options?.filter) {
      const filter = options.filter;
      if (filter.dimensions) {
        filtered = filtered.filter(e => filter.dimensions!.includes(e.dimension.value));
      }
      if (filter.sourceTypes) {
        filtered = filtered.filter(e => filter.sourceTypes!.includes(e.source.type));
      }
      if (filter.from) {
        filtered = filtered.filter(e => e.occurredAt >= filter.from!);
      }
      if (filter.to) {
        filtered = filtered.filter(e => e.occurredAt <= filter.to!);
      }
      if (filter.reasonCodes) {
        filtered = filtered.filter(e => filter.reasonCodes!.includes(e.reason.code));
      }
      if (filter.visibility) {
        filtered = filtered.filter(e => e.visibility.public === (filter.visibility === 'public'));
      }
    }

    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: PaginatedResult<RatingEvent> = {
      items,
      total,
      limit,
      offset,
      hasMore
    };
    
    if (nextOffset !== undefined) {
      result.nextOffset = nextOffset;
    }

    return result;
  }
}

describe('RatingEventRepository', () => {
  let repository: InMemoryRatingEventRepository;

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

  describe('save', () => {
    it('should save a new event', async () => {
      const event = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test event', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      const saved = await repository.save(event);
      expect(saved).toEqual(event);

      const found = await repository.findByUserId('user-1');
      expect(found).toHaveLength(1);
      expect(found[0]!.id).toEqual(event.id);
    });

    it('should update existing event', async () => {
      const event = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test event', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      await repository.save(event);

      // Rehydrate with same ID (simulating update)
      const updated = RatingEvent.rehydrate({
        id: event.id,
        userId: event.userId,
        dimension: event.dimension,
        delta: RatingDelta.create(10),
        weight: undefined,
        occurredAt: event.occurredAt,
        createdAt: event.createdAt,
        source: event.source,
        reason: event.reason,
        visibility: event.visibility,
        version: event.version,
      });

      await repository.save(updated);

      const found = await repository.findByUserId('user-1');
      expect(found).toHaveLength(1);
      expect(found[0]!.delta.value).toBe(10);
    });
  });

  describe('findByUserId', () => {
    it('should return events ordered by occurredAt', async () => {
      const events = [
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(5),
          occurredAt: new Date('2024-01-01T12:00:00Z'),
          createdAt: new Date('2024-01-01T12:00:00Z'),
          source: { type: 'race', id: 'race-2' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(3),
          occurredAt: new Date('2024-01-01T10:00:00Z'),
          createdAt: new Date('2024-01-01T10:00:00Z'),
          source: { type: 'race', id: 'race-1' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
      ];

      for (const event of events) {
        await repository.save(event);
      }

      const found = await repository.findByUserId('user-1');
      expect(found).toHaveLength(2);
      expect(found[0]!.occurredAt).toEqual(new Date('2024-01-01T10:00:00Z'));
      expect(found[1]!.occurredAt).toEqual(new Date('2024-01-01T12:00:00Z'));
    });

    it('should filter by afterId', async () => {
      const events = [
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(3),
          occurredAt: new Date('2024-01-01T10:00:00Z'),
          createdAt: new Date('2024-01-01T10:00:00Z'),
          source: { type: 'race', id: 'race-1' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(5),
          occurredAt: new Date('2024-01-01T12:00:00Z'),
          createdAt: new Date('2024-01-01T12:00:00Z'),
          source: { type: 'race', id: 'race-2' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
      ];

      for (const event of events) {
        await repository.save(event);
      }

      const found = await repository.findByUserId('user-1', {
        afterId: events[0]!.id,
      });

      expect(found).toHaveLength(1);
      expect(found[0]!.id).toEqual(events[1]!.id);
    });

    it('should limit results', async () => {
      const events = [
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(1),
          occurredAt: new Date('2024-01-01T10:00:00Z'),
          createdAt: new Date('2024-01-01T10:00:00Z'),
          source: { type: 'race', id: 'race-1' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(2),
          occurredAt: new Date('2024-01-01T11:00:00Z'),
          createdAt: new Date('2024-01-01T11:00:00Z'),
          source: { type: 'race', id: 'race-2' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(3),
          occurredAt: new Date('2024-01-01T12:00:00Z'),
          createdAt: new Date('2024-01-01T12:00:00Z'),
          source: { type: 'race', id: 'race-3' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
      ];

      for (const event of events) {
        await repository.save(event);
      }

      const found = await repository.findByUserId('user-1', { limit: 2 });
      expect(found).toHaveLength(2);
      expect(found[0]!.delta.value).toBe(1);
      expect(found[1]!.delta.value).toBe(2);
    });

    it('should return empty array for non-existent user', async () => {
      const found = await repository.findByUserId('non-existent');
      expect(found).toEqual([]);
    });
  });

  describe('findByIds', () => {
    it('should return events by IDs', async () => {
      const event1 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      const event2 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(3),
        occurredAt: new Date('2024-01-01T11:00:00Z'),
        createdAt: new Date('2024-01-01T11:00:00Z'),
        source: { type: 'race', id: 'race-2' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      await repository.save(event1);
      await repository.save(event2);

      const found = await repository.findByIds([event1.id, event2.id]);
      expect(found).toHaveLength(2);
      expect(found.map(e => e.id.value)).toContain(event1.id.value);
      expect(found.map(e => e.id.value)).toContain(event2.id.value);
    });

    it('should return empty array for non-existent IDs', async () => {
      const nonExistentId = RatingEventId.generate();
      const found = await repository.findByIds([nonExistentId]);
      expect(found).toEqual([]);
    });
  });

  describe('getAllByUserId', () => {
    it('should return all events for user', async () => {
      const events = [
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(5),
          occurredAt: new Date('2024-01-01T10:00:00Z'),
          createdAt: new Date('2024-01-01T10:00:00Z'),
          source: { type: 'race', id: 'race-1' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
        RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-2',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(3),
          occurredAt: new Date('2024-01-01T11:00:00Z'),
          createdAt: new Date('2024-01-01T11:00:00Z'),
          source: { type: 'race', id: 'race-2' },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        }),
      ];

      for (const event of events) {
        await repository.save(event);
      }

      const user1Events = await repository.getAllByUserId('user-1');
      expect(user1Events).toHaveLength(1);
      expect(user1Events[0]!.userId).toBe('user-1');

      const user2Events = await repository.getAllByUserId('user-2');
      expect(user2Events).toHaveLength(1);
      expect(user2Events[0]!.userId).toBe('user-2');
    });
  });

  describe('findEventsPaginated', () => {
    it('should return paginated results', async () => {
      // Create 15 events
      for (let i = 0; i < 15; i++) {
        const event = RatingEvent.create({
          id: RatingEventId.generate(),
          userId: 'user-1',
          dimension: RatingDimensionKey.create('driving'),
          delta: RatingDelta.create(i),
          occurredAt: new Date(`2024-01-01T${10 + i}:00:00Z`),
          createdAt: new Date(`2024-01-01T${10 + i}:00:00Z`),
          source: { type: 'race', id: `race-${i}` },
          reason: { code: 'TEST', summary: 'Test', details: {} },
          visibility: { public: true, redactedFields: [] },
          version: 1,
        });
        await repository.save(event);
      }

      const result = await repository.findEventsPaginated('user-1', { 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 dimensions', async () => {
      const event1 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      const event2 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('adminTrust'),
        delta: RatingDelta.create(3),
        occurredAt: new Date('2024-01-01T11:00:00Z'),
        createdAt: new Date('2024-01-01T11:00:00Z'),
        source: { type: 'vote', id: 'vote-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      await repository.save(event1);
      await repository.save(event2);

      const result = await repository.findEventsPaginated('user-1', {
        filter: { dimensions: ['driving'] }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.dimension.value).toBe('driving');
    });

    it('should filter by source types', async () => {
      const event1 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      const event2 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(3),
        occurredAt: new Date('2024-01-01T11:00:00Z'),
        createdAt: new Date('2024-01-01T11:00:00Z'),
        source: { type: 'vote', id: 'vote-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      await repository.save(event1);
      await repository.save(event2);

      const result = await repository.findEventsPaginated('user-1', {
        filter: { sourceTypes: ['race'] }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.source.type).toBe('race');
    });

    it('should filter by date range', async () => {
      const event1 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(5),
        occurredAt: new Date('2024-01-01T10:00:00Z'),
        createdAt: new Date('2024-01-01T10:00:00Z'),
        source: { type: 'race', id: 'race-1' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      const event2 = RatingEvent.create({
        id: RatingEventId.generate(),
        userId: 'user-1',
        dimension: RatingDimensionKey.create('driving'),
        delta: RatingDelta.create(3),
        occurredAt: new Date('2024-01-02T10:00:00Z'),
        createdAt: new Date('2024-01-02T10:00:00Z'),
        source: { type: 'race', id: 'race-2' },
        reason: { code: 'TEST', summary: 'Test', details: {} },
        visibility: { public: true, redactedFields: [] },
        version: 1,
      });

      await repository.save(event1);
      await repository.save(event2);

      const result = await repository.findEventsPaginated('user-1', {
        filter: {
          from: new Date('2024-01-02T00:00:00Z'),
          to: new Date('2024-01-02T23:59:59Z')
        }
      });

      expect(result.items).toHaveLength(1);
      expect(result.items[0]!.occurredAt).toEqual(new Date('2024-01-02T10:00:00Z'));
    });

    it('should return empty result when no events match', async () => {
      const result = await repository.findEventsPaginated('non-existent', {
        filter: { dimensions: ['driving'] }
      });

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