// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {
  c,
  cmpFromExpr,
  cmpFromSort,
  ConcreteEventSet,
  Direction,
  EmptyEventSet,
  EmptyKeySet,
  eq,
  Event,
  EventSet,
  isConcreteEventSet,
  isEmptyEventSet,
  KeySet,
  Num,
  Str,
  UntypedEvent,
  v,
} from './event_set';

describe('EventSet', () => {
  test('Event', () => {
    {
      const keyset: EmptyKeySet = {};
      const event: Event<typeof keyset> = {
        id: 'foo',
      };
      void event;
    }
    {
      const keyset = {
        bar: Num,
      };
      const event: Event<typeof keyset> = {
        id: 'foo',
        bar: 42,
      };
      void event;
    }
  });

  describe('EmptyEventSet', () => {
    test('isEmpty', async () => {
      const events = EmptyEventSet.get();
      expect(await events.isEmpty()).toEqual(true);
      expect(await events.count()).toEqual(0);
    });

    test('isEmptyEventSet', () => {
      const events: EventSet<KeySet> = EmptyEventSet.get();
      expect(isEmptyEventSet(events)).toEqual(true);
    });

    test('materialise', async () => {
      const events: EventSet<KeySet> = EmptyEventSet.get();
      const materialised = await events.materialise({});

      expect(await materialised.isEmpty()).toEqual(true);
      expect(await materialised.count()).toEqual(0);
      expect(materialised.events).toEqual([]);
      expect(isConcreteEventSet(materialised)).toEqual(true);
    });

    test('union', async () => {
      const a: EventSet<KeySet> = EmptyEventSet.get();
      const b: EventSet<KeySet> = EmptyEventSet.get();

      const aUnionB = a.union(b);

      expect(await aUnionB.isEmpty()).toEqual(true);
      expect(await aUnionB.count()).toEqual(0);
    });

    test('intersect', async () => {
      const a: EventSet<KeySet> = EmptyEventSet.get();
      const b: EventSet<KeySet> = EmptyEventSet.get();

      const aIntersectB = a.intersect(b);

      expect(await aIntersectB.isEmpty()).toEqual(true);
      expect(await aIntersectB.count()).toEqual(0);
    });

    test('filter', async () => {
      const events: EventSet<KeySet> = EmptyEventSet.get();
      const filtered = await events.filter(c(true));

      expect(filtered).toBe(events);
      expect(await filtered.isEmpty()).toEqual(true);
      expect(await filtered.count()).toEqual(0);
    });

    test('sort', async () => {
      const events: EventSet<KeySet> = EmptyEventSet.get();
      const sorted = await events.sort({
        direction: Direction.ASC,
        expression: c(0),
      });

      expect(sorted).toBe(events);
      expect(await sorted.isEmpty()).toEqual(true);
      expect(await sorted.count()).toEqual(0);
    });
  });

  describe('ConcreteEventSet', () => {
    test('isEmpty', async () => {
      const event: Event<EmptyKeySet> = {
        id: 'foo',
      };
      const empty = new ConcreteEventSet<EmptyKeySet>({}, []);
      const events = new ConcreteEventSet<EmptyKeySet>({}, [event]);
      expect(await empty.isEmpty()).toEqual(true);
      expect(await empty.count()).toEqual(0);
      expect(await events.isEmpty()).toEqual(false);
      expect(await events.count()).toEqual(1);
    });

    test('isConcreteEventSet', () => {
      expect(
        isConcreteEventSet(new ConcreteEventSet<EmptyKeySet>({}, [])),
      ).toEqual(true);
      expect(isConcreteEventSet(EmptyEventSet.get())).toEqual(false);
    });

    test('materialise', async () => {
      const keys = {
        num: Num,
        char: Str,
      };

      const a: Event<typeof keys> = {
        id: 'a',
        num: 97,
        char: 'a',
      };
      const b: Event<typeof keys> = {
        id: 'b',
        num: 98,
        char: 'b',
      };
      const d: Event<typeof keys> = {
        id: 'd',
        num: 100,
        char: 'd',
      };

      const events = new ConcreteEventSet(keys, [a, b, d]);

      expect((await events.materialise(keys)).events).toEqual([a, b, d]);
      expect((await events.materialise(keys, 1)).events).toEqual([b, d]);
      expect((await events.materialise(keys, 1, 1)).events).toEqual([b]);
      expect((await events.materialise(keys, 99)).events).toEqual([]);
      expect((await events.materialise(keys, 99, 0)).events).toEqual([]);
      expect((await events.materialise({num: Num})).events).toEqual([
        {id: 'a', num: 97},
        {id: 'b', num: 98},
        {id: 'd', num: 100},
      ]);
      expect((await events.materialise({char: Str}, 1, 1)).events).toEqual([
        {id: 'b', char: 'b'},
      ]);
    });

    test('union', async () => {
      const a: Event<EmptyKeySet> = {
        id: 'a',
      };
      const b: Event<EmptyKeySet> = {
        id: 'b',
      };
      const d: Event<EmptyKeySet> = {
        id: 'd',
      };

      const empty = EmptyEventSet.get();
      const justA = new ConcreteEventSet({}, [a]);
      const justB = new ConcreteEventSet({}, [b]);
      const justD = new ConcreteEventSet({}, [d]);

      const aAndB = justA.union(justB);
      const aAndA = justA.union(justA);
      const aAndD = justA.union(justD);
      const aAndBAndEmpty = aAndB.union(empty);
      const aAndDAndAAndB = aAndD.union(aAndB);

      expect((await aAndB.materialise({})).events).toEqual([a, b]);
      expect((await aAndA.materialise({})).events).toEqual([a]);
      expect((await aAndD.materialise({})).events).toEqual([a, d]);
      expect((await aAndBAndEmpty.materialise({})).events).toEqual([a, b]);
      expect((await aAndDAndAAndB.materialise({})).events).toEqual([a, d, b]);

      expect(await aAndB.isEmpty()).toEqual(false);
      expect(await aAndA.isEmpty()).toEqual(false);
      expect(await aAndD.isEmpty()).toEqual(false);
      expect(await aAndBAndEmpty.isEmpty()).toEqual(false);
      expect(await aAndDAndAAndB.isEmpty()).toEqual(false);

      expect(await aAndB.count()).toEqual(2);
      expect(await aAndA.count()).toEqual(1);
      expect(await aAndD.count()).toEqual(2);
      expect(await aAndBAndEmpty.count()).toEqual(2);
      expect(await aAndDAndAAndB.count()).toEqual(3);
    });

    test('intersection', async () => {
      const a: Event<EmptyKeySet> = {
        id: 'a',
      };
      const b: Event<EmptyKeySet> = {
        id: 'b',
      };
      const d: Event<EmptyKeySet> = {
        id: 'd',
      };

      const empty = EmptyEventSet.get();
      const justA = new ConcreteEventSet({}, [a]);
      const justB = new ConcreteEventSet({}, [b]);
      const justD = new ConcreteEventSet({}, [d]);

      const aAndB = justA.intersect(justB);
      const aAndA = justA.intersect(justA);
      const aAndD = justA.intersect(justD);
      const aBAndEmpty = justA.union(justB).intersect(empty);
      const aDAndAB = justA.union(justB).intersect(justA.union(justD));

      expect((await aAndB.materialise({})).events).toEqual([]);
      expect((await aAndA.materialise({})).events).toEqual([a]);
      expect((await aAndD.materialise({})).events).toEqual([]);
      expect((await aBAndEmpty.materialise({})).events).toEqual([]);
      expect((await aDAndAB.materialise({})).events).toEqual([a]);

      expect(await aAndB.isEmpty()).toEqual(true);
      expect(await aAndA.isEmpty()).toEqual(false);
      expect(await aAndD.isEmpty()).toEqual(true);
      expect(await aBAndEmpty.isEmpty()).toEqual(true);
      expect(await aDAndAB.isEmpty()).toEqual(false);

      expect(await aAndB.count()).toEqual(0);
      expect(await aAndA.count()).toEqual(1);
      expect(await aAndD.count()).toEqual(0);
      expect(await aBAndEmpty.count()).toEqual(0);
      expect(await aDAndAB.count()).toEqual(1);
    });

    test('filter', async () => {
      const keys = {
        num: Num,
        char: Str,
      };

      const a: Event<typeof keys> = {
        id: 'a',
        num: 97,
        char: 'a',
      };
      const b: Event<typeof keys> = {
        id: 'b',
        num: 98,
        char: 'b',
      };
      const d: Event<typeof keys> = {
        id: 'd',
        num: 100,
        char: 'd',
      };

      const events = new ConcreteEventSet(keys, [a, b, d]);

      const justA = events.filter(eq(v('id'), c('a')));
      const justD = events.filter(eq(v('num'), c(100)));

      expect((await justA.materialise(keys)).events).toEqual([a]);
      expect((await justD.materialise(keys)).events).toEqual([d]);
    });

    test('sort', async () => {
      const keys = {
        num: Num,
        char: Str,
      };

      const a: Event<typeof keys> = {
        id: 'a',
        num: 97,
        char: 'a',
      };
      const b: Event<typeof keys> = {
        id: 'b',
        num: 98,
        char: 'b',
      };
      const d: Event<typeof keys> = {
        id: 'd',
        num: 100,
        char: 'd',
      };

      const events = new ConcreteEventSet(keys, [a, b, d]);

      const byNum = events.sort({
        expression: v('num'),
        direction: Direction.ASC,
      });
      const byStr = events.sort({
        expression: v('char'),
        direction: Direction.ASC,
      });

      expect((await byNum.materialise(keys)).events).toEqual([a, b, d]);
      expect((await byStr.materialise(keys)).events).toEqual([a, b, d]);
    });

    test('sort desc', async () => {
      const keys = {
        num: Num,
        char: Str,
      };

      const a: Event<typeof keys> = {
        id: 'a',
        num: 97,
        char: 'a',
      };
      const b: Event<typeof keys> = {
        id: 'b',
        num: 98,
        char: 'b',
      };
      const d: Event<typeof keys> = {
        id: 'd',
        num: 100,
        char: 'd',
      };

      const events = new ConcreteEventSet(keys, [a, b, d]);

      const byNum = events.sort({
        expression: v('num'),
        direction: Direction.DESC,
      });
      const byStr = events.sort({
        expression: v('char'),
        direction: Direction.DESC,
      });

      expect((await byNum.materialise(keys)).events).toEqual([d, b, a]);
      expect((await byStr.materialise(keys)).events).toEqual([d, b, a]);
    });
  });
});

describe('cmpFromExpr', () => {
  test('simple', () => {
    const a: UntypedEvent = {
      id: 'a',
      x: 0,
    };
    const b: UntypedEvent = {
      id: 'b',
      x: 42,
    };
    const c: UntypedEvent = {
      id: 'c',
      x: 0,
    };

    const cmp = cmpFromExpr(v('x'));
    expect(cmp(a, b)).toEqual(-1);
    expect(cmp(a, a)).toEqual(0);
    expect(cmp(b, a)).toEqual(1);
    expect(cmp(a, c)).toEqual(0);
  });

  test('kinds', () => {
    const nullEvent: UntypedEvent = {
      id: 'nullEvent',
      x: null,
    };
    const sevenEvent: UntypedEvent = {
      id: 'sevenEvent',
      x: 7,
    };
    const oneEvent: UntypedEvent = {
      id: 'oneEvent',
      x: 1,
    };
    const zeroEvent: UntypedEvent = {
      id: 'zeroEvent',
      x: 0,
    };
    const trueEvent: UntypedEvent = {
      id: 'trueEvent',
      x: true,
    };
    const falseEvent: UntypedEvent = {
      id: 'falseEvent',
      x: false,
    };
    const aardvarkEvent: UntypedEvent = {
      id: 'aardvarkEvent',
      x: 'aardvark',
    };
    const zigguratEvent: UntypedEvent = {
      id: 'zigguratEvent',
      x: 'ziggurat',
    };
    const bigZeroEvent: UntypedEvent = {
      id: 'bigZeroEvent',
      x: 0n,
    };
    const bigOneEvent: UntypedEvent = {
      id: 'bigOneEvent',
      x: 1n,
    };
    const bigTwoEvent: UntypedEvent = {
      id: 'bigTwoEvent',
      x: 2n,
    };

    const cmp = cmpFromExpr(v('x'));

    // Everything is equal to itself:
    expect(cmp(nullEvent, nullEvent)).toEqual(0);
    expect(cmp(sevenEvent, sevenEvent)).toEqual(0);
    expect(cmp(oneEvent, oneEvent)).toEqual(0);
    expect(cmp(zeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(falseEvent, falseEvent)).toEqual(0);
    expect(cmp(trueEvent, trueEvent)).toEqual(0);
    expect(cmp(aardvarkEvent, aardvarkEvent)).toEqual(0);
    expect(cmp(zigguratEvent, zigguratEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, bigZeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, bigOneEvent)).toEqual(0);
    expect(cmp(bigTwoEvent, bigTwoEvent)).toEqual(0);

    // BigInt(x) == x
    expect(cmp(bigZeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, oneEvent)).toEqual(0);

    // one = true, zero = false:
    expect(cmp(oneEvent, trueEvent)).toEqual(0);
    expect(cmp(zeroEvent, falseEvent)).toEqual(0);
    expect(cmp(bigOneEvent, trueEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, falseEvent)).toEqual(0);

    // 0 < 1 < 7
    expect(cmp(zeroEvent, oneEvent)).toEqual(-1);
    expect(cmp(sevenEvent, oneEvent)).toEqual(1);

    // 0n < 1n < 2n
    expect(cmp(bigZeroEvent, bigOneEvent)).toEqual(-1);
    expect(cmp(bigTwoEvent, bigOneEvent)).toEqual(1);

    // 0 < 1n < 7
    expect(cmp(zeroEvent, bigOneEvent)).toEqual(-1);
    expect(cmp(sevenEvent, bigOneEvent)).toEqual(1);

    // aardvark < ziggurat
    expect(cmp(aardvarkEvent, zigguratEvent)).toEqual(-1);

    // null < {bools, numbers, BigInt} < strings
    expect(cmp(nullEvent, falseEvent)).toEqual(-1);
    expect(cmp(aardvarkEvent, sevenEvent)).toEqual(1);
    expect(cmp(nullEvent, bigZeroEvent)).toEqual(-1);
    expect(cmp(bigZeroEvent, sevenEvent)).toEqual(-1);
    expect(cmp(nullEvent, falseEvent)).toEqual(-1);
    expect(cmp(falseEvent, sevenEvent)).toEqual(-1);
  });
});

describe('cmpFromSort', () => {
  test('simple asc', () => {
    const a: UntypedEvent = {
      id: 'a',
      x: 0,
    };
    const b: UntypedEvent = {
      id: 'b',
      x: 42,
    };
    const c: UntypedEvent = {
      id: 'c',
      x: 0,
    };

    const cmp = cmpFromSort({
      expression: v('x'),
      direction: Direction.ASC,
    });
    expect(cmp(a, b)).toEqual(-1);
    expect(cmp(a, a)).toEqual(0);
    expect(cmp(b, a)).toEqual(1);
    expect(cmp(a, c)).toEqual(0);
  });

  test('kinds asc', () => {
    const nullEvent: UntypedEvent = {
      id: 'nullEvent',
      x: null,
    };
    const sevenEvent: UntypedEvent = {
      id: 'sevenEvent',
      x: 7,
    };
    const oneEvent: UntypedEvent = {
      id: 'oneEvent',
      x: 1,
    };
    const zeroEvent: UntypedEvent = {
      id: 'zeroEvent',
      x: 0,
    };
    const trueEvent: UntypedEvent = {
      id: 'trueEvent',
      x: true,
    };
    const falseEvent: UntypedEvent = {
      id: 'falseEvent',
      x: false,
    };
    const aardvarkEvent: UntypedEvent = {
      id: 'aardvarkEvent',
      x: 'aardvark',
    };
    const zigguratEvent: UntypedEvent = {
      id: 'zigguratEvent',
      x: 'ziggurat',
    };
    const bigZeroEvent: UntypedEvent = {
      id: 'bigZeroEvent',
      x: 0n,
    };
    const bigOneEvent: UntypedEvent = {
      id: 'bigOneEvent',
      x: 1n,
    };
    const bigTwoEvent: UntypedEvent = {
      id: 'bigTwoEvent',
      x: 2n,
    };

    const cmp = cmpFromSort({
      expression: v('x'),
      direction: Direction.ASC,
    });

    // Everything is equal to itself:
    expect(cmp(nullEvent, nullEvent)).toEqual(0);
    expect(cmp(sevenEvent, sevenEvent)).toEqual(0);
    expect(cmp(oneEvent, oneEvent)).toEqual(0);
    expect(cmp(zeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(falseEvent, falseEvent)).toEqual(0);
    expect(cmp(trueEvent, trueEvent)).toEqual(0);
    expect(cmp(aardvarkEvent, aardvarkEvent)).toEqual(0);
    expect(cmp(zigguratEvent, zigguratEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, bigZeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, bigOneEvent)).toEqual(0);
    expect(cmp(bigTwoEvent, bigTwoEvent)).toEqual(0);

    // BigInt(x) == x
    expect(cmp(bigZeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, oneEvent)).toEqual(0);

    // one = true, zero = false:
    expect(cmp(oneEvent, trueEvent)).toEqual(0);
    expect(cmp(zeroEvent, falseEvent)).toEqual(0);
    expect(cmp(bigOneEvent, trueEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, falseEvent)).toEqual(0);

    // 0 < 1 < 7
    expect(cmp(zeroEvent, oneEvent)).toEqual(-1);
    expect(cmp(sevenEvent, oneEvent)).toEqual(1);

    // 0n < 1n < 2n
    expect(cmp(bigZeroEvent, bigOneEvent)).toEqual(-1);
    expect(cmp(bigTwoEvent, bigOneEvent)).toEqual(1);

    // 0 < 1n < 7
    expect(cmp(zeroEvent, bigOneEvent)).toEqual(-1);
    expect(cmp(sevenEvent, bigOneEvent)).toEqual(1);

    // aardvark < ziggurat
    expect(cmp(aardvarkEvent, zigguratEvent)).toEqual(-1);

    // null < {bools, numbers, BigInt} < strings
    expect(cmp(nullEvent, falseEvent)).toEqual(-1);
    expect(cmp(aardvarkEvent, sevenEvent)).toEqual(1);
    expect(cmp(nullEvent, bigZeroEvent)).toEqual(-1);
    expect(cmp(bigZeroEvent, sevenEvent)).toEqual(-1);
    expect(cmp(nullEvent, falseEvent)).toEqual(-1);
    expect(cmp(falseEvent, sevenEvent)).toEqual(-1);
  });

  test('simple desc', () => {
    const a: UntypedEvent = {
      id: 'a',
      x: 0,
    };
    const b: UntypedEvent = {
      id: 'b',
      x: 42,
    };
    const c: UntypedEvent = {
      id: 'c',
      x: 0,
    };

    const cmp = cmpFromSort({
      expression: v('x'),
      direction: Direction.DESC,
    });
    expect(cmp(a, b)).toEqual(1);
    expect(cmp(a, a)).toEqual(0);
    expect(cmp(b, a)).toEqual(-1);
    expect(cmp(a, c)).toEqual(0);
  });

  test('kinds desc', () => {
    const nullEvent: UntypedEvent = {
      id: 'nullEvent',
      x: null,
    };
    const sevenEvent: UntypedEvent = {
      id: 'sevenEvent',
      x: 7,
    };
    const oneEvent: UntypedEvent = {
      id: 'oneEvent',
      x: 1,
    };
    const zeroEvent: UntypedEvent = {
      id: 'zeroEvent',
      x: 0,
    };
    const trueEvent: UntypedEvent = {
      id: 'trueEvent',
      x: true,
    };
    const falseEvent: UntypedEvent = {
      id: 'falseEvent',
      x: false,
    };
    const aardvarkEvent: UntypedEvent = {
      id: 'aardvarkEvent',
      x: 'aardvark',
    };
    const zigguratEvent: UntypedEvent = {
      id: 'zigguratEvent',
      x: 'ziggurat',
    };
    const bigZeroEvent: UntypedEvent = {
      id: 'bigZeroEvent',
      x: 0n,
    };
    const bigOneEvent: UntypedEvent = {
      id: 'bigOneEvent',
      x: 1n,
    };
    const bigTwoEvent: UntypedEvent = {
      id: 'bigTwoEvent',
      x: 2n,
    };

    const cmp = cmpFromSort({
      expression: v('x'),
      direction: Direction.DESC,
    });

    // Everything is equal to itself:
    expect(cmp(nullEvent, nullEvent)).toEqual(0);
    expect(cmp(sevenEvent, sevenEvent)).toEqual(0);
    expect(cmp(oneEvent, oneEvent)).toEqual(0);
    expect(cmp(zeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(falseEvent, falseEvent)).toEqual(0);
    expect(cmp(trueEvent, trueEvent)).toEqual(0);
    expect(cmp(aardvarkEvent, aardvarkEvent)).toEqual(0);
    expect(cmp(zigguratEvent, zigguratEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, bigZeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, bigOneEvent)).toEqual(0);
    expect(cmp(bigTwoEvent, bigTwoEvent)).toEqual(0);

    // BigInt(x) == x
    expect(cmp(bigZeroEvent, zeroEvent)).toEqual(0);
    expect(cmp(bigOneEvent, oneEvent)).toEqual(0);

    // one = true, zero = false:
    expect(cmp(oneEvent, trueEvent)).toEqual(0);
    expect(cmp(zeroEvent, falseEvent)).toEqual(0);
    expect(cmp(bigOneEvent, trueEvent)).toEqual(0);
    expect(cmp(bigZeroEvent, falseEvent)).toEqual(0);

    // 0 < 1 < 7
    expect(cmp(zeroEvent, oneEvent)).toEqual(1);
    expect(cmp(sevenEvent, oneEvent)).toEqual(-1);

    // 0n < 1n < 2n
    expect(cmp(bigZeroEvent, bigOneEvent)).toEqual(1);
    expect(cmp(bigTwoEvent, bigOneEvent)).toEqual(-1);

    // 0 < 1n < 7
    expect(cmp(zeroEvent, bigOneEvent)).toEqual(1);
    expect(cmp(sevenEvent, bigOneEvent)).toEqual(-1);

    // aardvark < ziggurat
    expect(cmp(aardvarkEvent, zigguratEvent)).toEqual(1);

    // null < {bools, numbers, BigInt} < strings
    expect(cmp(nullEvent, falseEvent)).toEqual(1);
    expect(cmp(aardvarkEvent, sevenEvent)).toEqual(-1);
    expect(cmp(nullEvent, bigZeroEvent)).toEqual(1);
    expect(cmp(bigZeroEvent, sevenEvent)).toEqual(1);
    expect(cmp(nullEvent, falseEvent)).toEqual(1);
    expect(cmp(falseEvent, sevenEvent)).toEqual(1);
  });
});
