import { RecordRaceRatingEventsUseCase } from './RecordRaceRatingEventsUseCase';
import { IRaceResultsProvider, RaceResultsData } from '../ports/IRaceResultsProvider';
import { IRatingEventRepository } from '../../domain/repositories/IRatingEventRepository';
import { IUserRatingRepository } from '../../domain/repositories/IUserRatingRepository';
import { AppendRatingEventsUseCase } from './AppendRatingEventsUseCase';
import { UserRating } from '../../domain/value-objects/UserRating';
import { RatingEvent } from '../../domain/entities/RatingEvent';
import { RatingEventId } from '../../domain/value-objects/RatingEventId';
import { RatingDimensionKey } from '../../domain/value-objects/RatingDimensionKey';
import { RatingDelta } from '../../domain/value-objects/RatingDelta';

// In-memory implementations for integration testing
class InMemoryRaceResultsProvider implements IRaceResultsProvider {
  private results: Map<string, RaceResultsData> = new Map();

  async getRaceResults(raceId: string): Promise<RaceResultsData | null> {
    return this.results.get(raceId) || null;
  }

  async hasRaceResults(raceId: string): Promise<boolean> {
    return this.results.has(raceId);
  }

  // Helper for tests
  setRaceResults(raceId: string, results: RaceResultsData) {
    this.results.set(raceId, results);
  }
}

class InMemoryRatingEventRepository implements IRatingEventRepository {
  private events: Map<string, RatingEvent[]> = new Map();

  async save(event: RatingEvent): Promise<RatingEvent> {
    const userId = event.userId;
    if (!this.events.has(userId)) {
      this.events.set(userId, []);
    }
    this.events.get(userId)!.push(event);
    return event;
  }

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

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

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

  async findEventsPaginated(userId: string, options?: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedQueryOptions): Promise<import('@core/identity/domain/repositories/IRatingEventRepository').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: import('@core/identity/domain/repositories/IRatingEventRepository').PaginatedResult<RatingEvent> = {
      items,
      total,
      limit,
      offset,
      hasMore
    };
    
    if (nextOffset !== undefined) {
      result.nextOffset = nextOffset;
    }

    return result;
  }

  // Helper for tests
  clear() {
    this.events.clear();
  }

  getAllEvents(): RatingEvent[] {
    return Array.from(this.events.values()).flat();
  }
}

class InMemoryUserRatingRepository implements IUserRatingRepository {
  private ratings: Map<string, UserRating> = new Map();

  async findByUserId(userId: string): Promise<UserRating | null> {
    return this.ratings.get(userId) || null;
  }

  async save(userRating: UserRating): Promise<UserRating> {
    this.ratings.set(userRating.userId, userRating);
    return userRating;
  }

  // Helper for tests
  getAllRatings(): Map<string, UserRating> {
    return new Map(this.ratings);
  }

  clear() {
    this.ratings.clear();
  }
}

describe('RecordRaceRatingEventsUseCase - Integration', () => {
  let useCase: RecordRaceRatingEventsUseCase;
  let raceResultsProvider: InMemoryRaceResultsProvider;
  let ratingEventRepository: InMemoryRatingEventRepository;
  let userRatingRepository: InMemoryUserRatingRepository;
  let appendRatingEventsUseCase: AppendRatingEventsUseCase;

  beforeEach(() => {
    raceResultsProvider = new InMemoryRaceResultsProvider();
    ratingEventRepository = new InMemoryRatingEventRepository();
    userRatingRepository = new InMemoryUserRatingRepository();
    appendRatingEventsUseCase = new AppendRatingEventsUseCase(
      ratingEventRepository,
      userRatingRepository
    );
    useCase = new RecordRaceRatingEventsUseCase(
      raceResultsProvider,
      ratingEventRepository,
      userRatingRepository,
      appendRatingEventsUseCase
    );
  });

  describe('Full flow: race facts -> events -> persist -> snapshot', () => {
    it('should complete full flow for single driver with good performance', async () => {
      // Step 1: Setup race facts
      const raceFacts: RaceResultsData = {
        raceId: 'race-001',
        results: [
          {
            userId: 'driver-001',
            startPos: 8,
            finishPos: 2,
            incidents: 0,
            status: 'finished',
            sof: 2500,
          },
        ],
      };

      raceResultsProvider.setRaceResults('race-001', raceFacts);

      // Step 2: Create initial user rating
      const initialRating = UserRating.create('driver-001');
      await userRatingRepository.save(initialRating);

      // Step 3: Execute use case
      const result = await useCase.execute({ raceId: 'race-001' });

      // Step 4: Verify success
      expect(result.success).toBe(true);
      expect(result.raceId).toBe('race-001');
      expect(result.eventsCreated).toBeGreaterThan(0);
      expect(result.driversUpdated).toContain('driver-001');
      expect(result.errors).toEqual([]);

      // Step 5: Verify events were persisted
      const events = await ratingEventRepository.findByUserId('driver-001');
      expect(events.length).toBeGreaterThan(0);

      // Step 6: Verify snapshot was updated
      const updatedRating = await userRatingRepository.findByUserId('driver-001');
      expect(updatedRating).toBeDefined();
      expect(updatedRating!.driver.value).toBeGreaterThan(initialRating.driver.value);
      expect(updatedRating!.driver.sampleSize).toBeGreaterThan(0);
      expect(updatedRating!.driver.confidence).toBeGreaterThan(0);

      // Step 7: Verify event details
      const performanceEvent = events.find(e => e.reason.code === 'DRIVING_FINISH_STRENGTH_GAIN');
      expect(performanceEvent).toBeDefined();
      expect(performanceEvent!.delta.value).toBeGreaterThan(0);
      expect(performanceEvent!.source.id).toBe('race-001');
    });

    it('should handle multiple drivers with mixed results', async () => {
      // Setup race with multiple drivers
      const raceFacts: RaceResultsData = {
        raceId: 'race-002',
        results: [
          {
            userId: 'driver-001',
            startPos: 5,
            finishPos: 1,
            incidents: 0,
            status: 'finished',
            sof: 2500,
          },
          {
            userId: 'driver-002',
            startPos: 3,
            finishPos: 8,
            incidents: 3,
            status: 'finished',
            sof: 2500,
          },
          {
            userId: 'driver-003',
            startPos: 5,
            finishPos: 5,
            incidents: 0,
            status: 'dns',
            sof: 2500,
          },
        ],
      };

      raceResultsProvider.setRaceResults('race-002', raceFacts);

      // Create initial ratings
      await userRatingRepository.save(UserRating.create('driver-001'));
      await userRatingRepository.save(UserRating.create('driver-002'));
      await userRatingRepository.save(UserRating.create('driver-003'));

      // Execute
      const result = await useCase.execute({ raceId: 'race-002' });

      // Verify
      expect(result.success).toBe(true);
      expect(result.driversUpdated.length).toBe(3);
      expect(result.eventsCreated).toBeGreaterThan(0);

      // Check each driver
      for (const driverId of ['driver-001', 'driver-002', 'driver-003']) {
        const events = await ratingEventRepository.findByUserId(driverId);
        expect(events.length).toBeGreaterThan(0);

        const rating = await userRatingRepository.findByUserId(driverId);
        expect(rating).toBeDefined();
        expect(rating!.driver.sampleSize).toBeGreaterThan(0);
      }

      // driver-001 should have positive delta
      const driver1Rating = await userRatingRepository.findByUserId('driver-001');
      expect(driver1Rating!.driver.value).toBeGreaterThan(50);

      // driver-002 should have negative delta (poor position + incidents)
      const driver2Rating = await userRatingRepository.findByUserId('driver-002');
      expect(driver2Rating!.driver.value).toBeLessThan(50);

      // driver-003 should have negative delta (DNS)
      const driver3Rating = await userRatingRepository.findByUserId('driver-003');
      expect(driver3Rating!.driver.value).toBeLessThan(50);
    });

    it('should compute SoF when not provided', async () => {
      const raceFacts: RaceResultsData = {
        raceId: 'race-003',
        results: [
          {
            userId: 'driver-001',
            startPos: 5,
            finishPos: 2,
            incidents: 0,
            status: 'finished',
            // No sof
          },
          {
            userId: 'driver-002',
            startPos: 3,
            finishPos: 8,
            incidents: 0,
            status: 'finished',
            // No sof
          },
        ],
      };

      raceResultsProvider.setRaceResults('race-003', raceFacts);

      // Set ratings for SoF calculation
      const rating1 = UserRating.create('driver-001').updateDriverRating(60);
      const rating2 = UserRating.create('driver-002').updateDriverRating(40);
      await userRatingRepository.save(rating1);
      await userRatingRepository.save(rating2);

      // Execute
      const result = await useCase.execute({ raceId: 'race-003' });

      // Verify SoF was computed (average of 60 and 40 = 50)
      expect(result.success).toBe(true);
      expect(result.eventsCreated).toBeGreaterThan(0);

      // Events should be created with computed SoF
      const events = await ratingEventRepository.getAllByUserId('driver-001');
      expect(events.length).toBeGreaterThan(0);
    });

    it('should handle partial failures gracefully', async () => {
      const raceFacts: RaceResultsData = {
        raceId: 'race-004',
        results: [
          {
            userId: 'driver-001',
            startPos: 5,
            finishPos: 2,
            incidents: 0,
            status: 'finished',
            sof: 2500,
          },
          {
            userId: 'driver-002',
            startPos: 3,
            finishPos: 8,
            incidents: 0,
            status: 'finished',
            sof: 2500,
          },
        ],
      };

      raceResultsProvider.setRaceResults('race-004', raceFacts);

      // Only create rating for first driver
      await userRatingRepository.save(UserRating.create('driver-001'));

      // Execute
      const result = await useCase.execute({ raceId: 'race-004' });

      // Should have partial success
      expect(result.raceId).toBe('race-004');
      expect(result.driversUpdated).toContain('driver-001');
      expect(result.errors).toBeDefined();
      expect(result.errors!.length).toBeGreaterThan(0);
    });

    it('should maintain event immutability and ordering', async () => {
      const raceFacts: RaceResultsData = {
        raceId: 'race-005',
        results: [
          {
            userId: 'driver-001',
            startPos: 5,
            finishPos: 2,
            incidents: 1,
            status: 'finished',
            sof: 2500,
          },
        ],
      };

      raceResultsProvider.setRaceResults('race-005', raceFacts);
      await userRatingRepository.save(UserRating.create('driver-001'));

      // Execute multiple times
      await useCase.execute({ raceId: 'race-005' });
      const result1 = await ratingEventRepository.findByUserId('driver-001');

      // Execute again (should add more events)
      await useCase.execute({ raceId: 'race-005' });
      const result2 = await ratingEventRepository.findByUserId('driver-001');

      // Events should accumulate
      expect(result2.length).toBeGreaterThan(result1.length);

      // All events should be immutable
      for (const event of result2) {
        expect(event.id).toBeDefined();
        expect(event.createdAt).toBeDefined();
        expect(event.occurredAt).toBeDefined();
      }
    });

    it('should update snapshot with weighted average and confidence', async () => {
      // Multiple races for same driver
      const race1: RaceResultsData = {
        raceId: 'race-006',
        results: [{ userId: 'driver-001', startPos: 10, finishPos: 5, incidents: 0, status: 'finished', sof: 2500 }],
      };
      const race2: RaceResultsData = {
        raceId: 'race-007',
        results: [{ userId: 'driver-001', startPos: 5, finishPos: 2, incidents: 0, status: 'finished', sof: 2500 }],
      };

      raceResultsProvider.setRaceResults('race-006', race1);
      raceResultsProvider.setRaceResults('race-007', race2);

      await userRatingRepository.save(UserRating.create('driver-001'));

      // Execute first race
      await useCase.execute({ raceId: 'race-006' });
      const rating1 = await userRatingRepository.findByUserId('driver-001');
      expect(rating1!.driver.sampleSize).toBe(1);

      // Execute second race
      await useCase.execute({ raceId: 'race-007' });
      const rating2 = await userRatingRepository.findByUserId('driver-001');
      expect(rating2!.driver.sampleSize).toBe(2);
      expect(rating2!.driver.confidence).toBeGreaterThan(rating1!.driver.confidence);

      // Trend should be calculated
      expect(rating2!.driver.trend).toBeDefined();
    });
  });
});
