import { Test, TestingModule } from '@nestjs/testing';
import { FilterEngineService } from './filter-engine.service';
import { SelectQueryBuilder } from 'typeorm';

describe('FilterEngineService', () => { // describe: 테스트 그룹 (테스트할 클래스/모듈)  
  let service: FilterEngineService;
  let mockQueryBuilder: any;

  beforeEach(async () => {  // beforeEach: 각 테스트 실행 전에 실행
    const module: TestingModule = await Test.createTestingModule({
      providers: [FilterEngineService], // 테스트할 서비스 주입
    }).compile();

    service = module.get<FilterEngineService>(FilterEngineService);

    // Mock QueryBuilder
    mockQueryBuilder = {
      andWhere: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      addOrderBy: jest.fn().mockReturnThis(),
      skip: jest.fn().mockReturnThis(),
      take: jest.fn().mockReturnThis(),
      getCount: jest.fn().mockResolvedValue(100),
      getMany: jest.fn().mockResolvedValue([
        { id: 1, name: 'Test1' },
        { id: 2, name: 'Test2' },
      ]),
    };
  });

  it('should be defined', () => { // it: 개별 테스트 케이스
    expect(service).toBeDefined(); // expect: 검증
  });

  describe('applyFilters', () => {    // 특정 메서드에 대한 하위 그룹
    it('필터가 없으면 queryBuilder를 그대로 반환해야 함', () => {
      const result = service.applyFilters(mockQueryBuilder, []);
      expect(result).toBe(mockQueryBuilder);
      expect(mockQueryBuilder.andWhere).not.toHaveBeenCalled();
    });

    it('eq 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'status', operator: 'eq', value: 'active' },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'status = :param_0',
        { param_0: 'active' },
      );
    });

    it('ne 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'status', operator: 'ne', value: 'deleted' },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'status != :param_0',
        { param_0: 'deleted' },
      );
    });

    it('gt 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'gt', value: 18 },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'age > :param_0',
        { param_0: 18 },
      );
    });

    it('gte 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'gte', value: 18 },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'age >= :param_0',
        { param_0: 18 },
      );
    });

    it('lt 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'lt', value: 65 },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'age < :param_0',
        { param_0: 65 },
      );
    });

    it('lte 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'lte', value: 65 },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'age <= :param_0',
        { param_0: 65 },
      );
    });

    it('like 연산자를 올바르게 적용해야 함 (와일드카드 자동 추가)', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'name', operator: 'like', value: 'John' },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'name LIKE :param_0',
        { param_0: '%John%' },
      );
    });

    it('in 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'status', operator: 'in', value: ['active', 'pending'] },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'status IN (:...param_0)',
        { param_0: ['active', 'pending'] },
      );
    });

    it('between 연산자를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'between', value: [18, 65] },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith(
        'age BETWEEN :param_0_min AND :param_0_max',
        { param_0_min: 18, param_0_max: 65 },
      );
    });

    it('다중 필터를 올바르게 적용해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'status', operator: 'eq', value: 'active' },
        { field: 'age', operator: 'gte', value: 18 },
        { field: 'name', operator: 'like', value: 'John' },
      ]);

      expect(mockQueryBuilder.andWhere).toHaveBeenCalledTimes(3);
    });

    it('between 연산자에서 배열 길이가 2가 아니면 무시해야 함', () => {
      service.applyFilters(mockQueryBuilder, [
        { field: 'age', operator: 'between', value: [18] }, // 길이 1
      ]);

      // between은 호출되지 않아야 함
      expect(mockQueryBuilder.andWhere).not.toHaveBeenCalled();
    });
  });

  describe('applySortOptions', () => {
    it('정렬 옵션이 없으면 queryBuilder를 그대로 반환해야 함', () => {
      const result = service.applySortOptions(mockQueryBuilder, []);
      expect(result).toBe(mockQueryBuilder);
      expect(mockQueryBuilder.orderBy).not.toHaveBeenCalled();
    });

    it('단일 정렬 옵션을 올바르게 적용해야 함', () => {
      service.applySortOptions(mockQueryBuilder, [
        { field: 'createdAt', order: 'DESC' },
      ]);

      expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('createdAt', 'DESC');
    });

    it('다중 정렬 옵션을 올바르게 적용해야 함 (첫 번째는 orderBy, 나머지는 addOrderBy)', () => {
      service.applySortOptions(mockQueryBuilder, [
        { field: 'status', order: 'ASC' },
        { field: 'createdAt', order: 'DESC' },
        { field: 'name', order: 'ASC' },
      ]);

      expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('status', 'ASC');
      expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledWith('createdAt', 'DESC');
      expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledWith('name', 'ASC');
      expect(mockQueryBuilder.addOrderBy).toHaveBeenCalledTimes(2);
    });
  });

  describe('applyPagination', () => {
    it('페이지네이션을 올바르게 적용해야 함 (1페이지)', () => {
      service.applyPagination(mockQueryBuilder, 1, 10);

      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(0);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
    });

    it('페이지네이션을 올바르게 적용해야 함 (2페이지)', () => {
      service.applyPagination(mockQueryBuilder, 2, 10);

      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(10);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
    });

    it('페이지네이션을 올바르게 적용해야 함 (3페이지, limit 20)', () => {
      service.applyPagination(mockQueryBuilder, 3, 20);

      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(40);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(20);
    });
  });

  describe('executeFilteredQuery', () => {
    it('필터, 정렬, 페이지네이션을 모두 적용하고 결과를 반환해야 함', async () => {
      const options = {
        filters: [
          { field: 'status', operator: 'eq' as const, value: 'active' },
        ],
        sorts: [
          { field: 'createdAt', order: 'DESC' as const },
        ],
        pagination: {
          page: 1,
          limit: 10,
        },
      };

      const result = await service.executeFilteredQuery(mockQueryBuilder, options);

      // 필터 적용 확인
      expect(mockQueryBuilder.andWhere).toHaveBeenCalled();

      // 정렬 적용 확인
      expect(mockQueryBuilder.orderBy).toHaveBeenCalled();

      // 개수 조회 확인
      expect(mockQueryBuilder.getCount).toHaveBeenCalled();

      // 페이지네이션 적용 확인
      expect(mockQueryBuilder.skip).toHaveBeenCalled();
      expect(mockQueryBuilder.take).toHaveBeenCalled();

      // 데이터 조회 확인
      expect(mockQueryBuilder.getMany).toHaveBeenCalled();

      // 결과 구조 확인
      expect(result).toEqual({
        data: [
          { id: 1, name: 'Test1' },
          { id: 2, name: 'Test2' },
        ],
        total: 100,
        page: 1,
        limit: 10,
        totalPages: 10,
      });
    });

    it('페이지네이션 없이 실행할 수 있어야 함', async () => {
      const options = {
        filters: [
          { field: 'status', operator: 'eq' as const, value: 'active' },
        ],
      };

      const result = await service.executeFilteredQuery(mockQueryBuilder, options);

      // 페이지네이션은 적용되지 않아야 함
      expect(mockQueryBuilder.skip).not.toHaveBeenCalled();
      expect(mockQueryBuilder.take).not.toHaveBeenCalled();

      // 결과에 페이지 정보가 없어야 함
      expect(result).toEqual({
        data: [
          { id: 1, name: 'Test1' },
          { id: 2, name: 'Test2' },
        ],
        total: 100,
      });
    });

    it('필터와 정렬 없이 페이지네이션만 적용할 수 있어야 함', async () => {
      const options = {
        pagination: {
          page: 2,
          limit: 20,
        },
      };

      const result = await service.executeFilteredQuery(mockQueryBuilder, options);

      // 필터와 정렬은 적용되지 않아야 함
      expect(mockQueryBuilder.andWhere).not.toHaveBeenCalled();
      expect(mockQueryBuilder.orderBy).not.toHaveBeenCalled();

      // 페이지네이션은 적용되어야 함
      expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
      expect(mockQueryBuilder.take).toHaveBeenCalledWith(20);

      // totalPages 계산 확인
      expect(result.totalPages).toBe(5); // 100 / 20 = 5
    });

    it('totalPages를 올바르게 계산해야 함 (나누어떨어지지 않는 경우)', async () => {
      mockQueryBuilder.getCount.mockResolvedValue(105);

      const options = {
        pagination: {
          page: 1,
          limit: 10,
        },
      };

      const result = await service.executeFilteredQuery(mockQueryBuilder, options);

      expect(result.totalPages).toBe(11); // Math.ceil(105 / 10) = 11
    });
  });
});
