import { randomUUID } from 'crypto';

import { OBJECT_MODEL_COMMON_FIELDS } from 'test/integration/constants/object-model-common-fields';
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
import {
  SearchFactoryParams,
  searchFactory,
} from 'test/integration/graphql/utils/search-factory.util';
import {
  LISTING_NAME_PLURAL,
  LISTING_NAME_SINGULAR,
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { EachTestingContext } from 'twenty-shared/testing';

import { SearchRecordDTO } from 'src/engine/core-modules/search/dtos/search-record-dto';

describe('SearchResolver', () => {
  let listingObjectMetadataId: { objectMetadataId: string };
  const [firstPerson, secondPerson, thirdPerson] = [
    { id: randomUUID(), name: { firstName: 'searchInput1' } },
    { id: randomUUID(), name: { firstName: 'searchInput2' } },
    { id: randomUUID(), name: { firstName: 'searchInput3' } },
  ];
  const [apiKey] = [
    {
      id: randomUUID(),
      name: 'record not searchable',
      expiresAt: new Date(Date.now()),
    },
  ];
  const [firstListing, secondListing] = [
    { id: randomUUID(), name: 'searchInput1' },
    { id: randomUUID(), name: 'searchInput2' },
  ];

  const hasSearchRecord = (search: SearchRecordDTO[], recordId: string) => {
    return search.some((item: SearchRecordDTO) => item.recordId === recordId);
  };

  beforeAll(async () => {
    try {
      const objectsMetadata = await findManyObjectMetadata({
        input: {
          filter: {},
          paging: {
            first: 1000,
          },
        },
      });
      const listingObjectMetadata = objectsMetadata.find(
        (object) => object.nameSingular === LISTING_NAME_SINGULAR,
      );

      if (listingObjectMetadata) {
        listingObjectMetadataId = {
          objectMetadataId: listingObjectMetadata.id,
        };
      } else {
        const { data } = await createOneObjectMetadata({
          input: {
            labelSingular: LISTING_NAME_SINGULAR,
            labelPlural: LISTING_NAME_PLURAL,
            nameSingular: LISTING_NAME_SINGULAR,
            namePlural: LISTING_NAME_PLURAL,
            icon: 'IconBuildingSkyscraper',
          },
        });

        listingObjectMetadataId = {
          objectMetadataId: data.createOneObject.id,
        };
      }

      await performCreateManyOperation(
        LISTING_NAME_SINGULAR,
        LISTING_NAME_PLURAL,
        OBJECT_MODEL_COMMON_FIELDS,
        [firstListing, secondListing],
      );

      await performCreateManyOperation('person', 'people', PERSON_GQL_FIELDS, [
        firstPerson,
        secondPerson,
        thirdPerson,
      ]);

      await performCreateManyOperation(
        'apiKey',
        'apiKeys',
        OBJECT_MODEL_COMMON_FIELDS,
        [apiKey],
      );
    } catch (error) {
      // eslint-disable-next-line no-console
      console.log(error);
      throw new Error('beforeAll failed');
    }
  });

  afterAll(async () => {
    await makeGraphqlAPIRequest(
      destroyManyOperationFactory({
        objectMetadataSingularName: 'person',
        objectMetadataPluralName: 'people',
        gqlFields: PERSON_GQL_FIELDS,
        filter: {
          id: {
            in: [firstPerson.id, secondPerson.id, thirdPerson.id],
          },
        },
      }),
    ).catch((error) => {
      // eslint-disable-next-line no-console
      console.log(error);
    });

    await deleteOneObjectMetadata({
      input: { idToDelete: listingObjectMetadataId.objectMetadataId },
    }).catch((error) => {
      // eslint-disable-next-line no-console
      console.log(error);
    });

    await makeGraphqlAPIRequest(
      destroyOneOperationFactory({
        objectMetadataSingularName: 'apiKey',
        gqlFields: OBJECT_MODEL_COMMON_FIELDS,
        recordId: apiKey.id,
      }),
    ).catch((error) => {
      // eslint-disable-next-line no-console
      console.log(error);
    });
  });

  const testsUseCases: EachTestingContext<{
    input: SearchFactoryParams;
    eval: {
      definedRecordIds: string[];
      undefinedRecordIds: string[];
    };
  }>[] = [
    {
      title:
        'should return all records for "isSearchable:true" objects when no search input is provided',
      context: {
        input: {
          searchInput: '',
        },
        eval: {
          definedRecordIds: [firstListing.id, secondListing.id],
          undefinedRecordIds: [apiKey.id],
        },
      },
    },
    {
      title: 'should return filtered records when search input is provided',
      context: {
        input: {
          searchInput: 'searchInput1',
        },
        eval: {
          definedRecordIds: [firstPerson.id, firstListing.id],
          undefinedRecordIds: [secondPerson.id, secondListing.id],
        },
      },
    },
    {
      title: 'should return record from included Objects only',
      context: {
        input: {
          searchInput: '',
          includedObjectNameSingulars: [LISTING_NAME_SINGULAR],
        },
        eval: {
          definedRecordIds: [firstListing.id, secondListing.id],
          undefinedRecordIds: [firstPerson.id, secondPerson.id],
        },
      },
    },
    {
      title: 'should not return record from excludedObject',
      context: {
        input: {
          searchInput: '',
          excludedObjectNameSingulars: ['person'],
        },
        eval: {
          definedRecordIds: [firstListing.id, secondListing.id],
          undefinedRecordIds: [firstPerson.id, secondPerson.id],
        },
      },
    },
    {
      title: 'should return filtered records when filter is provided',
      context: {
        input: {
          searchInput: '',
          filter: {
            id: { eq: firstListing.id },
          },
        },
        eval: {
          definedRecordIds: [firstListing.id],
          undefinedRecordIds: [secondListing.id],
        },
      },
    },
  ];

  it.each(testsUseCases)('$title', async ({ context }) => {
    const graphqlOperation = searchFactory(context.input);
    const response = await makeGraphqlAPIRequest(graphqlOperation);

    expect(response.body.data).toBeDefined();
    expect(response.body.data.search).toBeDefined();

    const search = response.body.data.search;

    context.eval.definedRecordIds.length > 0
      ? expect(search).not.toHaveLength(0)
      : expect(search).toHaveLength(0);

    context.eval.definedRecordIds.forEach((recordId) => {
      expect(hasSearchRecord(search, recordId)).toBeTruthy();
    });

    context.eval.undefinedRecordIds.forEach((recordId) => {
      expect(hasSearchRecord(search, recordId)).toBeFalsy();
    });
  });
});
