import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { EngagementService } from './engagement.service';
import { createMockDataSource, type MockDataSource } from '@test/mocks';
import {
  createEngagementOverviewResult,
  createUniqueViewsResult,
  createPageResults,
  createEventResults,
  createScrollDepthResults,
  createUserFlowResults,
} from '@test/fixtures/aggregated-metric.fixture';
import { PageSortBy } from './dto/engagement-query.dto';

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

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

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

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

  describe('getOverview', () => {
    it('returns engagement metrics for date range', async () => {
      const overviewData = createEngagementOverviewResult();
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      const result = await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result.engagementRate).toBe(0.7);
      expect(result.avgSessionDuration).toBe(185.5);
      expect(result.pagesPerSession).toBe(3.2);
      expect(result.bounceRate).toBe(0.2);
      expect(result.eventsPerSession).toBe(8.5);
      expect(result.totalPageViews).toBe(3840);
      expect(result.uniquePageViews).toBe(2800);
      expect(result.totalEvents).toBe(10200);
    });

    it('handles empty results with zero defaults', async () => {
      mockDataSource.query
        .mockResolvedValueOnce([{}])
        .mockResolvedValueOnce([{}]);

      const result = await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result.engagementRate).toBe(0);
      expect(result.avgSessionDuration).toBe(0);
      expect(result.pagesPerSession).toBe(0);
      expect(result.bounceRate).toBe(0);
      expect(result.eventsPerSession).toBe(0);
      expect(result.totalPageViews).toBe(0);
      expect(result.uniquePageViews).toBe(0);
      expect(result.totalEvents).toBe(0);
    });

    it('calculates engagementRate correctly', async () => {
      const overviewData = [{
        total_sessions: '1000',
        engaged_sessions: '750',
        avg_duration: '200',
        avg_page_views: '3.5',
        avg_events: '9',
        bounced_sessions: '150',
        total_page_views: '3500',
        total_events: '9000',
      }];
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      const result = await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result.engagementRate).toBe(0.75);
    });

    it('calculates bounceRate correctly', async () => {
      const overviewData = [{
        total_sessions: '1000',
        engaged_sessions: '800',
        avg_duration: '200',
        avg_page_views: '3.5',
        avg_events: '9',
        bounced_sessions: '200',
        total_page_views: '3500',
        total_events: '9000',
      }];
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      const result = await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result.bounceRate).toBe(0.2);
    });

    it('adds trafficSource filter to params when provided', async () => {
      const overviewData = createEngagementOverviewResult();
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        trafficSource: 'organic',
      });

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf."trafficSource" = $3'),
        ['2026-01-15', '2026-01-20', 'organic']
      );
      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        2,
        expect.stringContaining('sf."trafficSource" = $3'),
        ['2026-01-15', '2026-01-20', 'organic']
      );
    });

    it('adds deviceType filter to params when provided', async () => {
      const overviewData = createEngagementOverviewResult();
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        deviceType: 'desktop',
      });

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf."deviceType" = $3'),
        ['2026-01-15', '2026-01-20', 'desktop']
      );
    });

    it('adds country filter to params when provided', async () => {
      const overviewData = createEngagementOverviewResult();
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        country: 'Iceland',
      });

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

    it('adds multiple filters when all provided', async () => {
      const overviewData = createEngagementOverviewResult();
      const uniqueViewsData = createUniqueViewsResult();

      mockDataSource.query
        .mockResolvedValueOnce(overviewData)
        .mockResolvedValueOnce(uniqueViewsData);

      await service.getOverview({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        trafficSource: 'organic',
        deviceType: 'desktop',
        country: 'Iceland',
      });

      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf."trafficSource" = $3'),
        ['2026-01-15', '2026-01-20', 'organic', 'desktop', 'Iceland']
      );
      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf."deviceType" = $4'),
        ['2026-01-15', '2026-01-20', 'organic', 'desktop', 'Iceland']
      );
      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.stringContaining('sf.country = $5'),
        ['2026-01-15', '2026-01-20', 'organic', 'desktop', 'Iceland']
      );
    });
  });

  describe('getPages', () => {
    it('returns page performance metrics', async () => {
      const pageData = createPageResults();
      mockDataSource.query.mockResolvedValueOnce(pageData);

      const result = await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toHaveLength(2);
      expect(result[0].path).toBe('/');
      expect(result[0].views).toBe(1200);
      expect(result[0].uniqueViews).toBe(800);
      expect(result[0].avgTimeOnPage).toBe(45.2);
      expect(result[0].entrances).toBe(900);
      expect(result[0].exits).toBe(300);
      expect(result[0].bounceRate).toBe(0.25);
      expect(result[0].exitRate).toBe(0.25);

      expect(result[1].path).toBe('/profiles');
      expect(result[1].views).toBe(800);
      expect(result[1].exitRate).toBe(0.50);
    });

    it('handles empty results', async () => {
      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toEqual([]);
    });

    it('adds pathPattern filter when provided', async () => {
      const pageData = createPageResults();
      mockDataSource.query.mockResolvedValueOnce(pageData);

      await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        pathPattern: '/profiles',
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('pv."pageUrl" LIKE $3'),
        ['2026-01-15', '2026-01-20', '%/profiles%', 20]
      );
    });

    it('adds trafficSource filter when provided', async () => {
      const pageData = createPageResults();
      mockDataSource.query.mockResolvedValueOnce(pageData);

      await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        trafficSource: 'organic',
      });

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

    it('uses custom sort column when provided', async () => {
      const pageData = createPageResults();
      mockDataSource.query.mockResolvedValueOnce(pageData);

      await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        sortBy: PageSortBy.UNIQUE_VIEWS,
        limit: 10,
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('ORDER BY unique_views DESC'),
        ['2026-01-15', '2026-01-20', 10]
      );
    });

    it('uses default limit when not provided', async () => {
      const pageData = createPageResults();
      mockDataSource.query.mockResolvedValueOnce(pageData);

      await service.getPages({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.any(String),
        ['2026-01-15', '2026-01-20', 20]
      );
    });
  });

  describe('getEvents', () => {
    it('returns event breakdown metrics', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      const result = await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toHaveLength(2);
      expect(result[0].eventType).toBe('click_cta');
      expect(result[0].category).toBe('click');
      expect(result[0].count).toBe(450);
      expect(result[0].uniqueUsers).toBe(300);
      expect(result[0].avgValue).toBe(0);

      expect(result[1].eventType).toBe('form_submit');
      expect(result[1].category).toBe('form');
      expect(result[1].count).toBe(120);
    });

    it('handles empty results', async () => {
      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toEqual([]);
    });

    it('adds category filter when provided', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        category: 'click' as any,
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('re."eventType" LIKE $3'),
        ['2026-01-15', '2026-01-20', 'click%', 20]
      );
    });

    it('adds eventType filter when provided', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        eventType: 'click_cta',
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('re."eventType" = $3'),
        ['2026-01-15', '2026-01-20', 'click_cta', 20]
      );
    });

    it('adds trafficSource filter when provided', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        trafficSource: 'organic',
      });

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

    it('adds deviceType filter when provided', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        deviceType: 'mobile',
      });

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

    it('adds country filter when provided', async () => {
      const eventData = createEventResults();
      mockDataSource.query.mockResolvedValueOnce(eventData);

      await service.getEvents({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        country: 'Iceland',
      });

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

  describe('getScrollDepth', () => {
    it('returns scroll depth percentages', async () => {
      const scrollData = createScrollDepthResults();
      mockDataSource.query.mockResolvedValueOnce(scrollData);

      const result = await service.getScrollDepth({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toHaveLength(1);
      expect(result[0].page).toBe('/');
      expect(result[0].depth25).toBe(0.9);
      expect(result[0].depth50).toBe(0.7);
      expect(result[0].depth75).toBe(0.45);
      expect(result[0].depth100).toBe(0.25);
      expect(result[0].avgScrollDepth).toBe(55.5);
    });

    it('handles empty results', async () => {
      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getScrollDepth({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toEqual([]);
    });

    it('handles division by zero in depth calculations', async () => {
      const scrollData = [{
        page: '/test',
        depth_25: '0',
        depth_50: '0',
        depth_75: '0',
        depth_100: '0',
        avg_depth: '0',
        total: '0',
      }];
      mockDataSource.query.mockResolvedValueOnce(scrollData);

      const result = await service.getScrollDepth({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result[0].depth25).toBe(0);
      expect(result[0].depth50).toBe(0);
      expect(result[0].depth75).toBe(0);
      expect(result[0].depth100).toBe(0);
    });

    it('adds page filter when provided', async () => {
      const scrollData = createScrollDepthResults();
      mockDataSource.query.mockResolvedValueOnce(scrollData);

      await service.getScrollDepth({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        page: '/profiles',
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('"pageUrl" LIKE $3'),
        ['2026-01-15', '2026-01-20', '%/profiles%']
      );
    });
  });

  describe('getUserFlow', () => {
    it('returns user flow grouped by start page', async () => {
      const flowData = createUserFlowResults();
      mockDataSource.query.mockResolvedValueOnce(flowData);

      const result = await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toHaveLength(1);
      expect(result[0].startPage).toBe('/');
      expect(result[0].totalSessions).toBe(800);
      expect(result[0].steps).toHaveLength(3);

      expect(result[0].steps[0].page).toBe('/');
      expect(result[0].steps[0].sessions).toBe(800);
      expect(result[0].steps[0].dropOff).toBe(0);
      expect(result[0].steps[0].dropOffRate).toBe(0);

      expect(result[0].steps[1].page).toBe('/profiles');
      expect(result[0].steps[1].sessions).toBe(500);
      expect(result[0].steps[1].dropOff).toBe(300);
      expect(result[0].steps[1].dropOffRate).toBe(0.375);

      expect(result[0].steps[2].page).toBe('/profile/1');
      expect(result[0].steps[2].sessions).toBe(300);
      expect(result[0].steps[2].dropOff).toBe(200);
      expect(result[0].steps[2].dropOffRate).toBe(0.4);
    });

    it('handles empty results returning empty array', async () => {
      mockDataSource.query.mockResolvedValueOnce([]);

      const result = await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toEqual([]);
    });

    it('adds startPage filter to SQL WHERE clause when provided', async () => {
      const flowData = createUserFlowResults();
      mockDataSource.query.mockResolvedValueOnce(flowData);

      await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        startPage: '/profiles',
      });

      const sql = mockDataSource.query.mock.calls[0][0] as string;
      const params = mockDataSource.query.mock.calls[0][1] as unknown[];

      // startPage filter must appear in the SQL WHERE clause (regression: was built but never injected)
      expect(sql).toContain('"pageUrl" LIKE $3');
      // steps at $4, limit at $5 when startPage occupies $3
      expect(sql).toContain('step_num <= $4');
      expect(sql).toContain('LIMIT $5');
      expect(params).toEqual(['2026-01-15', '2026-01-20', '%/profiles%', 5, 10]);
    });

    it('uses default steps and limit with correct param indices', async () => {
      const flowData = createUserFlowResults();
      mockDataSource.query.mockResolvedValueOnce(flowData);

      await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      const sql = mockDataSource.query.mock.calls[0][0] as string;
      const params = mockDataSource.query.mock.calls[0][1] as unknown[];

      // Without startPage: steps=$3, limit=$4
      expect(sql).toContain('step_num <= $3');
      expect(sql).toContain('LIMIT $4');
      expect(params).toEqual(['2026-01-15', '2026-01-20', 5, 10]);
    });

    it('uses custom steps and limit when provided', async () => {
      const flowData = createUserFlowResults();
      mockDataSource.query.mockResolvedValueOnce(flowData);

      await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        steps: 3,
        limit: 5,
      });

      const sql = mockDataSource.query.mock.calls[0][0] as string;
      const params = mockDataSource.query.mock.calls[0][1] as unknown[];

      expect(sql).toContain('step_num <= $3');
      expect(sql).toContain('LIMIT $4');
      expect(params).toEqual(['2026-01-15', '2026-01-20', 3, 5]);
    });

    it('injects all base conditions into session_pages WHERE clause', async () => {
      const flowData = createUserFlowResults();
      mockDataSource.query.mockResolvedValueOnce(flowData);

      await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      const sql = mockDataSource.query.mock.calls[0][0] as string;

      // All base conditions must appear in the WHERE clause (regression: was hardcoded separately)
      expect(sql).toContain('timestamp BETWEEN $1 AND $2');
      expect(sql).toContain("\"eventType\" IN ('pageView', 'pageview')");
      expect(sql).toContain('"pageUrl" IS NOT NULL');
    });

    it('handles multiple start pages in results', async () => {
      const flowData = [
        { start_page: '/', total_sessions: '800', step_num: 1, step_page: '/', step_sessions: '800' },
        { start_page: '/', total_sessions: '800', step_num: 2, step_page: '/profiles', step_sessions: '500' },
        { start_page: '/profiles', total_sessions: '400', step_num: 1, step_page: '/profiles', step_sessions: '400' },
        { start_page: '/profiles', total_sessions: '400', step_num: 2, step_page: '/profile/1', step_sessions: '250' },
      ];
      mockDataSource.query.mockResolvedValueOnce(flowData);

      const result = await service.getUserFlow({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
      });

      expect(result).toHaveLength(2);
      expect(result[0].startPage).toBe('/');
      expect(result[0].steps).toHaveLength(2);
      expect(result[1].startPage).toBe('/profiles');
      expect(result[1].steps).toHaveLength(2);
    });
  });
});
