import { Op } from 'sequelize';

import { getOrder, getWhereFromQuery, MetadataSchema, normalizeOrder, createListParamSchema } from '../../src/libs/api';
import type { OrderItem } from '../../src/store/models';

describe('getWhereFromQuery', () => {
  it('should correctly parse > operator', () => {
    const result = getWhereFromQuery('response_status > 200');
    expect(result).toEqual({ response_status: { [Op.gt]: 200 } });
  });

  it('should correctly parse < operator', () => {
    const result = getWhereFromQuery('retry_count < 5');
    expect(result).toEqual({ retry_count: { [Op.lt]: 5 } });
  });

  it('should correctly parse >= operator', () => {
    const result = getWhereFromQuery('response_status >= 200');
    expect(result).toEqual({ response_status: { [Op.gte]: 200 } });
  });

  it('should correctly parse <= operator', () => {
    const result = getWhereFromQuery('retry_count <= 5');
    expect(result).toEqual({ retry_count: { [Op.lte]: 5 } });
  });

  it('should correctly parse = operator', () => {
    const result = getWhereFromQuery("status = 'succeeded'");
    expect(result).toEqual({ status: 'succeeded' });
  });

  it('should correctly parse != operator', () => {
    const result = getWhereFromQuery("status != 'failed'");
    expect(result).toEqual({ status: { [Op.ne]: 'failed' } });
  });

  it('should correctly parse IS operator', () => {
    const result = getWhereFromQuery("status IS 'failed'");
    expect(result).toEqual({ status: 'failed' });
  });

  it('should correctly parse IS NOT operator', () => {
    const result = getWhereFromQuery("status IS NOT 'failed'");
    expect(result).toEqual({ status: { [Op.ne]: 'failed' } });
  });

  it('should correctly parse IS NOT operator: NULL', () => {
    const result = getWhereFromQuery('status IS NOT null');
    expect(result).toEqual({ status: { [Op.ne]: null } });
  });

  it('should correctly parse LIKE operator', () => {
    const result = getWhereFromQuery("status LIKE 'fail'");
    expect(result).toEqual({ status: { [Op.like]: 'fail' } });
  });

  it('should correctly parse NOT LIKE operator', () => {
    const result = getWhereFromQuery("status NOT LIKE 'fail'");
    expect(result).toEqual({ status: { [Op.notLike]: 'fail' } });
  });

  it('should correctly parse IN operator', () => {
    const result = getWhereFromQuery('status in (1,2,3)');
    expect(result).toEqual({ status: { [Op.in]: [1, 2, 3] } });
  });

  it('should correctly parse NOT IN operator', () => {
    const result = getWhereFromQuery('status NOT IN (1,2,3)');
    expect(result).toEqual({ status: { [Op.notIn]: [1, 2, 3] } });
  });

  it('should correctly parse BETWEEN operator', () => {
    const result = getWhereFromQuery('age BETWEEN 1 AND 3');
    expect(result).toEqual({ age: { [Op.between]: [1, 3] } });
  });

  it('should correctly parse NOT BETWEEN operator', () => {
    const result = getWhereFromQuery('age NOT BETWEEN 1 AND 3');
    expect(result).toEqual({ age: { [Op.notBetween]: [1, 3] } });
  });

  it('should correctly parse AND operator', () => {
    const result = getWhereFromQuery('response_status >= 200 AND response_status < 300');
    expect(result).toEqual({
      [Op.and]: [{ response_status: { [Op.gte]: 200 } }, { response_status: { [Op.lt]: 300 } }],
    });
  });

  it('should correctly parse AND operator with different field types', () => {
    const result = getWhereFromQuery("status = 'succeeded' AND response_status = 200");
    expect(result).toEqual({ [Op.and]: [{ status: 'succeeded' }, { response_status: 200 }] });
  });

  it('should correctly parse AND operator for BETWEEN condition', () => {
    const result = getWhereFromQuery('retry_count >= 1 AND retry_count <= 3');
    expect(result).toEqual({ [Op.and]: [{ retry_count: { [Op.gte]: 1 } }, { retry_count: { [Op.lte]: 3 } }] });
  });

  it('should correctly parse OR operator with different field types', () => {
    const result = getWhereFromQuery("status = 'succeeded' OR response_status = 200");
    expect(result).toEqual({ [Op.or]: [{ status: 'succeeded' }, { response_status: 200 }] });
  });

  it('should correctly parse OR operator with Boolean and Number fields', () => {
    const result = getWhereFromQuery('livemode = true OR retry_count > 2');
    expect(result).toEqual({ [Op.or]: [{ livemode: true }, { retry_count: { [Op.gt]: 2 } }] });
  });

  it('should correctly parse OR operator with boolean,number,string fields', () => {
    const result = getWhereFromQuery('livemode = true OR retry_count > 2 OR status = "succeeded"');
    expect(result).toEqual({
      [Op.or]: [{ [Op.or]: [{ livemode: true }, { retry_count: { [Op.gt]: 2 } }] }, { status: 'succeeded' }],
    });
  });

  it('should correctly parse OR operator with two String fields', () => {
    const result = getWhereFromQuery("event_id LIKE '%event%' OR status != 'failed'");
    expect(result).toEqual({ [Op.or]: [{ event_id: { [Op.like]: '%event%' } }, { status: { [Op.ne]: 'failed' } }] });
  });

  it('should throw an error when the query is empty', () => {
    expect(() => getWhereFromQuery('')).toThrow(/empty query/);
  });

  it('should correctly parse AND operator x3 for metadata', () => {
    const result = getWhereFromQuery(
      // 'metadata.key IS NULL AND metadata.nftId="3" AND status IN ("succeeded","failed")'
      'metadata.key IS NULL AND metadata.nftId="3" AND metadata.appId="4"'
    );
    expect(result).toEqual({
      [Op.and]: [
        {
          [Op.and]: [{ 'metadata.key': null }, { 'metadata.nftId': '3' }],
        },
        { 'metadata.appId': '4' },
      ],
    });
  });

  it('should correctly parse AND + IN operator for metadata', () => {
    const result = getWhereFromQuery(
      'metadata.key IS NULL AND metadata.nftId="3" AND status IN ("succeeded","failed")'
    );
    expect(result).toEqual({
      [Op.and]: [
        {
          [Op.and]: [{ 'metadata.key': null }, { 'metadata.nftId': '3' }],
        },
        { status: { [Op.in]: ['succeeded', 'failed'] } },
      ],
    });
  });

  // eslint-disable-next-line jest/no-disabled-tests
  it.skip('should throw an error when an unsupported operator is used', () => {
    expect(() => getWhereFromQuery('status === "failed"')).toThrow(/Operator not supported/);
  });
});

describe('MetadataSchema', () => {
  it('should validate an object with string keys and custom validated values', () => {
    const data = {
      key1: 'value1',
      key2: 123,
    };
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeUndefined();
  });

  it('should validate an array of objects with key and custom validated value', () => {
    const data = [
      { key: 'key1', value: 'value1' },
      { key: 'key2', value: 123 },
    ];
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeUndefined();
  });

  it('should invalidate an object with a key longer than 64 characters', () => {
    const data = {
      ['a'.repeat(41)]: 'value1',
    };
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeDefined();
    expect(error?.details?.[0]?.message).toMatch(/"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not allowed/);
  });

  it('should invalidate an array with an object missing the key field', () => {
    const data = [{ value: 'value1' }];
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeDefined();
    expect(error?.details?.[0]?.message).toMatch(/"\[0\]\.key" is required/);
  });

  it('should invalidate an array with an object missing the value field', () => {
    const data = [{ key: 'key1' }];
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeDefined();
    expect(error?.details?.[0]?.message).toMatch(/"\[0\]\.value" is required/);
  });

  it('should invalidate an object with a value longer than 256 characters', () => {
    const data = {
      key1: 'a'.repeat(257),
    };
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeDefined();
    expect(error?.details?.[0]?.message).toMatch(/Metadata value should be less than 256 characters/);
  });
  it('should validate when data is null', () => {
    const data = null;
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeUndefined();
  });

  it('should validate when data is empty object', () => {
    const data = {};
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeUndefined();
  });

  it('should validate when data is empty array', () => {
    const data: any[] = [];
    const { error } = MetadataSchema.validate(data);
    expect(error).toBeUndefined();
  });
});

describe('normalizeOrder', () => {
  it('should return default order when input is undefined', () => {
    const result = normalizeOrder(undefined);
    expect(result).toEqual([['created_at', 'DESC']]);
  });

  it('should handle string input', () => {
    const result = normalizeOrder('name:asc');
    expect(result).toEqual([
      ['name', 'ASC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should handle string array input', () => {
    const result = normalizeOrder(['name:asc', 'age:desc']);
    expect(result).toEqual([
      ['name', 'ASC'],
      ['age', 'DESC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should handle tuple array input', () => {
    const result = normalizeOrder([
      ['name', 'ASC'],
      ['age', 'DESC'],
    ]);
    expect(result).toEqual([
      ['name', 'ASC'],
      ['age', 'DESC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should merge with default order correctly', () => {
    const defaultOrder: OrderItem[] = [
      ['updated_at', 'DESC'],
      ['created_at', 'ASC'],
    ];
    const result = normalizeOrder('name:asc', defaultOrder);
    expect(result).toEqual([
      ['name', 'ASC'],
      ['updated_at', 'DESC'],
      ['created_at', 'ASC'],
    ]);
  });

  it('should not duplicate fields from default order', () => {
    const defaultOrder: OrderItem[] = [
      ['name', 'DESC'],
      ['created_at', 'ASC'],
    ];
    const result = normalizeOrder('name:asc', defaultOrder);
    expect(result).toEqual([
      ['name', 'ASC'],
      ['created_at', 'ASC'],
    ]);
  });
});

describe('getOrder', () => {
  it('should handle query.order parameter', () => {
    const result = getOrder({ order: 'name:asc' });
    expect(result).toEqual([
      ['name', 'ASC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should handle query.o parameter', () => {
    const result = getOrder({ o: 'desc' }, [
      ['name', 'ASC'],
      ['age', 'ASC'],
    ]);
    expect(result).toEqual([
      ['name', 'DESC'],
      ['age', 'DESC'],
    ]);
  });

  it('should handle query.o parameter without defaultOrder', () => {
    const result = getOrder({ o: 'asc' });
    expect(result).toEqual([['created_at', 'ASC']]);
  });

  it('should prioritize query.order over query.o', () => {
    const result = getOrder({ order: 'name:asc', o: 'desc' });
    expect(result).toEqual([
      ['name', 'ASC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should return default order when no parameters provided', () => {
    const result = getOrder({});
    expect(result).toEqual([['created_at', 'DESC']]);
  });

  it('should return custom default order when provided', () => {
    const defaultOrder: OrderItem[] = [
      ['updated_at', 'DESC'],
      ['name', 'ASC'],
    ];
    const result = getOrder({}, defaultOrder);
    expect(result).toEqual(defaultOrder);
  });

  it('should handle complex ordering with array input', () => {
    const result = getOrder({ order: ['name:asc', 'age:desc'] });
    expect(result).toEqual([
      ['name', 'ASC'],
      ['age', 'DESC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should handle tuple array input', () => {
    const result = getOrder({
      order: [
        ['name', 'ASC'],
        ['age', 'DESC'],
      ],
    });
    expect(result).toEqual([
      ['name', 'ASC'],
      ['age', 'DESC'],
      ['created_at', 'DESC'],
    ]);
  });

  it('should merge order fields correctly with defaults', () => {
    const defaultOrder: OrderItem[] = [
      ['status', 'DESC'],
      ['created_at', 'DESC'],
    ];
    const result = getOrder({ order: 'name:asc' }, defaultOrder);
    expect(result).toEqual([
      ['name', 'ASC'],
      ['status', 'DESC'],
      ['created_at', 'DESC'],
    ]);
  });
});

describe('createListParamSchema', () => {
  const schema = createListParamSchema({});

  describe('order validation', () => {
    // Valid format validation
    it('should validate valid string order format', () => {
      const { error } = schema.validate({ order: 'name:asc' });
      expect(error).toBeUndefined();
    });

    it('should validate valid array of strings order format', () => {
      const { error } = schema.validate({ order: ['name:asc', 'age:desc'] });
      expect(error).toBeUndefined();
    });

    it('should validate valid tuple array format', () => {
      const { error } = schema.validate({
        order: [
          ['name', 'ASC'],
          ['age', 'DESC'],
        ],
      });
      expect(error).toBeUndefined();
    });

    // Invalid format validation
    it('should reject invalid format with detailed error message', () => {
      const { error } = schema.validate({ order: 'nameasc' });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toMatch(/Must use ":" to separate field and direction/);
    });

    // Field name validation
    it('should reject invalid field characters with detailed error message', () => {
      const invalidInputs = ['@name:asc', 'field-name:asc', 'field name:asc', ':asc'];

      invalidInputs.forEach((input) => {
        const { error } = schema.validate({ order: input });
        expect(error).toBeDefined();
        expect((error as any).details[0].message).toMatch(
          /Field name can only contain letters, numbers, dots and underscores/
        );
      });
    });

    // Direction validation
    it('should reject invalid direction with detailed error message', () => {
      const invalidDirections = ['name:up', 'name:', 'name:invalid'];

      invalidDirections.forEach((input) => {
        const { error } = schema.validate({ order: input });
        expect(error).toBeDefined();
        expect((error as any).details[0].message).toMatch(/Direction must be either "asc" or "desc"/);
      });
    });

    // Array format validation
    it('should reject invalid array items with detailed error message', () => {
      const invalidArrays = [['name:up'], ['@name:asc'], ['name:'], ['nameasc']];

      invalidArrays.forEach((input) => {
        const { error } = schema.validate({ order: input });
        expect(error).toBeDefined();
      });
    });

    // Tuple format validation
    it('should reject invalid tuple format with detailed error message', () => {
      const invalidTuples = [[['@name', 'ASC']], [['name', 'UP']], [['name']], [['name', 'ASC', 'extra']]];

      invalidTuples.forEach((input, index) => {
        const { error } = schema.validate({ order: input });
        expect(error).toBeDefined();

        if (index === 0) {
          expect((error as any).details[0].message).toMatch(
            /Field name can only contain letters, numbers, dots and underscores/
          );
        } else if (index === 1) {
          expect((error as any).details[0].message).toMatch(/Direction must be either "asc" or "desc"/);
        } else if (index === 2 || index === 3) {
          // Test for array length validation
          expect((error as any).details[0].message).toMatch(
            /Each tuple must contain exactly 2 elements|Each tuple must contain \[field, direction\]/
          );
        }
      });
    });

    // Case variation validation
    it('should accept all valid case variations', () => {
      const validInputs = [
        'name:asc',
        'name:ASC',
        'name:desc',
        'name:DESC',
        ['name:asc'],
        [['name', 'asc']],
        [['name', 'ASC']],
        [
          ['name', 'ASC'],
          ['age', 'desc'],
        ],
      ];

      validInputs.forEach((input) => {
        const { error } = schema.validate({ order: input });
        expect(error).toBeUndefined();
      });
    });

    // Testing specific error messages for each validation rule
    it('should validate field name format', () => {
      const { error } = schema.validate({ order: '@invalid:asc' });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toBe(
        'Field name can only contain letters, numbers, dots and underscores'
      );
    });

    it('should validate separator', () => {
      const { error } = schema.validate({ order: 'name-asc' });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toBe('Must use ":" to separate field and direction');
    });

    it('should validate direction', () => {
      const { error } = schema.validate({ order: 'name:up' });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toBe('Direction must be either "asc" or "desc"');
    });

    it('should validate tuple format length', () => {
      const { error } = schema.validate({
        order: [['name', 'ASC', 'extra']],
      });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toBe('Each tuple must contain exactly 2 elements');
    });

    // Object format should not be supported
    it('should reject object format', () => {
      const { error } = schema.validate({
        order: { name: 'asc', age: 'desc' },
      });
      expect(error).toBeDefined();
      expect((error as any).details[0].message).toMatch(/Invalid order format. Must be one of:/);
    });
  });
});
