import { Test } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { AcquisitionService } from './acquisition.service';
import { createMockDataSource, type MockDataSource } from '@test/mocks';
import {
  createAcquisitionOverviewResult,
  createChannelResults,
  createSourceResults,
  createCampaignResults,
  createReferrerResults,
} from '@test/fixtures/aggregated-metric.fixture';

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

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

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

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

  describe('getOverview', () => {
    it('returns overview metrics for date range', async () => {
      const overviewData = createAcquisitionOverviewResult();
      const channelData = createChannelResults();

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

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

      expect(result.totalSessions).toBe(1500);
      expect(result.totalUsers).toBe(900);
      expect(result.newUsers).toBe(350);
      expect(result.returningUsers).toBe(550);
      expect(result.topChannels).toHaveLength(3);
      expect(result.topChannels[0].channel).toBe('organic');
      expect(result.topChannels[0].sessions).toBe(600);
      expect(result.topChannels[0].users).toBe(400);
    });

    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.totalSessions).toBe(0);
      expect(result.totalUsers).toBe(0);
      expect(result.newUsers).toBe(0);
      expect(result.returningUsers).toBe(0);
      expect(result.newUserRate).toBe(0);
      expect(result.topChannels).toEqual([]);
    });

    it('calculates newUserRate correctly', async () => {
      const overviewData = createAcquisitionOverviewResult({
        total_users: '1000',
        new_users: '400',
      });
      const channelData = createChannelResults();

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

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

      expect(result.newUserRate).toBe(0.4);
    });

    it('calculates channel engagement rate correctly', async () => {
      const overviewData = createAcquisitionOverviewResult();
      const channelData = [{
        channel: 'organic',
        sessions: '500',
        users: '300',
        new_users: '100',
        engaged_sessions: '350',
        conversions: '25',
        revenue: '1000.00',
      }];

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

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

      expect(result.topChannels[0].engagementRate).toBe(0.7);
      expect(result.topChannels[0].conversionRate).toBe(0.05);
    });

    it('passes correct SQL parameters', async () => {
      const overviewData = createAcquisitionOverviewResult();
      const channelData = createChannelResults();

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

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

      expect(mockDataSource.query).toHaveBeenCalledTimes(2);
      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        1,
        expect.any(String),
        ['2026-01-15', '2026-01-20']
      );
      expect(mockDataSource.query).toHaveBeenNthCalledWith(
        2,
        expect.any(String),
        ['2026-01-15', '2026-01-20', 10]
      );
    });

    it('uses default limit when not provided', async () => {
      const overviewData = createAcquisitionOverviewResult();
      const channelData = createChannelResults();

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

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

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

  describe('getChannels', () => {
    it('returns channel metrics from overview', async () => {
      const overviewData = createAcquisitionOverviewResult();
      const channelData = createChannelResults();

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

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

      expect(result).toHaveLength(3);
      expect(result[0].channel).toBe('organic');
      expect(result[0].sessions).toBe(600);
      expect(result[1].channel).toBe('direct');
      expect(result[2].channel).toBe('social');
    });
  });

  describe('getSources', () => {
    it('returns source/medium breakdown', async () => {
      const sourceData = createSourceResults();
      mockDataSource.query.mockResolvedValueOnce(sourceData);

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

      expect(result).toHaveLength(2);
      expect(result[0].source).toBe('google');
      expect(result[0].medium).toBe('organic');
      expect(result[0].sessions).toBe(450);
      expect(result[0].users).toBe(300);
      expect(result[0].engagementRate).toBe(0.7);
      expect(result[0].conversionRate).toBeCloseTo(0.0489, 4);
    });

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

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

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

    it('adds channel filter to params when provided', async () => {
      const sourceData = createSourceResults();
      mockDataSource.query.mockResolvedValueOnce(sourceData);

      await service.getSources({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        channel: 'organic',
        limit: 10,
      });

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

    it('builds params correctly without channel filter', async () => {
      const sourceData = createSourceResults();
      mockDataSource.query.mockResolvedValueOnce(sourceData);

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

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

  describe('getCampaigns', () => {
    it('returns campaign performance data', async () => {
      const campaignData = createCampaignResults();
      mockDataSource.query.mockResolvedValueOnce(campaignData);

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

      expect(result).toHaveLength(1);
      expect(result[0].campaign).toBe('summer-2026');
      expect(result[0].source).toBe('google');
      expect(result[0].medium).toBe('cpc');
      expect(result[0].sessions).toBe(200);
      expect(result[0].users).toBe(150);
      expect(result[0].engagementRate).toBe(0.7);
      expect(result[0].conversions).toBe(12);
      expect(result[0].revenue).toBe(600);
    });

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

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

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

    it('adds source filter to params when provided', async () => {
      const campaignData = createCampaignResults();
      mockDataSource.query.mockResolvedValueOnce(campaignData);

      await service.getCampaigns({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        source: 'google',
        limit: 15,
      });

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

    it('builds params correctly without source filter', async () => {
      const campaignData = createCampaignResults();
      mockDataSource.query.mockResolvedValueOnce(campaignData);

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

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

  describe('getReferrers', () => {
    it('returns top referrers', async () => {
      const referrerData = createReferrerResults();
      mockDataSource.query.mockResolvedValueOnce(referrerData);

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

      expect(result).toHaveLength(1);
      expect(result[0].referrer).toBe('https://twitter.com/some-post');
      expect(result[0].domain).toBe('twitter.com');
      expect(result[0].sessions).toBe(150);
      expect(result[0].users).toBe(120);
      expect(result[0].engagementRate).toBe(0.6);
      expect(result[0].conversions).toBe(5);
    });

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

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

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

    it('excludes internal domains when requested', async () => {
      const referrerData = createReferrerResults();
      mockDataSource.query.mockResolvedValueOnce(referrerData);

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

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining("sf.referrer NOT LIKE '%atlilith.%'"),
        ['2026-01-15', '2026-01-20', 20]
      );
      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining("sf.referrer NOT LIKE '%trustedmeet.%'"),
        ['2026-01-15', '2026-01-20', 20]
      );
    });

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

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

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

  describe('compare', () => {
    it('calculates percentage changes between periods', async () => {
      const currentOverview = createAcquisitionOverviewResult({
        total_sessions: '1500',
        total_users: '900',
        new_users: '350',
      });
      const currentChannels = createChannelResults();

      const previousOverview = createAcquisitionOverviewResult({
        total_sessions: '1000',
        total_users: '600',
        new_users: '200',
      });
      const previousChannels = createChannelResults();

      mockDataSource.query
        .mockResolvedValueOnce(currentOverview)
        .mockResolvedValueOnce(currentChannels)
        .mockResolvedValueOnce(previousOverview)
        .mockResolvedValueOnce(previousChannels);

      const result = await service.compare({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        compareStartDate: '2026-01-08',
        compareEndDate: '2026-01-13',
      });

      expect(result.current.totalSessions).toBe(1500);
      expect(result.previous.totalSessions).toBe(1000);
      expect(result.sessionsChange).toBe(50);
      expect(result.usersChange).toBe(50);
      expect(result.newUsersChange).toBe(75);
    });

    it('handles zero previous values when current is positive', async () => {
      const currentOverview = createAcquisitionOverviewResult({
        total_sessions: '1500',
        total_users: '900',
        new_users: '350',
      });
      const currentChannels = createChannelResults();

      const previousOverview = createAcquisitionOverviewResult({
        total_sessions: '0',
        total_users: '0',
        new_users: '0',
      });
      const previousChannels = createChannelResults();

      mockDataSource.query
        .mockResolvedValueOnce(currentOverview)
        .mockResolvedValueOnce(currentChannels)
        .mockResolvedValueOnce(previousOverview)
        .mockResolvedValueOnce(previousChannels);

      const result = await service.compare({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        compareStartDate: '2026-01-08',
        compareEndDate: '2026-01-13',
      });

      expect(result.sessionsChange).toBe(100);
      expect(result.usersChange).toBe(100);
      expect(result.newUsersChange).toBe(100);
    });

    it('handles zero values for both periods', async () => {
      const currentOverview = createAcquisitionOverviewResult({
        total_sessions: '0',
        total_users: '0',
        new_users: '0',
      });
      const currentChannels = [];

      const previousOverview = createAcquisitionOverviewResult({
        total_sessions: '0',
        total_users: '0',
        new_users: '0',
      });
      const previousChannels = [];

      mockDataSource.query
        .mockResolvedValueOnce(currentOverview)
        .mockResolvedValueOnce(currentChannels)
        .mockResolvedValueOnce(previousOverview)
        .mockResolvedValueOnce(previousChannels);

      const result = await service.compare({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        compareStartDate: '2026-01-08',
        compareEndDate: '2026-01-13',
      });

      expect(result.sessionsChange).toBe(0);
      expect(result.usersChange).toBe(0);
      expect(result.newUsersChange).toBe(0);
    });

    it('handles negative changes correctly', async () => {
      const currentOverview = createAcquisitionOverviewResult({
        total_sessions: '800',
        total_users: '500',
        new_users: '150',
      });
      const currentChannels = createChannelResults();

      const previousOverview = createAcquisitionOverviewResult({
        total_sessions: '1000',
        total_users: '600',
        new_users: '200',
      });
      const previousChannels = createChannelResults();

      mockDataSource.query
        .mockResolvedValueOnce(currentOverview)
        .mockResolvedValueOnce(currentChannels)
        .mockResolvedValueOnce(previousOverview)
        .mockResolvedValueOnce(previousChannels);

      const result = await service.compare({
        startDate: '2026-01-15',
        endDate: '2026-01-20',
        compareStartDate: '2026-01-08',
        compareEndDate: '2026-01-13',
      });

      expect(result.sessionsChange).toBe(-20);
      expect(result.usersChange).toBeCloseTo(-16.67, 2);
      expect(result.newUsersChange).toBe(-25);
    });
  });
});
