import {
  isVariable,
  isUseI18NSetter,
  convertToI18NObject,
  isString,
  waitForThing,
  arrShallowEquals,
  isFromVC,
  executePendingFn,
  compatStage,
  invariant,
  isRegExp,
  shouldUseVariableSetter,
} from '../../src/misc';
import { IPublicModelComponentMeta } from '@felce/lowcode-types';

describe('isVariable', () => {
  it('should return true for a variable object', () => {
    const variable = { type: 'variable', variable: 'foo', value: 'bar' };
    const result = isVariable(variable);
    expect(result).toBe(true);
  });

  it('should return false for non-variable objects', () => {
    const obj = { type: 'object' };
    const result = isVariable(obj);
    expect(result).toBe(false);
  });
});

describe('isUseI18NSetter', () => {
  it('should return true for a property with I18nSetter', () => {
    const prototype = {
      options: {
        configure: [{ name: 'propName', setter: { type: { displayName: 'I18nSetter' } } }],
      },
    };
    const propName = 'propName';
    const result = isUseI18NSetter(prototype, propName);
    expect(result).toBe(true);
  });

  it('should return false for a property without I18nSetter', () => {
    const prototype = {
      options: {
        configure: [{ name: 'propName', setter: { type: { displayName: 'OtherSetter' } } }],
      },
    };
    const propName = 'propName';
    const result = isUseI18NSetter(prototype, propName);
    expect(result).toBe(false);
  });
});

describe('convertToI18NObject', () => {
  it('should return the input if it is already an I18N object', () => {
    const i18nObject = { type: 'i18n', use: 'en', en: 'Hello' };
    const result = convertToI18NObject(i18nObject);
    expect(result).toEqual(i18nObject);
  });

  it('should convert a string to an I18N object', () => {
    const inputString = 'Hello';
    const result = convertToI18NObject(inputString);
    const expectedOutput = { type: 'i18n', use: 'zh-CN', 'zh-CN': inputString };
    expect(result).toEqual(expectedOutput);
  });
});

describe('isString', () => {
  it('should return true for a string', () => {
    const stringValue = 'Hello, world!';
    const result = isString(stringValue);
    expect(result).toBe(true);
  });

  it('should return true for an empty string', () => {
    const emptyString = '';
    const result = isString(emptyString);
    expect(result).toBe(true);
  });

  it('should return false for a number', () => {
    const numberValue = 42; // Not a string
    const result = isString(numberValue);
    expect(result).toBe(false);
  });

  it('should return false for an object', () => {
    const objectValue = { key: 'value' }; // Not a string
    const result = isString(objectValue);
    expect(result).toBe(false);
  });

  it('should return false for null', () => {
    const result = isString(null);
    expect(result).toBe(false);
  });

  it('should return false for undefined', () => {
    const undefinedValue = undefined;
    const result = isString(undefinedValue);
    expect(result).toBe(false);
  });

  it('should return false for a boolean', () => {
    const booleanValue = true; // Not a string
    const result = isString(booleanValue);
    expect(result).toBe(false);
  });
});

describe('waitForThing', () => {
  it('should resolve immediately if the thing is available', async () => {
    const obj = { prop: 'value' };
    const path = 'prop';
    const result = await waitForThing(obj, path);
    expect(result).toBe('value');
  });

  it('should resolve after a delay if the thing becomes available', async () => {
    const obj = { prop: undefined };
    const path = 'prop';
    const delay = 100; // Adjust the delay as needed
    setTimeout(() => {
      obj.prop = 'value';
    }, delay);

    const result = await waitForThing(obj, path);
    expect(result).toBe('value');
  });
});

describe('arrShallowEquals', () => {
  it('should return true for two empty arrays', () => {
    const arr1 = [];
    const arr2 = [];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(true);
  });

  it('should return true for two arrays with the same elements in the same order', () => {
    const arr1 = [1, 2, 3];
    const arr2 = [1, 2, 3];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(true);
  });

  it('should return true for two arrays with the same elements in a different order', () => {
    const arr1 = [1, 2, 3];
    const arr2 = [3, 2, 1];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(true);
  });

  it('should return false for two arrays with different lengths', () => {
    const arr1 = [1, 2, 3];
    const arr2 = [1, 2];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(false);
  });

  it('should return false for one array and a non-array', () => {
    const arr1 = [1, 2, 3];
    const nonArray = 'not an array';
    const result = arrShallowEquals(arr1, nonArray);
    expect(result).toBe(false);
  });

  it('should return false for two arrays with different elements', () => {
    const arr1 = [1, 2, 3];
    const arr2 = [3, 4, 5];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(false);
  });

  it('should return true for arrays with duplicate elements', () => {
    const arr1 = [1, 2, 2, 3];
    const arr2 = [2, 3, 3, 1];
    const result = arrShallowEquals(arr1, arr2);
    expect(result).toBe(true);
  });
});

describe('isFromVC', () => {
  it('should return true when advanced configuration is present', () => {
    // Create a mock meta object with advanced configuration
    const meta: IPublicModelComponentMeta = {
      getMetadata: () => ({ configure: { advanced: true } }),
    };

    const result = isFromVC(meta);

    expect(result).toBe(true);
  });

  it('should return false when advanced configuration is not present', () => {
    // Create a mock meta object without advanced configuration
    const meta: IPublicModelComponentMeta = {
      getMetadata: () => ({ configure: { advanced: false } }),
    };

    const result = isFromVC(meta);

    expect(result).toBe(false);
  });

  it('should return false when meta is undefined', () => {
    const meta: IPublicModelComponentMeta | undefined = undefined;

    const result = isFromVC(meta);

    expect(result).toBe(false);
  });

  it('should return false when meta does not have configure information', () => {
    // Create a mock meta object without configure information
    const meta: IPublicModelComponentMeta = {
      getMetadata: () => ({}),
    };

    const result = isFromVC(meta);

    expect(result).toBe(false);
  });

  it('should return false when configure.advanced is not present', () => {
    // Create a mock meta object with incomplete configure information
    const meta: IPublicModelComponentMeta = {
      getMetadata: () => ({ configure: {} }),
    };

    const result = isFromVC(meta);

    expect(result).toBe(false);
  });
});

describe('executePendingFn', () => {
  it('should execute the provided function after the specified timeout', async () => {
    // Mock the function to execute
    const fn = vi.fn();

    // Call executePendingFn with the mocked function and a short timeout
    executePendingFn(fn, 100);

    // Ensure the function has not been called immediately
    expect(fn).not.toHaveBeenCalled();

    // Wait for the specified timeout
    await new Promise((resolve) => setTimeout(resolve, 100));

    // Ensure the function has been called after the timeout
    expect(fn).toHaveBeenCalled();
  });

  it('should execute the provided function with a default timeout if not specified', async () => {
    // Mock the function to execute
    const fn = vi.fn();

    // Call executePendingFn with the mocked function without specifying a timeout
    executePendingFn(fn);

    // Ensure the function has not been called immediately
    expect(fn).not.toHaveBeenCalled();

    // Wait for the default timeout (2000 milliseconds)
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Ensure the function has been called after the default timeout
    expect(fn).toHaveBeenCalled();
  });
});

describe('compatStage', () => {
  it('should convert a number to an enum stage', () => {
    const result = compatStage(3);
    expect(result).toBe('save');
  });

  it('should warn about the deprecated usage', () => {
    const warnSpy = vi.spyOn(console, 'warn');
    const result = compatStage(2);
    expect(result).toBe('serilize');
    expect(warnSpy).toHaveBeenCalledWith(
      'stage 直接指定为数字的使用方式已经过时，将在下一版本移除，请直接使用 IPublicEnumTransformStage.Render|Serilize|Save|Clone|Init|Upgrade',
    );
    warnSpy.mockRestore();
  });

  it('should return the enum stage if it is already an enum', () => {
    const result = compatStage('render');
    expect(result).toBe('render');
  });
});

describe('invariant', () => {
  it('should not throw an error if the check is true', () => {
    expect(() => invariant(true, 'Test invariant', 'thing')).not.toThrow();
  });

  it('should throw an error if the check is false', () => {
    expect(() => invariant(false, 'Test invariant', 'thing')).toThrowError(
      "Invariant failed: Test invariant in 'thing'",
    );
  });
});

describe('isRegExp', () => {
  it('should return true for a valid RegExp', () => {
    const regex = /test/;
    const result = isRegExp(regex);
    expect(result).toBe(true);
  });

  it('should return false for a non-RegExp object', () => {
    const nonRegExp = { test: /test/ };
    const result = isRegExp(nonRegExp);
    expect(result).toBe(false);
  });

  it('should return false for null', () => {
    const result = isRegExp(null);
    expect(result).toBe(false);
  });
});

it('shouldUseVariableSetter', () => {
  expect(shouldUseVariableSetter(false, true)).toBeFalsy();
  expect(shouldUseVariableSetter(true, true)).toBeTruthy();
  expect(shouldUseVariableSetter(true, false)).toBeTruthy();
  expect(shouldUseVariableSetter(undefined, false)).toBeFalsy();
  expect(shouldUseVariableSetter(undefined, true)).toBeTruthy();
});
