'use strict';

// Test an API with all the possible filed types and simple filtering (no deep filtering, no relations)

const { createStrapiInstance } = require('../../../test/helpers/strapi');
const { createTestBuilder } = require('../../../test/helpers/builder');
const { createAuthRequest } = require('../../../test/helpers/request');

const builder = createTestBuilder();
let strapi;
let rq;
const data = { product: [] };

// complete list of existing fields and tests
const product = {
  attributes: {
    name: {
      type: 'string',
    },
    description: {
      type: 'text',
    },
    price: {
      type: 'float',
    },
    decimal_field: {
      type: 'decimal',
    },
    rank: {
      type: 'integer',
    },
    big_rank: {
      type: 'biginteger',
    },
  },
  connection: 'default',
  name: 'product',
  description: '',
  collectionName: '',
};

const productFixtures = [
  {
    name: 'Product 1',
    description: 'Product description',
    price: 10.99,
    decimal_field: 42.43,
    rank: 42,
    big_rank: '345678912983',
  },
  {
    name: 'Product 2',
    description: 'Product description 2',
    price: 28.31,
    decimal_field: 91.22,
    rank: 82,
    big_rank: '926371623421',
  },
  {
    name: 'Product 3',
    description: 'Product description 3',
    price: 28.31,
    decimal_field: 12.22,
    rank: 91,
    big_rank: '926372323421',
  },
  {
    name: 'Product 4',
    description: 'Product description 4',
    price: null,
    decimal_field: 12.22,
    rank: 99,
    big_rank: '999999999999',
  },
  {
    name: 'Продукт 5, Product 5',
    description: 'Опис на продукт 5',
    price: null,
    decimal_field: 142.43,
    rank: 142,
    big_rank: 345678912983,
  },
];

describe('Filtering API', () => {
  beforeAll(async () => {
    await builder
      .addContentType(product)
      .addFixtures(product.name, productFixtures)
      .build();

    strapi = await createStrapiInstance();
    rq = await createAuthRequest({ strapi });

    Object.assign(data, builder.sanitizedFixtures(strapi));
  }, 60000);

  afterAll(async () => {
    await strapi.destroy();
    await builder.cleanup();
  }, 60000);

  describe('Basic filters', () => {
    describe('Filter equals', () => {
      test('Should be the default filter', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name: 'Product 1',
          },
        });

        expect(Array.isArray(res.body)).toBe(true);
        expect(res.body.length).toBe(1);
        expect(res.body[0]).toMatchObject(data.product[0]);
      });

      test('Should be usable with equal suffix', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_eq: 'Product 1',
          },
        });

        expect(Array.isArray(res.body)).toBe(true);
        expect(res.body.length).toBe(1);
        expect(res.body[0]).toMatchObject(data.product[0]);
      });

      test('Should return an empty array when no match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_eq: 'Product non existant',
          },
        });

        expect(res.body).toEqual([]);
      });
    });

    describe('Filter not equals', () => {
      test('Should return an array with matching entities', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ne: 'Non existent product ',
          },
        });

        expect(res.body).toEqual(
          expect.arrayContaining(data.product.map(o => expect.objectContaining(o)))
        );
      });

      test('Should return an empty array when no match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ne: 'Product 1',
          },
        });

        expect(res.body).toEqual(expect.not.arrayContaining([data.product[0]]));
      });
    });

    describe('Filter null', () => {
      test('Should return only matching items', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_null: true,
          },
        });

        const matching = data.product.filter(x => x.price === null);
        res.body.sort((a, b) => (a.id > b.id ? 1 : -1));
        expect(Array.isArray(res.body)).toBe(true);
        expect(res.body.length).toBe(matching.length);
        expect(res.body).toEqual(expect.arrayContaining(matching));
      });

      test('Should return three matches', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_null: false,
          },
        });

        expect(Array.isArray(res.body)).toBe(true);
        expect(res.body.length).toBe(3);
      });
    });

    describe('Filter contains insensitive', () => {
      test('Should match with insensitive case', async () => {
        const res1 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_contains: 'product',
          },
        });

        expect(res1.body).toEqual(
          expect.arrayContaining(data.product.map(o => expect.objectContaining(o)))
        );

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_contains: 'PrOdUct',
          },
        });

        expect(res1.body).toEqual(res2.body);
      });

      test('Should return an empty array on no insensitive case match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_contains: 'production',
          },
        });

        expect(res.body).toEqual([]);

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_contains: 'ProdUctIon',
          },
        });

        expect(res2.body).toEqual(res.body);
      });
    });

    describe('Filter not contains insensitive', () => {
      test('Should return an array of entities on match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontains: 'production',
          },
        });

        expect(res.body).toEqual(
          expect.arrayContaining(data.product.map(o => expect.objectContaining(o)))
        );

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontains: 'ProdUctIon',
          },
        });

        expect(res2.body).toEqual(res.body);
      });

      test('Should return an empty array when no match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontains: 'product',
          },
        });

        expect(res.body).toEqual([]);

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontains: 'ProDuCt',
          },
        });

        expect(res2.body).toEqual(res.body);
      });
    });

    // FIXME: Not working on sqlite due to https://www.sqlite.org/draft/pragma.html#pragma_case_sensitive_like
    describe('Filter contains sensitive', () => {
      test.skip('Should return empty if the case does not match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_containss: 'product',
          },
        });

        expect(res.body).toEqual([]);
      });

      test('Should return the entities if the case matches', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_containss: 'Product',
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });

    // FIXME: Not working on sqlite due to https://www.sqlite.org/draft/pragma.html#pragma_case_sensitive_like
    describe('Filter not contains sensitive', () => {
      test.skip('Should return the entities if the case does not match', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontainss: 'product',
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should return an empty array if the case matches', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            name_ncontainss: 'Product',
          },
        });

        expect(res.body).toEqual([]);
      });
    });

    describe('Filter in', () => {
      test('Should return the Product with a single value', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_in: 42,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should return the Product with an array of values', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_in: [42, 12],
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should return a, empty array if no values are matching', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_in: [43, 12],
          },
        });

        expect(res.body).toEqual([]);
      });
    });

    describe('Filter not in', () => {
      test('Should return an array without the values matching when a single value is provided', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_nin: 42,
          },
        });

        expect(res.body).toEqual(expect.not.arrayContaining([data.product[0]]));
      });

      test('Should return an array without the values matching when an array of values is provided', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_nin: [42, 12],
          },
        });

        expect(res.body).toEqual(expect.not.arrayContaining([data.product[0]]));
      });

      test('Should return an array with values that do not match the filter', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_nin: [43, 12],
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });

    describe('Filter greater than', () => {
      test('Should match values only greater than', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gt: 42,
          },
        });

        expect(res.body).toEqual(expect.not.arrayContaining([data.product[0]]));

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gt: 40,
          },
        });

        expect(res2.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with integers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gt: 40,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with float', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_gt: 9.3,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with decimal', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            decimal_field_gt: 1.23,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with bigintegers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            big_rank_gt: 34567891298,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });

    describe('Filter greater than or equal', () => {
      test('Should work correclty on equal values', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gte: 42,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gte: 43,
          },
        });

        expect(res2.body).toEqual(expect.not.arrayContaining([data.product[0]]));
      });

      test('Should work with integers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_gte: 40,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with float', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_gte: 10.99,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with decimal', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            decimal_field_gte: 42.43,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with bigintegers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            big_rank_gte: '345678912983',
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });

    describe('Filter less than', () => {
      test('Should match values only less than', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lt: 42,
          },
        });

        expect(res.body).toEqual(expect.not.arrayContaining([data.product[0]]));

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lt: 43,
          },
        });

        expect(res2.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with integers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lt: 45,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with float', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_lt: 21.3,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with decimal', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            decimal_field_lt: 46.23,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with bigintegers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            big_rank_lt: 3456789129812,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });

    describe('Filter less than or equal', () => {
      test('Should work correclty on equal values', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lte: 52,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));

        const res2 = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lte: 21,
          },
        });

        expect(res2.body).toEqual(expect.not.arrayContaining([data.product[0]]));
      });

      test('Should work with integers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            rank_lte: 42,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with float', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            price_lte: 10.99,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with decimal', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            decimal_field_lte: 42.43,
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });

      test('Should work with bigintegers', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            big_rank_lte: '345678912983',
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
      });
    });
  });

  describe('Or filtering', () => {
    describe('_or filter', () => {
      test('Supports simple or', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            _where: {
              _or: [
                {
                  rank: 42,
                },
                {
                  rank: 82,
                },
              ],
            },
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0], data.product[1]]));
      });

      test('Supports simple or on different fields', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            _where: {
              _or: [
                {
                  rank: 42,
                },
                {
                  price_gt: 28,
                },
              ],
            },
          },
        });

        expect(res.body).toEqual(
          expect.arrayContaining([data.product[0], data.product[1], data.product[2]])
        );
      });

      test('Supports or with nested and', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            _where: {
              _or: [
                {
                  rank: 42,
                },
                [
                  {
                    price_gt: 28,
                  },
                  {
                    rank: 91,
                  },
                ],
              ],
            },
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0], data.product[2]]));
      });

      test('Supports or with nested or', async () => {
        const res = await rq({
          method: 'GET',
          url: '/products',
          qs: {
            _where: {
              _or: [
                {
                  rank: 42,
                },
                [
                  {
                    price_gt: 28,
                  },
                  {
                    _or: [
                      {
                        rank: 91,
                      },
                    ],
                  },
                ],
              ],
            },
          },
        });

        expect(res.body).toEqual(expect.arrayContaining([data.product[0], data.product[2]]));
      });
    });

    test('Filter equals', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank: [42, 43],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter not equals', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_ne: [41, 43],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter contains insensitive', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_contains: ['Product', '1'],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter not contains insensitive', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_ncontains: ['Product', 'Non existent'],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter contains sensitive', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_containss: ['Product', 'Non existent'],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter not contains sensitive', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_ncontainss: ['product', 'Non existent'],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter greater than', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_gt: [12, 56],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter greater than or equal', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_gte: [42, 56],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter less than', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_lt: [56, 12],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Filter less than or equal', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_lte: [12, 42],
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });
  });

  describe('Complexe filtering', () => {
    test('Greater than and less than at the same time', async () => {
      let res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_lte: 42,
          rank_gte: 42,
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));

      res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_lt: 43,
          rank_gt: 41,
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));

      res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          rank_lt: 43,
          rank_gt: 431,
        },
      });

      expect(res.body).toEqual([]);
    });

    test('Contains and Not contains on same column', async () => {
      let res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_contains: 'Product',
          name_ncontains: '1',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));

      res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_contains: 'Product 1',
          name_ncontains: ['2', '3'],
        },
      });

      expect(res.body).toEqual(expect.not.arrayContaining([data.product[1], data.product[2]]));
      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));

      res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_contains: '2',
          name_ncontains: 'Product',
        },
      });

      expect(res.body).toEqual([]);
    });

    test('Combined filters', async () => {
      let res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          name_contains: 'Product',
          rank_lt: 45,
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });
  });

  describe('Sorting', () => {
    test('Default sorting is asc', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _sort: 'rank',
        },
      });

      expect(res.body).toEqual(
        expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
      );
    });

    test('Simple sorting', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _sort: 'rank:asc',
        },
      });

      expect(res.body).toEqual(
        expect.arrayContaining(data.product.slice(0).sort((a, b) => a.rank - b.rank))
      );

      const res2 = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _sort: 'rank:desc',
        },
      });

      expect(res2.body).toEqual(
        expect.arrayContaining(data.product.slice(0).sort((a, b) => b.rank - a.rank))
      );
    });

    test('Multi column sorting', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _sort: 'price:asc,rank:desc',
        },
      });

      [data.product[3], data.product[0], data.product[2], data.product[1]].forEach(expectedPost => {
        expect(res.body).toEqual(expect.arrayContaining([expectedPost]));
      });
    });
  });

  describe('Limit and offset', () => {
    test('Limit', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _limit: 1,
          _sort: 'rank:asc',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Limit with sorting', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _limit: 1,
          _sort: 'rank:desc',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[data.product.length - 1]]));
    });

    test('Offset', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _start: 1,
          _sort: 'rank:asc',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1)));
    });

    test('Offset with limit', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _limit: 1,
          _start: 1,
          _sort: 'rank:asc',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining(data.product.slice(1, 2)));
    });
  });

  describe('Text query', () => {
    test('Cyrillic query', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _q: 'Опис',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
    });

    test('Multi word query', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _q: 'Product description',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[0]]));
    });

    test('Multi word cyrillic query', async () => {
      const res = await rq({
        method: 'GET',
        url: '/products',
        qs: {
          _q: 'Опис на продукт',
        },
      });

      expect(res.body).toEqual(expect.arrayContaining([data.product[4]]));
    });
  });
});
