import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { NotFoundException, ConflictException } from '@nestjs/common';
import { Segment } from './segment.entity';
import { SegmentsService } from './segments.service';
import { SegmentOperator, ConditionOperator } from './dto/segment.dto';
import type { CreateSegmentDto, UpdateSegmentDto } from './dto/segment.dto';
import { createMockRepository, createMockDataSource, MockRepository, MockDataSource } from '@test/mocks';
import { createSegmentMetricsResult } from '@test/fixtures';

describe('SegmentsService', () => {
  let service: SegmentsService;
  let mockRepository: MockRepository<Segment>;
  let mockDataSource: MockDataSource;

  beforeEach(async () => {
    mockRepository = createMockRepository<Segment>();
    mockDataSource = createMockDataSource();

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

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

  describe('onModuleInit', () => {
    it('creates built-in segments that dont exist', async () => {
      mockRepository.findOne.mockResolvedValue(null);
      mockRepository.create.mockImplementation((dto) => dto as Segment);
      mockRepository.save.mockImplementation((entity) => Promise.resolve(entity as Segment));

      await service.onModuleInit();

      expect(mockRepository.findOne).toHaveBeenCalledTimes(11);
      expect(mockRepository.create).toHaveBeenCalledTimes(11);
      expect(mockRepository.save).toHaveBeenCalledTimes(11);

      expect(mockRepository.findOne).toHaveBeenCalledWith({
        where: { name: 'Mobile Users', isBuiltIn: true },
      });

      expect(mockRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 'Mobile Users',
          isBuiltIn: true,
        }),
      );
    });

    it('skips existing built-in segments', async () => {
      const existingSegment: Segment = {
        id: 'seg-001',
        name: 'Mobile Users',
        description: 'Users accessing from mobile devices',
        conditions: [{ dimension: 'device_type', operator: ConditionOperator.EQUALS, values: ['mobile'] }],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(existingSegment);

      await service.onModuleInit();

      expect(mockRepository.findOne).toHaveBeenCalledTimes(11);
      expect(mockRepository.create).not.toHaveBeenCalled();
      expect(mockRepository.save).not.toHaveBeenCalled();
    });
  });

  describe('findAll', () => {
    it('returns all segments ordered by isBuiltIn DESC, name ASC', async () => {
      const segments: Segment[] = [
        {
          id: 'seg-001',
          name: 'Desktop Users',
          description: 'Users on desktop',
          conditions: [],
          operator: SegmentOperator.AND,
          isBuiltIn: true,
          createdBy: null,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
        {
          id: 'seg-002',
          name: 'Custom Segment',
          description: 'Custom',
          conditions: [],
          operator: SegmentOperator.AND,
          isBuiltIn: false,
          createdBy: 'user-001',
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      mockRepository.find.mockResolvedValue(segments);

      const result = await service.findAll();

      expect(result).toEqual(segments);
      expect(mockRepository.find).toHaveBeenCalledWith({
        order: { isBuiltIn: 'DESC', name: 'ASC' },
      });
    });
  });

  describe('findById', () => {
    it('returns segment by id', async () => {
      const segment: Segment = {
        id: 'seg-001',
        name: 'Mobile Users',
        description: 'Users on mobile',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(segment);

      const result = await service.findById('seg-001');

      expect(result).toEqual(segment);
      expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 'seg-001' } });
    });

    it('throws NotFoundException for missing segment', async () => {
      mockRepository.findOne.mockResolvedValue(null);

      await expect(service.findById('seg-999')).rejects.toThrow(NotFoundException);
      await expect(service.findById('seg-999')).rejects.toThrow('Segment seg-999 not found');
    });
  });

  describe('create', () => {
    it('creates new segment', async () => {
      const dto: CreateSegmentDto = {
        name: 'Premium Users',
        description: 'Users with premium subscriptions',
        conditions: [
          { dimension: 'subscription_tier', operator: ConditionOperator.EQUALS, values: ['premium'] },
        ],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
      };

      const createdSegment: Segment = {
        id: 'seg-new',
        name: dto.name,
        description: dto.description,
        conditions: dto.conditions,
        operator: dto.operator,
        isBuiltIn: false,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(null);
      mockRepository.create.mockReturnValue(createdSegment);
      mockRepository.save.mockResolvedValue(createdSegment);

      const result = await service.create(dto);

      expect(result).toEqual(createdSegment);
      expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { name: dto.name } });
      expect(mockRepository.create).toHaveBeenCalledWith(dto);
      expect(mockRepository.save).toHaveBeenCalledWith(createdSegment);
    });

    it('throws ConflictException for duplicate name', async () => {
      const dto: CreateSegmentDto = {
        name: 'Existing Segment',
        description: 'Already exists',
        conditions: [],
        operator: SegmentOperator.AND,
      };

      const existingSegment: Segment = {
        id: 'seg-existing',
        name: dto.name,
        description: 'Existing',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(existingSegment);

      await expect(service.create(dto)).rejects.toThrow(ConflictException);
      await expect(service.create(dto)).rejects.toThrow('Segment with name "Existing Segment" already exists');
    });
  });

  describe('update', () => {
    it('updates segment properties', async () => {
      const existingSegment: Segment = {
        id: 'seg-001',
        name: 'Old Name',
        description: 'Old description',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: 'user-001',
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const updateDto: UpdateSegmentDto = {
        description: 'Updated description',
        conditions: [
          { dimension: 'country', operator: ConditionOperator.EQUALS, values: ['IS'] },
        ],
      };

      mockRepository.findOne.mockResolvedValue(existingSegment);
      mockRepository.save.mockImplementation((segment) => Promise.resolve(segment as Segment));

      const result = await service.update('seg-001', updateDto);

      expect(result.description).toBe(updateDto.description);
      expect(result.conditions).toEqual(updateDto.conditions);
      expect(mockRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          id: 'seg-001',
          description: 'Updated description',
        }),
      );
    });

    it('throws ConflictException when modifying built-in segment', async () => {
      const builtInSegment: Segment = {
        id: 'seg-builtin',
        name: 'Mobile Users',
        description: 'Built-in',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(builtInSegment);

      const updateDto: UpdateSegmentDto = { description: 'New description' };

      await expect(service.update('seg-builtin', updateDto)).rejects.toThrow(ConflictException);
      await expect(service.update('seg-builtin', updateDto)).rejects.toThrow('Cannot modify built-in segments');
    });

    it('throws ConflictException for duplicate name on rename', async () => {
      const existingSegment: Segment = {
        id: 'seg-001',
        name: 'Original Name',
        description: 'Desc',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const conflictingSegment: Segment = {
        id: 'seg-002',
        name: 'Conflicting Name',
        description: 'Other',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne
        .mockResolvedValueOnce(existingSegment)
        .mockResolvedValueOnce(conflictingSegment);

      const updateDto: UpdateSegmentDto = { name: 'Conflicting Name' };

      await expect(service.update('seg-001', updateDto)).rejects.toThrow(ConflictException);
      expect(mockRepository.findOne).toHaveBeenCalledWith({
        where: { name: 'Conflicting Name' },
      });
    });
  });

  describe('delete', () => {
    it('deletes non-built-in segment', async () => {
      const segment: Segment = {
        id: 'seg-001',
        name: 'Custom Segment',
        description: 'Custom',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: 'user-001',
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(segment);
      mockRepository.remove.mockResolvedValue(segment);

      await service.delete('seg-001');

      expect(mockRepository.remove).toHaveBeenCalledWith(segment);
    });

    it('throws ConflictException when deleting built-in segment', async () => {
      const builtInSegment: Segment = {
        id: 'seg-builtin',
        name: 'Mobile Users',
        description: 'Built-in',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(builtInSegment);

      await expect(service.delete('seg-builtin')).rejects.toThrow(ConflictException);
      await expect(service.delete('seg-builtin')).rejects.toThrow('Cannot delete built-in segments');
    });
  });

  describe('applySegment', () => {
    it('returns segment metrics for date range', async () => {
      const segment: Segment = {
        id: 'seg-001',
        name: 'Mobile Users',
        description: 'Mobile',
        conditions: [
          { dimension: 'device_type', operator: ConditionOperator.EQUALS, values: ['mobile'] },
        ],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(segment);
      mockDataSource.query.mockResolvedValue(createSegmentMetricsResult());

      const result = await service.applySegment('seg-001', {
        startDate: '2026-01-01',
        endDate: '2026-01-31',
      });

      expect(result).toEqual({
        segmentId: 'seg-001',
        segmentName: 'Mobile Users',
        sessions: 600,
        users: 400,
        engagementRate: 0.72,
        conversionRate: 0.06,
        avgSessionDuration: 195.0,
        pagesPerSession: 3.5,
        revenue: 1200.0,
      });

      expect(mockDataSource.query).toHaveBeenCalledWith(
        expect.stringContaining('WITH session_data AS'),
        ['2026-01-01', '2026-01-31'],
      );
    });

    it('handles empty results with zero defaults', async () => {
      const segment: Segment = {
        id: 'seg-002',
        name: 'Empty Segment',
        description: 'Empty',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: false,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne.mockResolvedValue(segment);
      mockDataSource.query.mockResolvedValue([]);

      const result = await service.applySegment('seg-002', {
        startDate: '2026-01-01',
        endDate: '2026-01-31',
      });

      expect(result).toEqual({
        segmentId: 'seg-002',
        segmentName: 'Empty Segment',
        sessions: 0,
        users: 0,
        engagementRate: 0,
        conversionRate: 0,
        avgSessionDuration: 0,
        pagesPerSession: 0,
        revenue: 0,
      });
    });
  });

  describe('compareSegments', () => {
    it('compares multiple segments and returns winner', async () => {
      const segment1: Segment = {
        id: 'seg-001',
        name: 'Desktop Users',
        description: 'Desktop',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const segment2: Segment = {
        id: 'seg-002',
        name: 'Mobile Users',
        description: 'Mobile',
        conditions: [],
        operator: SegmentOperator.AND,
        isBuiltIn: true,
        createdBy: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockRepository.findOne
        .mockResolvedValueOnce(segment1)
        .mockResolvedValueOnce(segment2);

      mockDataSource.query
        .mockResolvedValueOnce([
          {
            sessions: '800',
            users: '500',
            engagement_rate: '0.75',
            conversion_rate: '0.08',
            avg_duration: '210.0',
            avg_page_views: '4.0',
            total_revenue: '1500.00',
          },
        ])
        .mockResolvedValueOnce([
          {
            sessions: '600',
            users: '400',
            engagement_rate: '0.72',
            conversion_rate: '0.06',
            avg_duration: '195.0',
            avg_page_views: '3.5',
            total_revenue: '1200.00',
          },
        ]);

      const result = await service.compareSegments({
        segmentIds: ['seg-001', 'seg-002'],
        startDate: '2026-01-01',
        endDate: '2026-01-31',
        metric: 'sessions',
      });

      expect(result.segments).toHaveLength(2);
      expect(result.metric).toBe('sessions');
      expect(result.winner).toBe('Desktop Users');
      expect(result.segments[0]?.sessions).toBe(800);
      expect(result.segments[1]?.sessions).toBe(600);
    });
  });
});
