// This helps generate pagination test cases

import { mergePaginate, defaultTimeOrderBy, DataSource } from '../../src/libs/pagination';

describe('mergePaginate', () => {
  // Create test data source
  const createTestDataSource = (items: any[]): DataSource<any> => ({
    count: () => Promise.resolve(items.length),
    fetch: (limit: number, offset: number = 0) => Promise.resolve(items.slice(offset, offset + limit)),
    meta: {
      type: 'database',
      cacheable: false,
    },
  });

  // Create test data source for sorting tests (cached type to enable in-memory sorting)
  const createSortingTestDataSource = (items: any[]): DataSource<any> => ({
    count: () => Promise.resolve(items.length),
    fetch: (limit: number, offset: number = 0) => Promise.resolve(items.slice(offset, offset + limit)),
    meta: {
      type: 'cached',
      cacheable: true,
    },
  });

  // Create pre-sorted database data source (simulates database with ORDER BY)
  const createSortedDatabaseDataSource = (items: any[], orderBy: (a: any, b: any) => number): DataSource<any> => {
    const sortedItems = [...items].sort(orderBy);
    return {
      count: () => Promise.resolve(sortedItems.length),
      fetch: (limit: number, offset: number = 0) => Promise.resolve(sortedItems.slice(offset, offset + limit)),
      meta: {
        type: 'database',
        cacheable: false,
      },
    };
  };

  // Create test items
  const createTestItems = (count: number, baseDate: Date = new Date()) => {
    return Array.from({ length: count }, (_, i) => ({
      id: `id_${i}`,
      created_at: new Date(baseDate.getTime() + i * 1000).toISOString(),
      value: i,
    }));
  };

  describe('Single Data Source', () => {
    it('should handle empty data source', async () => {
      const source = createTestDataSource([]);
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy());

      expect(result).toEqual({
        total: 0,
        data: [],
        paging: {
          page: 1,
          pageSize: 10,
          totalPages: 0,
        },
      });
    });

    it('should handle single data source with pagination', async () => {
      const items = createTestItems(25);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 2, pageSize: 10 }, defaultTimeOrderBy());

      expect(result.total).toBe(25);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 2,
        pageSize: 10,
        totalPages: 3,
      });
      expect(result.data[0].value).toBe(10); // First item of second page
    });

    it('should handle single data source with empty page', async () => {
      const items = createTestItems(5);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 2, pageSize: 10 }, defaultTimeOrderBy());

      expect(result.total).toBe(5);
      expect(result.data).toHaveLength(0);
      expect(result.paging).toEqual({
        page: 2,
        pageSize: 10,
        totalPages: 1,
      });
    });

    it('should handle page size larger than total items', async () => {
      const items = createTestItems(5);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 1, pageSize: 20 }, defaultTimeOrderBy());

      expect(result.total).toBe(5);
      expect(result.data).toHaveLength(5);
      expect(result.paging).toEqual({
        page: 1,
        pageSize: 20,
        totalPages: 1,
      });
    });

    it('should handle page number out of range', async () => {
      const items = createTestItems(15);
      const source = createTestDataSource(items);

      // Page number out of range
      const result1 = await mergePaginate([source], { page: 0, pageSize: 10 }, defaultTimeOrderBy());
      expect(result1.paging.page).toBe(1); // Should be corrected to first page
      expect(result1.data).toHaveLength(10);

      // Page number too large
      const result2 = await mergePaginate([source], { page: 100, pageSize: 10 }, defaultTimeOrderBy());
      expect(result2.paging.page).toBe(100);
      expect(result2.data).toHaveLength(0);
    });

    it('should handle page size of 0', async () => {
      const items = createTestItems(10);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 1, pageSize: 0 }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 1,
        pageSize: 0,
        totalPages: 1,
      });
    });

    it('should return all data when pageSize is 0', async () => {
      const items = createTestItems(10);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 1, pageSize: 0 }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 1,
        pageSize: 0,
        totalPages: 1,
      });
    });

    it('should return all data when pageSize is undefined', async () => {
      const items = createTestItems(10);
      const source = createTestDataSource(items);

      const result = await mergePaginate([source], { page: 1, pageSize: undefined as any }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 1,
        pageSize: 0,
        totalPages: 1,
      });
    });

    it('should return all data from multiple sources when pageSize is 0', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const source1 = createTestDataSource(createTestItems(5, baseDate));
      const source2 = createTestDataSource(createTestItems(5, new Date(baseDate.getTime() + 5000)));

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 0 }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 1,
        pageSize: 0,
        totalPages: 1,
      });
    });
  });

  describe('Multiple Data Sources', () => {
    it('should merge multiple data sources correctly', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items1 = [
        { id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
        { id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
      ];
      const items2 = [
        { id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
        { id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
      ];

      // Create pre-sorted database sources (simulating ORDER BY in database)
      const orderBy = defaultTimeOrderBy('desc');
      const source1 = createSortedDatabaseDataSource(items1, orderBy);
      const source2 = createSortedDatabaseDataSource(items2, orderBy);

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);

      expect(result.total).toBe(4);
      expect(result.data).toHaveLength(4);
      expect(result.data[0].value).toBe(4); // Latest item should be first
      expect(result.data[3].value).toBe(1); // Oldest item should be last
    });

    it('should handle pagination across multiple sources', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items1 = createTestItems(5, baseDate);
      const items2 = createTestItems(5, new Date(baseDate.getTime() + 5000));

      // For pagination test, we can use default database sources since we're testing pagination logic
      const source1 = createTestDataSource(items1);
      const source2 = createTestDataSource(items2);

      const result = await mergePaginate([source1, source2], { page: 2, pageSize: 3 }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(3);
      expect(result.paging).toEqual({
        page: 2,
        pageSize: 3,
        totalPages: 4,
      });
    });

    it('should handle sources with different types', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const source1 = createTestDataSource(createTestItems(5, baseDate));
      const source2: DataSource<any> = {
        count: () => Promise.resolve(3),
        fetch: (limit: number, offset: number = 0) =>
          Promise.resolve(createTestItems(3, new Date(baseDate.getTime() + 10000)).slice(offset, offset + limit)),
        meta: {
          type: 'computed',
          estimatedSize: 3,
          cacheable: true,
        },
      };

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());

      expect(result.total).toBe(8);
      expect(result.data).toHaveLength(8);
    });

    it('should handle error in data source gracefully', async () => {
      const source1 = createTestDataSource(createTestItems(5));
      const source2: DataSource<any> = {
        count: () => Promise.reject(new Error('Count error')),
        fetch: () => Promise.reject(new Error('Fetch error')),
        meta: {
          type: 'database',
          cacheable: false,
        },
      };

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());

      expect(result.total).toBe(5);
      expect(result.data).toHaveLength(5);
    });

    it('should handle empty data sources in the middle', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const source1 = createTestDataSource(createTestItems(5, baseDate));
      const source2 = createTestDataSource([]);
      const source3 = createTestDataSource(createTestItems(5, new Date(baseDate.getTime() + 5000)));

      const result = await mergePaginate([source1, source2, source3], { page: 1, pageSize: 10 }, defaultTimeOrderBy());

      expect(result.total).toBe(10);
      expect(result.data).toHaveLength(10);
    });

    it('should handle multiple data sources with overlapping timestamps', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items1 = [
        { id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
        { id: 'id_2', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 2 },
      ];
      const items2 = [
        { id: 'id_3', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 3 },
        { id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
      ];

      // Create pre-sorted database sources
      const orderBy = defaultTimeOrderBy('desc');
      const source1 = createSortedDatabaseDataSource(items1, orderBy);
      const source2 = createSortedDatabaseDataSource(items2, orderBy);

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);

      // Verify sorting order: First by timestamp descending, then by ID descending
      expect(result.data.map((item) => item.id)).toEqual(['id_4', 'id_2', 'id_3', 'id_1']);
    });

    it('should handle 29 records with last page showing correct 9 items', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');

      // Create 3 data sources with uneven distribution: 10, 10, 9 records
      const source1 = createTestDataSource(createTestItems(10, baseDate));
      const source2 = createTestDataSource(createTestItems(10, new Date(baseDate.getTime() + 10000)));
      const source3 = createTestDataSource(createTestItems(9, new Date(baseDate.getTime() + 20000)));

      // Test the last page (page 3, should have 9 items: records 21-29)
      const result = await mergePaginate(
        [source1, source2, source3],
        { page: 3, pageSize: 10 },
        defaultTimeOrderBy('desc')
      );

      expect(result.total).toBe(29);
      expect(result.data).toHaveLength(9); // Should show all 9 remaining items, not just 2
      expect(result.paging).toEqual({
        page: 3,
        pageSize: 10,
        totalPages: 3,
      });

      // Verify we got the correct range of items (the oldest ones)
      expect(result.data.every((item) => item.value < 20)).toBe(true); // These should be the oldest items
    });

    it('should handle multiple data sources with late pages correctly', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');

      // Create 3 data sources, each with 20 items (total 60 items)
      const source1 = createTestDataSource(createTestItems(20, baseDate));
      const source2 = createTestDataSource(createTestItems(20, new Date(baseDate.getTime() + 20000)));
      const source3 = createTestDataSource(createTestItems(20, new Date(baseDate.getTime() + 40000)));

      // Test page 4 (should have items 31-40 out of 60 total)
      const result = await mergePaginate(
        [source1, source2, source3],
        { page: 4, pageSize: 10 },
        defaultTimeOrderBy('desc')
      );

      expect(result.total).toBe(60);
      expect(result.data).toHaveLength(10);
      expect(result.paging).toEqual({
        page: 4,
        pageSize: 10,
        totalPages: 6,
      });

      // Test the last page (page 6, should have items 51-60)
      const lastPageResult = await mergePaginate(
        [source1, source2, source3],
        { page: 6, pageSize: 10 },
        defaultTimeOrderBy('desc')
      );

      expect(lastPageResult.total).toBe(60);
      expect(lastPageResult.data).toHaveLength(10);
      expect(lastPageResult.paging).toEqual({
        page: 6,
        pageSize: 10,
        totalPages: 6,
      });
    });
  });

  describe('Sorting', () => {
    it('should sort items in ascending order', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items1 = [
        { id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
        { id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
      ];
      const items2 = [
        { id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
        { id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
      ];

      // Create pre-sorted database sources
      const orderBy = defaultTimeOrderBy('asc');
      const source1 = createSortedDatabaseDataSource(items1, orderBy);
      const source2 = createSortedDatabaseDataSource(items2, orderBy);

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);

      expect(result.data[0].value).toBe(1); // Oldest item should be first
      expect(result.data[3].value).toBe(4); // Latest item should be last
    });

    it('should sort items in descending order', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items1 = [
        { id: 'id_1', created_at: new Date(baseDate.getTime() + 1000).toISOString(), value: 1 },
        { id: 'id_2', created_at: new Date(baseDate.getTime() + 2000).toISOString(), value: 2 },
      ];
      const items2 = [
        { id: 'id_3', created_at: new Date(baseDate.getTime() + 3000).toISOString(), value: 3 },
        { id: 'id_4', created_at: new Date(baseDate.getTime() + 4000).toISOString(), value: 4 },
      ];

      // Create pre-sorted database sources
      const orderBy = defaultTimeOrderBy('desc');
      const source1 = createSortedDatabaseDataSource(items1, orderBy);
      const source2 = createSortedDatabaseDataSource(items2, orderBy);

      const result = await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, orderBy);

      expect(result.data[0].value).toBe(4); // Latest item should be first
      expect(result.data[3].value).toBe(1); // Oldest item should be last
    });

    it('should use id as secondary sort key', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 1 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 2 },
      ];
      const source = createSortingTestDataSource(items);

      // Sort by timestamp ascending, then by id ascending
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('asc', 'id'));

      // Verify secondary sort key: When timestamps are equal, sort by id in ascending order
      expect(result.data[0].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb'); // L comes before r
      expect(result.data[1].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
    });

    it('should use value as secondary sort key', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
      ];
      const source = createSortingTestDataSource(items);

      // Sort by timestamp ascending, then by value ascending
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('asc', 'value'));

      // Verify secondary sort key: When timestamps are equal, sort by value in ascending order
      expect(result.data[0].value).toBe(1);
      expect(result.data[1].value).toBe(2);
    });

    it('should respect primary sort order for secondary field', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 1 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 2 },
      ];
      const source = createSortingTestDataSource(items);

      // Sort by timestamp descending, then by value descending
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));

      // Verify secondary sort key: When timestamps are equal, sort by value in descending order
      expect(result.data[0].value).toBe(2);
      expect(result.data[1].value).toBe(1);
    });

    it('should handle items with same timestamp without secondary field', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
      ];
      const source = createSortingTestDataSource(items);

      // Sort by timestamp only, without specifying secondary sort field (defaults to 'desc' order with 'id' secondary)
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc'));

      // Verify: When timestamps are equal, sort by id in descending order (because primary sort is 'desc')
      expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
      expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
    });

    it('should handle items with undefined secondary field values', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString() }, // value undefined
      ];
      const source = createSortingTestDataSource(items);

      // Specify value as secondary sort field, but one item has no value (should fall back to id)
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));

      // Verify: When secondary field value is undefined, use id as fallback sort in descending order
      expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
      expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
    });

    it('should use id as default secondary sort key', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: 1 },
      ];
      const source = createSortingTestDataSource(items);

      // No secondary sort field specified, should use id by default with descending order
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc'));

      // Verify: When timestamps are equal, sort by id in descending order
      expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5'); // r comes after L in descending order
      expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
    });

    it('should use id as fallback when secondary field is undefined', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: 2 },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString() }, // value undefined
      ];
      const source = createSortingTestDataSource(items);

      // Specify value as secondary sort field, but one item has no value
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));

      // Verify: When secondary field value is undefined, use id as fallback sort
      expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
      expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
    });

    it('should use id as fallback for unsupported field types', async () => {
      const baseDate = new Date('2024-01-01T00:00:00Z');
      const items = [
        { id: 'in_rBgXh8srXYXAXPs1Sn4XsJy5', created_at: baseDate.toISOString(), value: { complex: 'object' } },
        { id: 'in_LA0hts9lHaDycDii3TEfCzkb', created_at: baseDate.toISOString(), value: { complex: 'object' } },
      ];
      const source = createSortingTestDataSource(items);

      // Specify value as secondary sort field, but value is complex object
      const result = await mergePaginate([source], { page: 1, pageSize: 10 }, defaultTimeOrderBy('desc', 'value'));

      // Verify: When secondary field type is unsupported, use id as fallback sort
      expect(result.data[0].id).toBe('in_rBgXh8srXYXAXPs1Sn4XsJy5');
      expect(result.data[1].id).toBe('in_LA0hts9lHaDycDii3TEfCzkb');
    });
  });

  describe('Performance', () => {
    it('should handle large data sources efficiently', async () => {
      const largeItems = createTestItems(1000);
      const source = createTestDataSource(largeItems);

      const startTime = Date.now();
      const result = await mergePaginate([source], { page: 10, pageSize: 10 }, defaultTimeOrderBy());
      const duration = Date.now() - startTime;

      expect(result.total).toBe(1000);
      expect(result.data).toHaveLength(10);
      expect(duration).toBeLessThan(100); // Ensure processing time is within reasonable range
    });

    it('should not fetch all data when not necessary', async () => {
      const source1 = createTestDataSource(createTestItems(100));
      const source2 = createTestDataSource(createTestItems(100));

      // Mock fetch method calls
      const spy1 = jest.spyOn(source1, 'fetch');
      const spy2 = jest.spyOn(source2, 'fetch');

      await mergePaginate([source1, source2], { page: 1, pageSize: 10 }, defaultTimeOrderBy());

      // Ensure no data is fetched all at once
      expect(spy1).toHaveBeenCalledWith(expect.any(Number), expect.any(Number));
      expect(spy2).toHaveBeenCalledWith(expect.any(Number), expect.any(Number));

      spy1.mockRestore();
      spy2.mockRestore();
    });
  });
});
