import { FibonacciHeap, Heap, MaxHeap, MinHeap } from '../../../../src';
import { logBigOMetricsWrap } from '../../../utils';

describe('Heap Operation Test', () => {
  it('should initiate heap', function () {
    const hp = new Heap<number>();
    hp.add(1);
    expect(hp.size).toBe(1);
  });

  it('should heap add and delete work well', function () {
    const hp = new MinHeap<number>();
    expect(hp.delete(1)).toBe(false);
    hp.add(2);
    hp.add(3);
    hp.add(1);
    hp.add(4);
    hp.add(6);
    hp.add(5);
    hp.add(7);

    hp.delete(4);
    hp.delete(7);
    hp.delete(1);

    expect(hp.size).toBe(4);
    expect(hp.peek()).toBe(2);
  });

  it('should numeric heap work well', function () {
    const minNumHeap = new MinHeap<number>();
    expect(minNumHeap.poll()).toBe(undefined);
    minNumHeap.add(1);
    minNumHeap.add(6);
    minNumHeap.add(2);
    minNumHeap.add(0);
    minNumHeap.add(5);
    minNumHeap.add(9);
    expect(minNumHeap.has(1)).toBe(true);
    expect(minNumHeap.has(2)).toBe(true);
    expect(minNumHeap.poll()).toBe(0);
    expect(minNumHeap.poll()).toBe(1);
    expect(minNumHeap.peek()).toBe(2);
    expect(!minNumHeap.has(1));
    expect(minNumHeap.has(2));
    const arrFromHeap = minNumHeap.toArray();
    expect(arrFromHeap.length).toBe(4);
    expect(arrFromHeap[0]).toBe(2);
    expect(arrFromHeap[1]).toBe(5);
    expect(arrFromHeap[2]).toBe(9);
    expect(arrFromHeap[3]).toBe(6);
    expect(minNumHeap.sort()).toEqual([2, 5, 6, 9]);
  });

  it('should clone', function () {
    const minNumHeap = new Heap<string>([], {
      comparator: (a, b) => Number(a) - Number(b)
    });
    minNumHeap.add('1');
    minNumHeap.add('6');
    minNumHeap.add('2');
    minNumHeap.add('0');
    minNumHeap.add('5');
    minNumHeap.add('9');
    minNumHeap.delete('2');
    expect([...minNumHeap]).toEqual(['0', '1', '9', '6', '5']);
    const cloned = minNumHeap.clone();
    expect([...cloned]).toEqual(['0', '1', '9', '6', '5']);
    minNumHeap.delete('5');
    expect([...minNumHeap]).toEqual(['0', '1', '9', '6']);
    expect([...cloned]).toEqual(['0', '1', '9', '6', '5']);
  });

  it('should object heap work well', function () {
    const minHeap = new MinHeap<{
      a: string;
      key: number;
    }>([], {
      comparator: (a, b) => a.key - b.key
    });
    minHeap.add({ key: 1, a: 'a1' });
    minHeap.add({ key: 6, a: 'a6' });
    minHeap.add({ key: 2, a: 'a2' });
    minHeap.add({ key: 0, a: 'a0' });

    expect(minHeap.peek()).toEqual({
      a: 'a0',
      key: 0
    });
    expect(minHeap.toArray().map(item => ({ a: item.a }))).toEqual([
      { a: 'a0' },
      { a: 'a1' },
      { a: 'a2' },
      { a: 'a6' }
    ]);
    let i = 0;
    const expectPolled = [{ a: 'a0' }, { a: 'a1' }, { a: 'a2' }, { a: 'a6' }];
    while (minHeap.size > 0) {
      expect({
        a: minHeap.poll()?.a
      }).toEqual(expectPolled[i]);
      i++;
    }

    const maxHeap = new MaxHeap<{
      key: number;
      a: string;
    }>([], {
      comparator: (a, b) => b.key - a.key
    });
    maxHeap.add({ key: 1, a: 'a1' });
    maxHeap.add({ key: 6, a: 'a6' });
    maxHeap.add({ key: 5, a: 'a5' });
    maxHeap.add({ key: 2, a: 'a2' });
    maxHeap.add({ key: 0, a: 'a0' });
    maxHeap.add({ key: 9, a: 'a9' });
    expect(maxHeap.peek()).toEqual({
      a: 'a9',
      key: 9
    });
    expect(maxHeap.toArray().map(item => ({ a: item.a }))).toEqual([
      { a: 'a9' },
      { a: 'a2' },
      { a: 'a6' },
      { a: 'a1' },
      { a: 'a0' },
      { a: 'a5' }
    ]);
    const maxExpectPolled = [{ a: 'a9' }, { a: 'a6' }, { a: 'a5' }, { a: 'a2' }, { a: 'a1' }, { a: 'a0' }];
    let maxI = 0;
    while (maxHeap.size > 0) {
      expect({
        a: maxHeap.poll()?.a
      }).toEqual(maxExpectPolled[maxI]);
      maxI++;
    }
  });

  it('should object heap map & filter', function () {
    const minHeap = new MinHeap<{
      a: string;
      key: number;
    }>(
      [
        { key: 1, a: 'a1' },
        { key: 6, a: 'a6' },
        { key: 5, a: 'a5' },
        { key: 3, a: 'a3' },
        { key: 2, a: 'a2' },
        { key: 4, a: 'a4' },
        { key: 0, a: 'a0' }
      ],
      {
        comparator: (a, b) => a.key - b.key
      }
    );

    const mappedMinHeap = minHeap.map(
      item => item.key,
      (a, b) => a - b
    );
    expect(mappedMinHeap.peek()).toBe(0);
    expect(mappedMinHeap.sort()).toEqual([0, 1, 2, 3, 4, 5, 6]);

    const mappedToElementFnMinHeap = minHeap.map<string, { id: string }>(
      item => item.key.toString(),
      (a, b) => Number(a) - Number(b),
      rawElement => rawElement.id
    );
    expect(mappedToElementFnMinHeap.peek()).toBe('0');
    expect(mappedToElementFnMinHeap.sort()).toEqual(['0', '1', '2', '3', '4', '5', '6']);

    const filteredHeap = minHeap.filter(item => item.key > 3);
    expect(filteredHeap.peek()).toEqual({ a: 'a4', key: 4 });
    expect(filteredHeap.sort()).toEqual([
      { a: 'a4', key: 4 },
      { a: 'a5', key: 5 },
      { a: 'a6', key: 6 }
    ]);
  });

  it('should object heap', () => {
    const heap = new Heap<{
      rawItem: { id: number };
    }>(
      [
        { rawItem: { id: 4 } },
        { rawItem: { id: 8 } },
        { rawItem: { id: 6 } },
        { rawItem: { id: 7 } },
        { rawItem: { id: 1 } },
        { rawItem: { id: 3 } },
        { rawItem: { id: 5 } }
      ],
      {
        comparator: (a, b) => a.rawItem.id - b.rawItem.id
      }
    );

    expect([...heap.sort()]).toEqual([
      { rawItem: { id: 1 } },
      { rawItem: { id: 3 } },
      { rawItem: { id: 4 } },
      { rawItem: { id: 5 } },
      { rawItem: { id: 6 } },
      { rawItem: { id: 7 } },
      { rawItem: { id: 8 } }
    ]);
  });

  it('should toElementFn', () => {
    const heap = new Heap<number, { rawItem: { id: number } }>(
      [
        { rawItem: { id: 4 } },
        { rawItem: { id: 8 } },
        { rawItem: { id: 6 } },
        { rawItem: { id: 7 } },
        { rawItem: { id: 1 } },
        { rawItem: { id: 3 } },
        { rawItem: { id: 5 } }
      ],
      {
        toElementFn: rawElement => rawElement.rawItem.id
      }
    );

    expect([...heap.sort()]).toEqual([1, 3, 4, 5, 6, 7, 8]);
  });

  it('should getter leaf', function () {
    const hp = new Heap<number>();
    expect(hp.leaf).toBe(undefined);
    hp.add(1);
    expect(hp.leaf).toBe(1);
  });

  it('should error', function () {
    expect(() => {
      new Heap([{ key: 1 }, { key: 2 }, { key: 3 }]);
    }).toThrow(
      "When comparing object types, a custom comparator must be defined in the constructor's options parameter."
    );
  });
});

describe('Heap HOF', () => {
  let hp: Heap;

  beforeEach(() => {
    hp = new Heap([{ key: 1 }, { key: 2 }, { key: 3 }], { comparator: (a, b) => a.key - b.key });
  });

  it('should filter', () => {
    const filtered = hp.filter(({ key }) => key % 2 === 1);
    expect([...filtered]).toEqual([{ key: 1 }, { key: 3 }]);
  });

  it('should map', () => {
    const mapped = hp.map(
      ({ key }) => [key, key],
      (a, b) => a[0] - b[0]
    );
    expect([...mapped]).toEqual([
      [1, 1],
      [2, 2],
      [3, 3]
    ]);
  });
});
describe('FibonacciHeap', () => {
  let heap: FibonacciHeap<number>;

  beforeEach(() => {
    heap = new FibonacciHeap<number>();
  });

  it('push & peek', () => {
    heap.push(10);
    heap.push(5);
    expect(heap.peek()).toBe(5);
  });

  it('pop', () => {
    heap.push(10);
    heap.push(5);
    heap.push(15);
    expect(heap.pop()).toBe(5);
    expect(heap.pop()).toBe(10);
    expect(heap.pop()).toBe(15);
  });

  it('pop on an empty heap', () => {
    expect(heap.pop()).toBeUndefined();
  });

  it('size', () => {
    expect(heap.size).toBe(0);
    heap.push(10);
    expect(heap.size).toBe(1);
    heap.pop();
    expect(heap.size).toBe(0);
  });

  it('clear', () => {
    heap.push(10);
    heap.push(5);
    heap.clear();
    expect(heap.size).toBe(0);
    expect(heap.peek()).toBeUndefined();
  });

  it('custom comparator', () => {
    const maxHeap = new FibonacciHeap<number>((a, b) => b - a);
    maxHeap.push(10);
    maxHeap.push(5);
    expect(maxHeap.peek()).toBe(10);
  });
});

describe('FibonacciHeap', () => {
  let heap: FibonacciHeap<number>;

  beforeEach(() => {
    heap = new FibonacciHeap<number>();
  });

  it('should initialize an empty heap', () => {
    expect(heap.size).toBe(0);
    expect(heap.peek()).toBeUndefined();
  });

  it('should push items into the heap and update size', () => {
    heap.push(10);
    heap.push(5);

    expect(heap.size).toBe(2);
  });

  it('should peek the minimum item', () => {
    heap.push(10);
    heap.push(5);
    heap.push(15);

    expect(heap.peek()).toBe(5);
  });

  it('should pop the minimum item and update size', () => {
    heap.push(10);
    heap.push(5);
    heap.push(15);

    const minItem = heap.pop();

    expect(minItem).toBe(5);
    expect(heap.size).toBe(2);
  });

  it('should correctly merge two heaps', () => {
    const heap1 = new FibonacciHeap<number>();
    const heap2 = new FibonacciHeap<number>();

    heap1.push(10);
    heap2.push(5);

    heap1.merge(heap2);

    expect(heap1.size).toBe(2);
    expect(heap1.peek()).toBe(5);
  });

  it('should clear the heap', () => {
    heap.push(10);
    heap.push(5);

    heap.clear();

    expect(heap.size).toBe(0);
    expect(heap.peek()).toBeUndefined();
  });

  it('should handle custom comparators', () => {
    const customComparator = (a: number, b: number) => b - a;
    const customHeap = new FibonacciHeap<number>(customComparator);

    customHeap.push(10);
    customHeap.push(5);
    customHeap.push(15);

    expect(customHeap.peek()).toBe(15);
  });

  describe('FibonacciHeap Merge', () => {
    it('should merge two Fibonacci heaps correctly', () => {
      const heap1 = new FibonacciHeap<number>();
      heap1.push(5).push(10);

      const heap2 = new FibonacciHeap<number>();
      heap2.push(3).push(7);

      heap1.merge(heap2);

      expect(heap1.size).toBe(4); // Combined size of both heaps
      expect(heap2.size).toBe(0); // Merged heap should be empty
      expect(heap1.peek()).toBe(3); // Minimum element should be 3
    });
  });
});

describe('FibonacciHeap Stress Test', () => {
  it('should handle a large number of elements efficiently', () => {
    const testByMagnitude = (magnitude: number) => {
      const heap = new FibonacciHeap<number>();

      // Add 1000 elements to the heap
      for (let i = 1; i <= magnitude; i++) {
        heap.push(i);
      }

      // Verify that the minimum element is 1 (smallest element)
      expect(heap.peek()).toBe(1);

      // Remove all 1000 elements from the heap
      const elements = [];
      while (heap.size > 0) {
        elements.push(heap.pop());
      }

      // Verify that all elements were removed in ascending order
      for (let i = 1; i <= magnitude; i++) {
        expect(elements[i - 1]).toBe(i);
      }

      // Verify that the heap is now empty
      expect(heap.size).toBe(0);
    };

    testByMagnitude(1000);

    // [
    //   10, 100, 1000, 5000, 10000, 20000, 50000, 75000, 100000,
    //   150000, 200000, 250000, 300000, 400000, 500000, 600000, 700000, 800000, 900000, 1000000
    // ].forEach(m => logBigOMetricsWrap<typeof testByMagnitude>(testByMagnitude, [m]));
    // [
    //   10, 100, 1000, 5000, 10000, 20000, 50000, 75000, 100000, 150000, 200000, 250000, 300000, 400000, 500000, 600000,
    //   700000, 800000, 900000, 1000000
    // ]
    [10, 100, 1000, 5000].forEach(m =>
      logBigOMetricsWrap(
        (c: number) => {
          const result: number[] = [];
          for (let i = 0; i < c; i++) result.push(i);
          return result;
        },
        [m],
        'loopPush'
      )
    );
  });
});

describe('classic use', () => {
  it('@example Use Heap to sort an array', () => {
    function heapSort(arr: number[]): number[] {
      const heap = new Heap<number>(arr, { comparator: (a, b) => a - b });
      const sorted: number[] = [];
      while (!heap.isEmpty()) {
        sorted.push(heap.poll()!); // Poll minimum element
      }
      return sorted;
    }

    const array = [5, 3, 8, 4, 1, 2];
    expect(heapSort(array)).toEqual([1, 2, 3, 4, 5, 8]);
  });

  it('@example Use Heap to solve top k problems', () => {
    function topKElements(arr: number[], k: number): number[] {
      const heap = new Heap<number>([], { comparator: (a, b) => b - a }); // Max heap
      arr.forEach(num => {
        heap.add(num);
        if (heap.size > k) heap.poll(); // Keep the heap size at K
      });
      return heap.toArray();
    }

    const numbers = [10, 30, 20, 5, 15, 25];
    expect(topKElements(numbers, 3)).toEqual([15, 10, 5]);
  });

  it('@example Use Heap to merge sorted sequences', () => {
    function mergeSortedSequences(sequences: number[][]): number[] {
      const heap = new Heap<{ value: number; seqIndex: number; itemIndex: number }>([], {
        comparator: (a, b) => a.value - b.value // Min heap
      });

      // Initialize heap
      sequences.forEach((seq, seqIndex) => {
        if (seq.length) {
          heap.add({ value: seq[0], seqIndex, itemIndex: 0 });
        }
      });

      const merged: number[] = [];
      while (!heap.isEmpty()) {
        const { value, seqIndex, itemIndex } = heap.poll()!;
        merged.push(value);

        if (itemIndex + 1 < sequences[seqIndex].length) {
          heap.add({
            value: sequences[seqIndex][itemIndex + 1],
            seqIndex,
            itemIndex: itemIndex + 1
          });
        }
      }

      return merged;
    }

    const sequences = [
      [1, 4, 7],
      [2, 5, 8],
      [3, 6, 9]
    ];
    expect(mergeSortedSequences(sequences)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  });

  it('@example Use Heap to dynamically maintain the median', () => {
    class MedianFinder {
      private low: MaxHeap<number>; // Max heap, stores the smaller half
      private high: MinHeap<number>; // Min heap, stores the larger half

      constructor() {
        this.low = new MaxHeap<number>([]);
        this.high = new MinHeap<number>([]);
      }

      addNum(num: number): void {
        if (this.low.isEmpty() || num <= this.low.peek()!) this.low.add(num);
        else this.high.add(num);

        // Balance heaps
        if (this.low.size > this.high.size + 1) this.high.add(this.low.poll()!);
        else if (this.high.size > this.low.size) this.low.add(this.high.poll()!);
      }

      findMedian(): number {
        if (this.low.size === this.high.size) return (this.low.peek()! + this.high.peek()!) / 2;
        return this.low.peek()!;
      }
    }

    const medianFinder = new MedianFinder();
    medianFinder.addNum(10);
    expect(medianFinder.findMedian()).toBe(10);
    medianFinder.addNum(20);
    expect(medianFinder.findMedian()).toBe(15);
    medianFinder.addNum(30);
    expect(medianFinder.findMedian()).toBe(20);
    medianFinder.addNum(40);
    expect(medianFinder.findMedian()).toBe(25);
    medianFinder.addNum(50);
    expect(medianFinder.findMedian()).toBe(30);
  });

  it('@example Use Heap for load balancing', () => {
    function loadBalance(requests: number[], servers: number): number[] {
      const serverHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // min heap
      const serverLoads = new Array(servers).fill(0);

      for (let i = 0; i < servers; i++) {
        serverHeap.add({ id: i, load: 0 });
      }

      requests.forEach(req => {
        const server = serverHeap.poll()!;
        serverLoads[server.id] += req;
        server.load += req;
        serverHeap.add(server); // The server after updating the load is re-entered into the heap
      });

      return serverLoads;
    }

    const requests = [5, 2, 8, 3, 7];
    expect(loadBalance(requests, 3)).toEqual([12, 8, 5]);
  });

  it('@example Use Heap to schedule tasks', () => {
    type Task = [string, number];

    function scheduleTasks(tasks: Task[], machines: number): Map<number, Task[]> {
      const machineHeap = new Heap<{ id: number; load: number }>([], { comparator: (a, b) => a.load - b.load }); // Min heap
      const allocation = new Map<number, Task[]>();

      // Initialize the load on each machine
      for (let i = 0; i < machines; i++) {
        machineHeap.add({ id: i, load: 0 });
        allocation.set(i, []);
      }

      // Assign tasks
      tasks.forEach(([task, load]) => {
        const machine = machineHeap.poll()!;
        allocation.get(machine.id)!.push([task, load]);
        machine.load += load;
        machineHeap.add(machine); // The machine after updating the load is re-entered into the heap
      });

      return allocation;
    }

    const tasks: Task[] = [
      ['Task1', 3],
      ['Task2', 1],
      ['Task3', 2],
      ['Task4', 5],
      ['Task5', 4]
    ];
    const expectedMap = new Map<number, Task[]>();
    expectedMap.set(0, [
      ['Task1', 3],
      ['Task4', 5]
    ]);
    expectedMap.set(1, [
      ['Task2', 1],
      ['Task3', 2],
      ['Task5', 4]
    ]);
    expect(scheduleTasks(tasks, 2)).toEqual(expectedMap);
  });
});
