import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { SessionsService } from './sessions.service';
import { createMockDataSource, MockDataSource } from '@test/mocks';
import {
  createSessionListResults,
  createSessionCountResult,
  createSessionMetricsResult,
} from '@test/fixtures';
import { SessionSortBy } from './dto/sessions-query.dto';

describe('SessionsService', () => {
  let service: SessionsService;
  let mockDataSource: MockDataSource;

  beforeEach(async () => {
    mockDataSource = createMockDataSource();

    const module = await Test.createTestingModule({
      providers: [
        SessionsService,
        {
          provide: DataSource,
          useValue: mockDataSource,
        },
      ],
    }).compile();

    service = module.get<SessionsService>(SessionsService);
  });

  describe('getSessions', () => {
    it('returns paginated session list', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        limit: 50,
        offset: 0,
      };

      const sessionData = createSessionListResults();
      const countData = createSessionCountResult(150);

      mockDataSource.query
        .mockResolvedValueOnce(sessionData)
        .mockResolvedValueOnce(countData);

      const result = await service.getSessions(query);

      expect(result).toEqual({
        sessions: [
          {
            sessionId: 'sess-001',
            userId: 'user-001',
            startTime: new Date('2026-01-15T10:00:00Z'),
            lastActivityTime: new Date('2026-01-15T10:05:30Z'),
            durationSeconds: 330,
            pageViews: 5,
            totalEvents: 12,
            hasConversion: true,
            trafficSource: 'organic',
            deviceType: 'desktop',
            browser: 'Chrome',
            os: 'Windows',
            country: 'Iceland',
            city: 'Reykjavik',
            landingPage: '/',
          },
          {
            sessionId: 'sess-002',
            userId: 'user-002',
            startTime: new Date('2026-01-15T11:00:00Z'),
            lastActivityTime: new Date('2026-01-15T11:01:00Z'),
            durationSeconds: 60,
            pageViews: 1,
            totalEvents: 2,
            hasConversion: false,
            trafficSource: 'direct',
            deviceType: 'mobile',
            browser: 'Safari',
            os: 'iOS',
            country: 'United States',
            city: 'New York',
            landingPage: '/profiles',
          },
        ],
        total: 150,
        limit: 50,
        offset: 0,
      });

      expect(mockDataSource.query).toHaveBeenCalledTimes(2);
    });

    it('maps raw rows to SessionRecord objects correctly', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
      };

      const customSessionData = [
        {
          sessionId: 'sess-custom',
          userId: null,
          start_time: '2026-01-15T12:00:00Z',
          last_activity_time: '2026-01-15T12:03:45Z',
          duration_seconds: '225',
          page_views: '3',
          total_events: '8',
          has_conversion: false,
          trafficSource: null,
          deviceType: 'tablet',
          browser: 'Firefox',
          os: 'Android',
          country: 'Canada',
          city: null,
          landingPage: '/about',
        },
      ];

      mockDataSource.query
        .mockResolvedValueOnce(customSessionData)
        .mockResolvedValueOnce(createSessionCountResult(1));

      const result = await service.getSessions(query);

      expect(result.sessions).toHaveLength(1);
      expect(result.sessions[0]).toEqual({
        sessionId: 'sess-custom',
        userId: null,
        startTime: new Date('2026-01-15T12:00:00Z'),
        lastActivityTime: new Date('2026-01-15T12:03:45Z'),
        durationSeconds: 225,
        pageViews: 3,
        totalEvents: 8,
        hasConversion: false,
        trafficSource: null,
        deviceType: 'tablet',
        browser: 'Firefox',
        os: 'Android',
        country: 'Canada',
        city: null,
        landingPage: '/about',
      });
    });

    it('returns empty sessions with total 0 for no results', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce([{ total: '0' }]);

      const result = await service.getSessions(query);

      expect(result).toEqual({
        sessions: [],
        total: 0,
        limit: 50,
        offset: 0,
      });
    });

    it('uses default limit 50 and offset 0', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      const result = await service.getSessions(query);

      expect(result.limit).toBe(50);
      expect(result.offset).toBe(0);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([50, 0]),
      );
    });

    it('respects custom limit and offset', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        limit: 25,
        offset: 100,
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      const result = await service.getSessions(query);

      expect(result.limit).toBe(25);
      expect(result.offset).toBe(100);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('LIMIT'),
        expect.arrayContaining([25, 100]),
      );
    });

    it('filters by userId when provided', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        userId: 'user-specific',
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      await service.getSessions(query);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf.userId = $3'),
        expect.arrayContaining(['user-specific']),
      );
    });

    it('filters by trafficSource when provided', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        trafficSource: 'organic',
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      await service.getSessions(query);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf.trafficSource = $3'),
        expect.arrayContaining(['organic']),
      );
    });

    it('sorts by specified column', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        sortBy: SessionSortBy.DURATION,
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      await service.getSessions(query);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('ORDER BY duration_seconds DESC'),
        expect.any(Array),
      );
    });

    it('passes correct params to count query', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        userId: 'user-001',
        limit: 10,
        offset: 5,
      };

      mockDataSource.query
        .mockResolvedValueOnce([])
        .mockResolvedValueOnce(createSessionCountResult(0));

      await service.getSessions(query);

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        2,
        expect.stringContaining('COUNT(DISTINCT re."sessionId")'),
        ['2026-01-15', '2026-01-22', 'user-001'],
      );
    });
  });

  describe('getSessionMetrics', () => {
    it('returns aggregated session metrics', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
      };

      const metricsData = createSessionMetricsResult();
      mockDataSource.query.mockResolvedValueOnce(metricsData);

      const result = await service.getSessionMetrics(query);

      expect(result).toEqual({
        totalSessions: 1500,
        avgSessionDuration: 185.5,
        avgPageViews: 3.2,
        bounceRate: 0.20,
        engagementRate: 0.70,
        newUserRate: 0.39,
        conversionRate: 0.04,
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('WITH session_stats AS'),
        [query.startDate, query.endDate],
      );
    });

    it('handles empty results with zero defaults', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
      };

      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getSessionMetrics(query);

      expect(result).toEqual({
        totalSessions: 0,
        avgSessionDuration: 0,
        avgPageViews: 0,
        bounceRate: 0,
        engagementRate: 0,
        newUserRate: 0,
        conversionRate: 0,
      });
    });

    it('filters by trafficSource when provided', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        trafficSource: 'social',
      };

      mockDataSource.query.mockResolvedValueOnce(createSessionMetricsResult());

      await service.getSessionMetrics(query);

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('sf.trafficSource = $3'),
        ['2026-01-15', '2026-01-22', 'social'],
      );
    });

    it('filters by deviceType when provided', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        deviceType: 'mobile',
      };

      mockDataSource.query.mockResolvedValueOnce(createSessionMetricsResult());

      await service.getSessionMetrics(query);

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('sf.deviceType = $3'),
        ['2026-01-15', '2026-01-22', 'mobile'],
      );
    });

    it('filters by country when provided', async () => {
      const query = {
        startDate: '2026-01-15',
        endDate: '2026-01-22',
        country: 'Iceland',
      };

      mockDataSource.query.mockResolvedValueOnce(createSessionMetricsResult());

      await service.getSessionMetrics(query);

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('sf.country = $3'),
        ['2026-01-15', '2026-01-22', 'Iceland'],
      );
    });
  });

  describe('getSession', () => {
    it('returns session by ID', async () => {
      const sessionId = 'sess-001';
      const sessionData = createSessionListResults().slice(0, 1);

      mockDataSource.query.mockResolvedValueOnce(sessionData);

      const result = await service.getSession(sessionId);

      expect(result).toEqual({
        sessionId: 'sess-001',
        userId: 'user-001',
        startTime: new Date('2026-01-15T10:00:00Z'),
        lastActivityTime: new Date('2026-01-15T10:05:30Z'),
        durationSeconds: 330,
        pageViews: 5,
        totalEvents: 12,
        hasConversion: true,
        trafficSource: 'organic',
        deviceType: 'desktop',
        browser: 'Chrome',
        os: 'Windows',
        country: 'Iceland',
        city: 'Reykjavik',
        landingPage: '/',
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('WHERE re."sessionId" = $1'),
        [sessionId],
      );
    });

    it('returns null for non-existent session', async () => {
      const sessionId = 'non-existent';

      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getSession(sessionId);

      expect(result).toBeNull();

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('WHERE re."sessionId" = $1'),
        [sessionId],
      );
    });

    it('maps session with null values correctly', async () => {
      const sessionId = 'sess-minimal';
      const minimalSessionData = [
        {
          sessionId: 'sess-minimal',
          userId: null,
          start_time: '2026-01-15T14:00:00Z',
          last_activity_time: '2026-01-15T14:00:00Z',
          duration_seconds: '0',
          page_views: '0',
          total_events: '1',
          has_conversion: false,
          trafficSource: null,
          deviceType: null,
          browser: null,
          os: null,
          country: null,
          city: null,
          landingPage: null,
        },
      ];

      mockDataSource.query.mockResolvedValueOnce(minimalSessionData);

      const result = await service.getSession(sessionId);

      expect(result).toEqual({
        sessionId: 'sess-minimal',
        userId: null,
        startTime: new Date('2026-01-15T14:00:00Z'),
        lastActivityTime: new Date('2026-01-15T14:00:00Z'),
        durationSeconds: 0,
        pageViews: 0,
        totalEvents: 1,
        hasConversion: false,
        trafficSource: null,
        deviceType: null,
        browser: null,
        os: null,
        country: null,
        city: null,
        landingPage: null,
      });
    });
  });
});
