import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AggregationService } from './aggregation.service';
import {
  AggregatedMetric,
  MetricType,
  TimeGranularity,
} from '../entities/aggregated-metric.entity';

describe('AggregationService', () => {
  let service: AggregationService;
  let mockRepository: {
    query: ReturnType<typeof vi.fn>;
  };
  let mockRedisSession: {
    getSession: ReturnType<typeof vi.fn>;
    setSession: ReturnType<typeof vi.fn>;
    deleteSession: ReturnType<typeof vi.fn>;
    hasSeenUser: ReturnType<typeof vi.fn>;
    markUserSeen: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    // Track session states in memory for test consistency
    const sessionStates = new Map<string, any>();
    const seenUsers = new Set<string>();

    mockRepository = {
      query: vi.fn().mockResolvedValue([]),
    };

    mockRedisSession = {
      getSession: vi.fn().mockImplementation(async (sessionId: string) => {
        return sessionStates.get(sessionId) || null;
      }),
      setSession: vi.fn().mockImplementation(async (sessionId: string, state: any) => {
        sessionStates.set(sessionId, state);
      }),
      deleteSession: vi.fn().mockImplementation(async (sessionId: string) => {
        sessionStates.delete(sessionId);
      }),
      hasSeenUser: vi.fn().mockImplementation(async (userId: string) => {
        return seenUsers.has(userId);
      }),
      markUserSeen: vi.fn().mockImplementation(async (userId: string) => {
        seenUsers.add(userId);
      }),
    };

    // Manually create instance with mocked dependencies
    service = new AggregationService(
      mockRepository as any,
      mockRedisSession as any,
    );
  });

  afterEach(() => {
    vi.clearAllTimers();
  });

  describe('processEvent', () => {
    describe('pageView events', () => {
      it('should increment page views metric', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:30:45Z'),
          sessionId: 'session-123',
          properties: {
            path: '/home',
          },
        };

        await service.processEvent(event);

        // Should increment page views
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.stringContaining('INSERT INTO aggregated_metrics'),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should track page views by path dimension', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:30:45Z'),
          sessionId: 'session-123',
          properties: {
            path: '/products/123',
          },
        };

        await service.processEvent(event);

        // Should track path dimension
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'path',
            '/products/123',
          ]),
        );
      });

      it('should track page views by device type', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:30:45Z'),
          sessionId: 'session-123',
          properties: {
            path: '/home',
            deviceType: 'mobile',
          },
        };

        await service.processEvent(event);

        // Should track device type dimension
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'device_type',
            'mobile',
          ]),
        );
      });

      it('should track page views by traffic source', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:30:45Z'),
          sessionId: 'session-123',
          properties: {
            path: '/home',
            trafficSource: 'google',
          },
        };

        await service.processEvent(event);

        // Should track traffic source dimension
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'traffic_source',
            'google',
          ]),
        );
      });

      it('should handle pageview event type (lowercase)', async () => {
        const event = {
          eventType: 'pageview',
          timestamp: new Date('2026-01-29T12:30:45Z'),
          sessionId: 'session-123',
          properties: {},
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.PAGE_VIEWS]),
        );
      });
    });

    describe('session_start events', () => {
      it('should increment sessions metric', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-new',
          userId: 'user-123',
          properties: {},
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.SESSIONS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should track new users', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-new',
          userId: 'user-first-time',
          properties: {},
        };

        await service.processEvent(event);

        // Should increment new users
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.NEW_USERS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should track returning users', async () => {
        const userId = 'user-returning';

        // First session - new user
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-1',
          userId,
          properties: {},
        });

        mockRepository.query.mockClear();

        // Second session - returning user
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T13:00:00Z'),
          sessionId: 'session-2',
          userId,
          properties: {},
        });

        // Should increment returning users
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.RETURNING_USERS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should track sessions by traffic source', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-123',
          properties: {
            trafficSource: 'facebook',
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.SESSIONS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'traffic_source',
            'facebook',
          ]),
        );
      });

      it('should track sessions by device type', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-123',
          properties: {
            deviceType: 'tablet',
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.SESSIONS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'device_type',
            'tablet',
          ]),
        );
      });

      it('should track sessions by country', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-123',
          properties: {
            country: 'US',
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.SESSIONS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'country',
            'US',
          ]),
        );
      });

      it('should not track user metrics when userId is not provided', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-anonymous',
          properties: {},
        };

        await service.processEvent(event);

        // Should not call query for NEW_USERS or RETURNING_USERS
        const calls = mockRepository.query.mock.calls;
        const hasUserMetrics = calls.some(
          (call) =>
            call[1]?.includes(MetricType.NEW_USERS) ||
            call[1]?.includes(MetricType.RETURNING_USERS),
        );

        expect(hasUserMetrics).toBe(false);
      });
    });

    describe('session_end events', () => {
      it('should calculate and track session duration', async () => {
        const sessionId = 'session-duration';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        mockRepository.query.mockClear();

        // End session 2 minutes later
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:02:00Z'),
          sessionId,
          properties: {},
        });

        // Should track duration (120 seconds)
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.AVG_SESSION_DURATION,
            TimeGranularity.HOUR,
            expect.any(Date),
            120,
          ]),
        );
      });

      it('should track pages per session', async () => {
        const sessionId = 'session-pages';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Page views
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId,
          properties: { path: '/page1' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:01:00Z'),
          sessionId,
          properties: { path: '/page2' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:01:30Z'),
          sessionId,
          properties: { path: '/page3' },
        });

        mockRepository.query.mockClear();

        // End session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:02:00Z'),
          sessionId,
          properties: {},
        });

        // Should track 3 pages per session
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGES_PER_SESSION,
            TimeGranularity.HOUR,
            expect.any(Date),
            3,
          ]),
        );
      });

      it('should identify engaged session by duration', async () => {
        const sessionId = 'session-engaged-duration';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        mockRepository.query.mockClear();

        // End session after 15 seconds (>= 10 second threshold)
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:15Z'),
          sessionId,
          properties: {},
        });

        // Should track engaged session
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.ENGAGED_SESSIONS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should identify engaged session by page views', async () => {
        const sessionId = 'session-engaged-pages';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Two page views
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:01Z'),
          sessionId,
          properties: { path: '/page1' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:02Z'),
          sessionId,
          properties: { path: '/page2' },
        });

        mockRepository.query.mockClear();

        // End session after 5 seconds (short duration, but 2+ pages)
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:05Z'),
          sessionId,
          properties: {},
        });

        // Should track engaged session
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.ENGAGED_SESSIONS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should identify engaged session by conversion', async () => {
        const sessionId = 'session-engaged-conversion';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // One page view
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:01Z'),
          sessionId,
          properties: { path: '/product' },
        });

        // Conversion event
        await service.processEvent({
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:00:02Z'),
          sessionId,
          properties: { revenue: 50 },
        });

        mockRepository.query.mockClear();

        // End session after 3 seconds (short duration, 1 page, but has conversion)
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:03Z'),
          sessionId,
          properties: {},
        });

        // Should track engaged session
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.ENGAGED_SESSIONS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should identify bounce session', async () => {
        const sessionId = 'session-bounce';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Single page view
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:01Z'),
          sessionId,
          properties: { path: '/landing' },
        });

        mockRepository.query.mockClear();

        // End session after 2 seconds (short duration, 1 page, no conversion)
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:02Z'),
          sessionId,
          properties: {},
        });

        // Should track bounce
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.BOUNCE_RATE,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
          ]),
        );
      });

      it('should not mark bounce if conversion occurred', async () => {
        const sessionId = 'session-no-bounce';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Single page view
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:01Z'),
          sessionId,
          properties: { path: '/product' },
        });

        // Conversion
        await service.processEvent({
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:00:02Z'),
          sessionId,
          properties: { revenue: 25 },
        });

        mockRepository.query.mockClear();

        // End session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:03Z'),
          sessionId,
          properties: {},
        });

        // Should NOT track bounce
        const calls = mockRepository.query.mock.calls;
        const hasBounce = calls.some(
          (call) => call[1]?.includes(MetricType.BOUNCE_RATE),
        );

        expect(hasBounce).toBe(false);
      });

      it('should track engaged sessions by traffic source', async () => {
        const sessionId = 'session-source';

        // Start session with traffic source
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {
            trafficSource: 'twitter',
          },
        });

        // Multiple page views for engagement
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:01Z'),
          sessionId,
          properties: { path: '/page1' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:02Z'),
          sessionId,
          properties: { path: '/page2' },
        });

        mockRepository.query.mockClear();

        // End session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:10Z'),
          sessionId,
          properties: {},
        });

        // Should track engaged session by traffic source
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.ENGAGED_SESSIONS,
            TimeGranularity.DAY,
            expect.any(Date),
            1,
            'traffic_source',
            'twitter',
          ]),
        );
      });
    });

    describe('conversion events', () => {
      it('should track purchase conversion', async () => {
        const event = {
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:30:00Z'),
          sessionId: 'session-purchase',
          properties: {
            revenue: 99.99,
          },
        };

        await service.processEvent(event);

        // Should track event count
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.EVENT_COUNT,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'event_type',
            'purchase',
          ]),
        );

        // Should track revenue
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.REVENUE,
            TimeGranularity.HOUR,
            expect.any(Date),
            99.99,
          ]),
        );
      });

      it('should track conversion event', async () => {
        const event = {
          eventType: 'conversion',
          timestamp: new Date('2026-01-29T12:30:00Z'),
          sessionId: 'session-convert',
          properties: {
            conversionType: 'signup',
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.CONVERSION_RATE,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'conversion_type',
            'signup',
          ]),
        );
      });

      it('should mark session as having conversion', async () => {
        const sessionId = 'session-has-conversion';

        // Start session
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Conversion
        await service.processEvent({
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId,
          properties: { revenue: 50 },
        });

        // Single page view
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:31Z'),
          sessionId,
          properties: { path: '/thank-you' },
        });

        mockRepository.query.mockClear();

        // End session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:00:35Z'),
          sessionId,
          properties: {},
        });

        // Should be engaged (has conversion) and NOT bounce
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.ENGAGED_SESSIONS]),
        );

        const calls = mockRepository.query.mock.calls;
        const hasBounce = calls.some(
          (call) => call[1]?.includes(MetricType.BOUNCE_RATE),
        );
        expect(hasBounce).toBe(false);
      });

      it('should handle conversion without revenue', async () => {
        const event = {
          eventType: 'conversion',
          timestamp: new Date('2026-01-29T12:30:00Z'),
          sessionId: 'session-no-revenue',
          properties: {
            conversionType: 'newsletter_signup',
          },
        };

        await service.processEvent(event);

        // Should track event but not revenue
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.EVENT_COUNT]),
        );

        const calls = mockRepository.query.mock.calls;
        const hasRevenue = calls.some(
          (call) => call[1]?.includes(MetricType.REVENUE),
        );
        expect(hasRevenue).toBe(false);
      });
    });

    describe('custom events', () => {
      it('should track unknown event types as event_count', async () => {
        const event = {
          eventType: 'custom_action',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-custom',
          properties: {},
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.EVENT_COUNT,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'event_type',
            'custom_action',
          ]),
        );
      });

      it('should handle multiple custom event types', async () => {
        const events = [
          'video_play',
          'video_pause',
          'form_submit',
          'button_click',
        ];

        for (const eventType of events) {
          await service.processEvent({
            eventType,
            timestamp: new Date('2026-01-29T12:00:00Z'),
            sessionId: 'session-multi',
            properties: {},
          });
        }

        // Each should be tracked separately
        events.forEach((eventType) => {
          expect(mockRepository.query).toHaveBeenCalledWith(
            expect.any(String),
            expect.arrayContaining([
              MetricType.EVENT_COUNT,
              TimeGranularity.HOUR,
              expect.any(Date),
              1,
              'event_type',
              eventType,
            ]),
          );
        });
      });
    });

    describe('session state management', () => {
      it('should create new session state on first event', async () => {
        const sessionId = 'session-state-new';

        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // Verify state is created (indirect - test through subsequent events)
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId,
          properties: {},
        });

        // Should succeed without errors
        expect(mockRepository.query).toHaveBeenCalled();
      });

      it('should update session state across events', async () => {
        const sessionId = 'session-state-update';

        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId,
          properties: { path: '/page1' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:01:00Z'),
          sessionId,
          properties: { path: '/page2' },
        });

        mockRepository.query.mockClear();

        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:02:00Z'),
          sessionId,
          properties: {},
        });

        // Should track 2 page views
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.PAGES_PER_SESSION, expect.any(String), expect.any(Date), 2]),
        );
      });

      it('should update userId if provided later', async () => {
        const sessionId = 'session-late-user';

        // Anonymous session start
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId,
          properties: {},
        });

        // User logs in during session
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId,
          userId: 'user-late',
          properties: { path: '/login-success' },
        });

        // Session state should now have userId
        // Verified indirectly through no errors
        expect(mockRepository.query).toHaveBeenCalled();
      });

      it('should track separate state for concurrent sessions', async () => {
        const session1 = 'session-concurrent-1';
        const session2 = 'session-concurrent-2';

        // Start both sessions
        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: session1,
          properties: {},
        });

        await service.processEvent({
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T12:00:05Z'),
          sessionId: session2,
          properties: {},
        });

        // Different page views for each
        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:30Z'),
          sessionId: session1,
          properties: { path: '/page1' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:35Z'),
          sessionId: session2,
          properties: { path: '/page2' },
        });

        await service.processEvent({
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:40Z'),
          sessionId: session2,
          properties: { path: '/page3' },
        });

        mockRepository.query.mockClear();

        // End first session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:01:00Z'),
          sessionId: session1,
          properties: {},
        });

        // Should track 1 page for session1
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.PAGES_PER_SESSION, expect.any(String), expect.any(Date), 1]),
        );

        mockRepository.query.mockClear();

        // End second session
        await service.processEvent({
          eventType: 'session_end',
          timestamp: new Date('2026-01-29T12:01:30Z'),
          sessionId: session2,
          properties: {},
        });

        // Should track 2 pages for session2
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.PAGES_PER_SESSION, expect.any(String), expect.any(Date), 2]),
        );
      });
    });

    describe('time bucket calculation', () => {
      it('should calculate hour bucket correctly', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:34:56.789Z'),
          sessionId: 'session-hour',
          properties: {},
        };

        await service.processEvent(event);

        // Hour bucket should be 2026-01-29T12:00:00.000Z
        const expectedBucket = new Date('2026-01-29T12:00:00.000Z');

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            expectedBucket,
            1,
          ]),
        );
      });

      it('should calculate day bucket correctly', async () => {
        const event = {
          eventType: 'session_start',
          timestamp: new Date('2026-01-29T23:59:59.999Z'),
          sessionId: 'session-day',
          properties: {},
        };

        await service.processEvent(event);

        // Day bucket should be 2026-01-29T00:00:00.000Z
        const expectedBucket = new Date('2026-01-29T00:00:00.000Z');

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.SESSIONS,
            TimeGranularity.HOUR,
            expect.any(Date),
          ]),
        );
      });

      it('should handle events at exact hour boundaries', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00.000Z'),
          sessionId: 'session-boundary',
          properties: {},
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            new Date('2026-01-29T12:00:00.000Z'),
            1,
          ]),
        );
      });
    });

    describe('error handling', () => {
      it('should throw error when database query fails', async () => {
        const dbError = new Error('Database connection lost');
        mockRepository.query.mockRejectedValueOnce(dbError);

        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-error',
          properties: {},
        };

        await expect(service.processEvent(event)).rejects.toThrow('Database connection lost');
      });

      it('should propagate errors for retry handling', async () => {
        const error = new Error('Temporary failure');
        mockRepository.query.mockRejectedValueOnce(error);

        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-retry',
          properties: {},
        };

        await expect(service.processEvent(event)).rejects.toThrow(error);
      });

      it('should handle partial query failures in multi-dimensional tracking', async () => {
        // First query succeeds, second fails
        mockRepository.query
          .mockResolvedValueOnce([])
          .mockRejectedValueOnce(new Error('Dimension insert failed'));

        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-partial',
          properties: {
            path: '/test',
          },
        };

        // Should throw on the second query
        await expect(service.processEvent(event)).rejects.toThrow('Dimension insert failed');
      });
    });

    describe('edge cases', () => {
      it('should handle events with missing optional properties', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-minimal',
          properties: {},
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalled();
      });

      it('should handle zero revenue', async () => {
        const event = {
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-free',
          properties: {
            revenue: 0,
          },
        };

        await service.processEvent(event);

        // Should track event but not revenue (0 is falsy)
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.EVENT_COUNT]),
        );

        // Revenue should NOT be tracked when value is 0 (falsy check in code)
        const calls = mockRepository.query.mock.calls;
        const hasRevenue = calls.some(
          (call: any[]) => call[1]?.includes(MetricType.REVENUE),
        );
        expect(hasRevenue).toBe(false);
      });

      it('should handle very large revenue values', async () => {
        const event = {
          eventType: 'purchase',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-large',
          properties: {
            revenue: 999999.99,
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.REVENUE, expect.any(String), expect.any(Date), 999999.99]),
        );
      });

      it('should handle special characters in dimensions', async () => {
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-special',
          properties: {
            path: '/products/特殊商品?query=テスト',
            trafficSource: 'email-campaign-🎉',
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'path',
            '/products/特殊商品?query=テスト',
          ]),
        );
      });

      it('should handle very long dimension values', async () => {
        const longPath = '/products/' + 'a'.repeat(500);
        const event = {
          eventType: 'pageView',
          timestamp: new Date('2026-01-29T12:00:00Z'),
          sessionId: 'session-long',
          properties: {
            path: longPath,
          },
        };

        await service.processEvent(event);

        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([
            MetricType.PAGE_VIEWS,
            TimeGranularity.HOUR,
            expect.any(Date),
            1,
            'path',
            longPath,
          ]),
        );
      });

      it('should handle instant session (start and end at same time)', async () => {
        const sessionId = 'session-instant';
        const timestamp = new Date('2026-01-29T12:00:00Z');

        await service.processEvent({
          eventType: 'session_start',
          timestamp,
          sessionId,
          properties: {},
        });

        mockRepository.query.mockClear();

        await service.processEvent({
          eventType: 'session_end',
          timestamp,
          sessionId,
          properties: {},
        });

        // Duration should be 0 seconds
        expect(mockRepository.query).toHaveBeenCalledWith(
          expect.any(String),
          expect.arrayContaining([MetricType.AVG_SESSION_DURATION, expect.any(String), expect.any(Date), 0]),
        );
      });
    });
  });
});
