import { Op } from 'sequelize';

import { getWhereFromKvQuery } from '../../src/libs/api';
import dayjs from '../../src/libs/dayjs';
import {
  createCodeGenerator,
  createIdGenerator,
  formatAmountPrecisionLimit,
  formatMetadata,
  getDataObjectFromQuery,
  getNextRetry,
  tryWithTimeout,
  getSubscriptionNotificationCustomActions,
  isUserInBlocklist,
  api,
  md5,
  safeJsonParse,
  getExplorerLink,
  resolveAddressChainTypes,
  formatCurrencyInfo,
  getExplorerTxUrl,
} from '../../src/libs/util';
import type { Subscription, PaymentMethod, PaymentCurrency } from '../../src/store/models';

import { blocklet } from '../../src/libs/auth';
import logger from '../../src/libs/logger';

describe('createIdGenerator', () => {
  it('should return a function that generates an ID with the specified prefix and size', () => {
    const generateId = createIdGenerator('test', 10);
    const id = generateId();
    expect(id.startsWith('test_')).toBe(true);
    expect(id.length).toBe(15); // 10 for ID, 4 for prefix, 1 for underscore
  });

  it('should return a function that generates an ID with the specified prefix and default size when size is not provided', () => {
    const generateId = createIdGenerator('test');
    const id = generateId();
    expect(id.startsWith('test_')).toBe(true);
    expect(id.length).toBe(29); // 24 for ID, 4 for prefix, 1 for underscore
  });

  it('should return a function that generates an ID without a prefix when prefix is not provided', () => {
    const generateId = createIdGenerator('', 10);
    const id = generateId();
    expect(id.startsWith('_')).toBe(false);
    expect(id.length).toBe(10);
  });
});

describe('createCodeGenerator', () => {
  it('should return a function that generates a code with the specified prefix and size', () => {
    const generateCode = createCodeGenerator('test', 10);
    const code = generateCode();
    expect(code.startsWith('test_')).toBe(true);
    expect(code.length).toBe(15); // 10 for code, 4 for prefix, 1 for underscore
  });

  it('should return a function that generates a code with the specified prefix and default size when size is not provided', () => {
    const generateCode = createCodeGenerator('test');
    const code = generateCode();
    expect(code.startsWith('test_')).toBe(true);
    expect(code.length).toBe(29); // 24 for code, 4 for prefix, 1 for underscore
  });

  it('should return a function that generates a code without a prefix when prefix is not provided', () => {
    const generateCode = createCodeGenerator('', 10);
    const code = generateCode();
    expect(code.startsWith('_')).toBe(false);
    expect(code.length).toBe(10);
  });
});

describe('formatMetadata', () => {
  it('should return an empty object when metadata is undefined', () => {
    const result = formatMetadata(undefined);
    expect(result).toEqual({});
  });

  it('should return a formatted object when metadata is an array', () => {
    const metadata = [
      { key: 'key1', value: 'value1' },
      { key: 'key2', value: '' },
      { key: 'key3', value: 'value3' },
    ];
    const result = formatMetadata(metadata);
    expect(result).toEqual({ key1: 'value1', key3: 'value3' });
  });

  it('should return a formatted object when metadata is an object', () => {
    const metadata = {
      key1: 'value1',
      key2: '',
      key3: 'value3',
    };
    const result = formatMetadata(metadata);
    expect(result).toEqual({ key1: 'value1', key3: 'value3' });
  });
});

describe('tryWithTimeout', () => {
  it('should resolve with the result of the async function when it completes before the timeout', async () => {
    const asyncFn = jest.fn().mockResolvedValue('result');
    const result = await tryWithTimeout(asyncFn, 100);
    expect(result).toBe('result');
    expect(asyncFn).toHaveBeenCalled();
  });

  it('should reject with a timeout error when the async function does not complete before the timeout', async () => {
    // eslint-disable-next-line no-promise-executor-return
    const asyncFn = jest.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 200)));
    await expect(tryWithTimeout(asyncFn, 100)).rejects.toThrow('Operation timed out after 100 ms');
    expect(asyncFn).toHaveBeenCalled();
  });

  it('should reject with the error of the async function when it rejects', async () => {
    const asyncFn = jest.fn().mockRejectedValue(new Error('error'));
    await expect(tryWithTimeout(asyncFn, 100)).rejects.toThrow('error');
    expect(asyncFn).toHaveBeenCalled();
  });

  it('should throw an error when the provided asyncFn is not a function', () => {
    // @ts-ignore
    expect(() => tryWithTimeout(null)).toThrow('Must provide a valid asyncFn function');
  });
});

describe('getNextRetry', () => {
  it('should return the current time plus 1 second when retry count is 0', () => {
    const now = dayjs().unix();
    const result = getNextRetry(0);
    expect(result).toBe(now + 1);
  });

  it('should return the current time plus 2 seconds when retry count is 1', () => {
    const now = dayjs().unix();
    const result = getNextRetry(1);
    expect(result).toBe(now + 2);
  });

  it('should return the current time plus 4 seconds when retry count is 2', () => {
    const now = dayjs().unix();
    const result = getNextRetry(2);
    expect(result).toBe(now + 4);
  });

  it('should return the current time plus 8 seconds when retry count is 3', () => {
    const now = dayjs().unix();
    const result = getNextRetry(3);
    expect(result).toBe(now + 8);
  });
});

describe('getDataObjectFromQuery', () => {
  it('should return an empty object when the query object is empty', () => {
    const result = getDataObjectFromQuery({});
    expect(result).toEqual({});
  });

  it('should return an object with metadata when the query object contains metadata keys', () => {
    const query = {
      'metadata.key1': 'value1',
      'metadata.key2': 'value2',
    };
    const result = getDataObjectFromQuery(query);
    expect(result).toEqual({
      key1: 'value1',
      key2: 'value2',
    });
  });

  it('should return an object without non-metadata keys when the query object contains non-metadata keys', () => {
    const query = {
      'metadata.key1': 'value1',
      key2: 'value2',
    };
    const result = getDataObjectFromQuery(query);
    expect(result).toEqual({
      key1: 'value1',
    });
  });

  it('should ignore metadata keys with undefined or null values', () => {
    const query = {
      'metadata.key1': 'value1',
      'metadata.key2': undefined,
      'metadata.key3': null,
    };
    const result = getDataObjectFromQuery(query);
    expect(result).toEqual({
      key1: 'value1',
    });
  });
});

describe('getWhereFromKvQuery', () => {
  it('should return an empty object when the q string is empty', () => {
    const result = getWhereFromKvQuery('');
    expect(result).toEqual({});
  });

  it('should return a common object when the q string without like', () => {
    const q = 'status:succeeded xxx:yyy';
    const result = getWhereFromKvQuery(q);
    expect(result).toEqual({
      status: 'succeeded',
      xxx: 'yyy',
    });
  });

  it('should return a likes object when the q string with like', () => {
    const q = 'like-status:succ like-number:1533';
    const result = getWhereFromKvQuery(q);
    expect(result).toEqual({
      [Op.or]: [{ status: { [Op.like]: '%succ%' } }, { number: { [Op.like]: '%1533%' } }],
    });
  });
});

describe('formatAmountPrecisionLimit', () => {
  it('should return an empty string if the decimal places are within the precision limit', () => {
    const result = formatAmountPrecisionLimit('123.456', 6);
    expect(result).toBe('');
  });

  it('should return an error message if the decimal places exceed the precision limit', () => {
    const result = formatAmountPrecisionLimit('123.456789', 5);
    expect(result).toBe('Amount decimal places must be less than or equal to 5');
  });

  it('should use the default precision of 6 if not provided', () => {
    const result = formatAmountPrecisionLimit('123.456789');
    expect(result).toBe('');
  });

  it('should use the provided amountLabel in the error message', () => {
    const result = formatAmountPrecisionLimit('123.456789', 5, 'Price');
    expect(result).toBe('Price decimal places must be less than or equal to 5');
  });

  it('should return an empty string if there are no decimal places', () => {
    const result = formatAmountPrecisionLimit('123');
    expect(result).toBe('');
  });

  it('should handle an empty string as amount', () => {
    const result = formatAmountPrecisionLimit('');
    expect(result).toBe('');
  });

  it('should handle an amount with no decimal part', () => {
    const result = formatAmountPrecisionLimit('123.');
    expect(result).toBe('');
  });
});

describe('getSubscriptionNotificationCustomActions', () => {
  const mockSubscription: Partial<Subscription> = {
    service_actions: [
      {
        type: 'notification',
        triggerEvents: ['customer.subscription.started', 'customer.subscription.renewed'],
        name: 'Action 1',
        text: { en: 'Action 1 Text', zh: '操作1文本' },
        link: 'https://example.com/action1',
        color: 'blue',
        variant: 'outlined',
      },
      {
        type: 'notification',
        triggerEvents: ['customer.subscription.renewed', 'customer.subscription.upgraded'],
        name: 'Action 2',
        text: { en: 'Action 2 Text', zh: '操作2文本' },
        link: 'https://example.com/action2',
        color: 'green',
        variant: 'contained',
      },
      {
        type: 'other',
        triggerEvents: ['customer.subscription.started'],
        name: 'Action 3',
        text: { en: 'Action 3 Text', zh: '操作3文本' },
        link: 'https://example.com/action3',
        color: 'red',
        variant: 'text',
      },
    ],
  };

  it('should return an empty array if subscription is null', () => {
    const result = getSubscriptionNotificationCustomActions(null as any, 'customer.subscription.started', 'en');
    expect(result).toEqual([]);
  });

  it('should return an empty array if service_actions is empty', () => {
    const result = getSubscriptionNotificationCustomActions(
      { service_actions: [] } as any,
      'customer.subscription.started',
      'en'
    );
    expect(result).toEqual([]);
  });

  it('should filter actions based on event type and return formatted actions with color and variant', () => {
    const result = getSubscriptionNotificationCustomActions(
      mockSubscription as any,
      'customer.subscription.renewed',
      'en'
    );
    expect(result).toEqual([
      {
        name: 'Action 1',
        title: 'Action 1 Text',
        link: 'https://example.com/action1',
      },
      {
        name: 'Action 2',
        title: 'Action 2 Text',
        link: 'https://example.com/action2',
      },
    ]);
  });

  it('should return actions with correct locale, color, and variant', () => {
    const result = getSubscriptionNotificationCustomActions(
      mockSubscription as any,
      'customer.subscription.renewed',
      'zh'
    );
    expect(result).toEqual([
      {
        name: 'Action 1',
        title: '操作1文本',
        link: 'https://example.com/action1',
      },
      {
        name: 'Action 2',
        title: '操作2文本',
        link: 'https://example.com/action2',
      },
    ]);
  });

  it('should not return actions of non-notification type', () => {
    const result = getSubscriptionNotificationCustomActions(
      mockSubscription as any,
      'customer.subscription.started',
      'en'
    );
    expect(result).toEqual([
      {
        name: 'Action 1',
        title: 'Action 1 Text',
        link: 'https://example.com/action1',
      },
    ]);
  });

  it('should return an empty array if no matching actions found', () => {
    const result = getSubscriptionNotificationCustomActions(
      mockSubscription as any,
      'customer.subscription.deleted',
      'en'
    );
    expect(result).toEqual([]);
  });

  it('should handle all valid NotificationActionEvents', () => {
    const allEvents = [
      'customer.subscription.started',
      'customer.subscription.renewed',
      'customer.subscription.renew_failed',
      'refund.succeeded',
      'subscription.stake.slash.succeeded',
      'customer.subscription.trial_will_end',
      'customer.subscription.trial_start',
      'customer.subscription.upgraded',
      'customer.subscription.will_renew',
      'customer.subscription.will_canceled',
      'customer.subscription.deleted',
    ];

    allEvents.forEach((event) => {
      const result = getSubscriptionNotificationCustomActions(mockSubscription as any, event, 'en');
      expect(Array.isArray(result)).toBeTruthy();
    });
  });
});

describe('isUserInBlocklist', () => {
  let mockAxios: any;
  let mockBlocklet: any;
  let mockLogger: any;

  beforeEach(() => {
    mockAxios = jest.spyOn(api, 'get').mockImplementation();

    mockBlocklet = jest.spyOn(blocklet, 'getUser').mockImplementation();

    mockLogger = {
      warn: jest.spyOn(logger, 'warn').mockImplementation(),
      info: jest.spyOn(logger, 'info').mockImplementation(),
      error: jest.spyOn(logger, 'error').mockImplementation(),
    };
  });

  afterEach(() => {
    jest.clearAllMocks();
    jest.resetAllMocks();
    jest.restoreAllMocks();
  });

  it('should return true when user DID is in blocklist', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:user:123', 'did:user:456'] });
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        connectedAccounts: [{ provider: 'wallet', did: userDid }],
      },
    });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(true);
    expect(mockAxios).toHaveBeenCalledWith('https://api.example.com/blocked');
    expect(mockBlocklet).toHaveBeenCalledWith(userDid);
    expect(mockLogger.info).toHaveBeenCalled();
  });

  it('should return true when user wallet DID is in blocklist', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const walletDid = 'did:wallet:456';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:wallet:456', 'did:user:789'] });
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        connectedAccounts: [{ provider: 'wallet', did: walletDid }],
      },
    });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(true);
    expect(mockAxios).toHaveBeenCalledWith('https://api.example.com/blocked');
    expect(mockBlocklet).toHaveBeenCalledWith(userDid);
  });

  it('should return false when user is not in blocklist', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const walletDid = 'did:wallet:456';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:user:789', 'did:wallet:789'] });
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        connectedAccounts: [{ provider: 'wallet', did: walletDid }],
      },
    });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(false);
    expect(mockAxios).toHaveBeenCalledWith('https://api.example.com/blocked');
    expect(mockBlocklet).toHaveBeenCalledWith(userDid);
  });

  it('should handle API host with trailing slash', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com/',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:user:789'] });
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        connectedAccounts: [{ provider: 'wallet', did: userDid }],
      },
    });

    // Execute
    await isUserInBlocklist(userDid, paymentMethod);

    // Verify - 确保调用了正确的 URL
    expect(mockAxios).toHaveBeenCalledWith('https://api.example.com/blocked');
  });

  it('should return false when API host is not found', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {},
    } as PaymentMethod;

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(false);
    expect(mockLogger.warn).toHaveBeenCalled();
    expect(mockAxios).not.toHaveBeenCalled();
  });

  it('should return false when blocklist is empty', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    // 先模拟用户信息获取
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        connectedAccounts: [{ provider: 'wallet', did: userDid }],
      },
    });

    // 再模拟空黑名单
    mockAxios.mockResolvedValueOnce({ data: [] });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(false);
    expect(mockBlocklet).toHaveBeenCalled(); // 现在应该调用了用户信息获取
  });

  it('should return false when user is not found', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:user:123'] });
    mockBlocklet.mockResolvedValueOnce({ user: null });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(false);
    expect(mockLogger.error).toHaveBeenCalled();
  });

  it('should return false when API request fails', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockRejectedValueOnce(new Error('Network error'));

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

    // Verify
    expect(result).toBe(false);
    expect(mockLogger.error).toHaveBeenCalled();
  });

  it('should handle extraConfigs for connected accounts', async () => {
    // Setup mocks
    const userDid = 'did:user:123';
    const walletDid = 'did:wallet:456';
    const paymentMethod = {
      id: 'pm_123',
      type: 'arcblock',
      settings: {
        arcblock: {
          api_host: 'https://api.example.com',
        },
      },
    } as PaymentMethod;

    mockAxios.mockResolvedValueOnce({ data: ['did:wallet:456'] });
    mockBlocklet.mockResolvedValueOnce({
      user: {
        did: userDid,
        extraConfigs: {
          connectedAccounts: [{ provider: 'wallet', did: walletDid }],
        },
      },
    });

    // Execute
    const result = await isUserInBlocklist(userDid, paymentMethod);

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

describe('md5', () => {
  it('should generate a consistent hash for the same input', () => {
    const input = 'test-string';
    const result1 = md5(input);
    const result2 = md5(input);
    expect(result1).toBe(result2);
  });

  it('should generate different hashes for different inputs', () => {
    const result1 = md5('test-string-1');
    const result2 = md5('test-string-2');
    expect(result1).not.toBe(result2);
  });

  it('should return a 32 character hexadecimal string', () => {
    const result = md5('test-string');
    expect(result).toMatch(/^[0-9a-f]{32}$/);
  });
});

describe('safeJsonParse', () => {
  it('should parse valid JSON strings', () => {
    const input = '{"key":"value","number":123}';
    const result = safeJsonParse(input, null);
    expect(result).toEqual({ key: 'value', number: 123 });
  });

  it('should return the default value when parsing invalid JSON', () => {
    const input = '{invalid json}';
    const defaultValue = { default: true };
    const result = safeJsonParse(input, defaultValue);
    expect(result).toEqual(defaultValue);
  });

  it('should return the input when parsing fails and no default value is provided', () => {
    const input = '{invalid json}';
    const result = safeJsonParse(input, undefined);
    expect(result).toBe(input);
  });
});

describe('getExplorerLink', () => {
  it('should return undefined when chainHost is not provided', () => {
    const result = getExplorerLink({
      type: 'asset',
      did: 'did:example:123',
      chainHost: undefined,
    });
    expect(result).toBeUndefined();
  });

  it('should construct a proper asset URL with the given parameters', () => {
    const result = getExplorerLink({
      type: 'asset',
      did: 'did:example:123',
      chainHost: 'https://chain.example.com',
    });
    expect(result).toBe('https://chain.example.com/explorer/assets/did:example:123');
  });

  it('should construct a proper account URL with the given parameters', () => {
    const result = getExplorerLink({
      type: 'account',
      did: 'did:example:123',
      chainHost: 'https://chain.example.com',
    });
    expect(result).toBe('https://chain.example.com/explorer/accounts/did:example:123');
  });

  it('should append query parameters when provided', () => {
    const result = getExplorerLink({
      type: 'tx',
      did: 'txhash123',
      chainHost: 'https://chain.example.com',
      queryParams: { foo: 'bar', baz: 'qux' },
    });
    expect(result).toBe('https://chain.example.com/explorer/txs/txhash123?foo=bar&baz=qux');
  });

  it('should construct a default URL when type is not recognized', () => {
    const result = getExplorerLink({
      type: 'unknown',
      did: 'did:example:123',
      chainHost: 'https://chain.example.com',
    });
    expect(result).toBe('https://chain.example.com/');
  });

  it('should handle invalid chain host URLs', () => {
    const result = getExplorerLink({
      type: 'asset',
      did: 'did:example:123',
      chainHost: 'invalid-url',
    });
    expect(result).toBeUndefined();
  });
});

describe('resolveAddressChainTypes', () => {
  it('should return ethereum, base, and arcblock for Ethereum addresses', () => {
    const result = resolveAddressChainTypes('0x71C7656EC7ab88b098defB751B7401B5f6d8976F');
    expect(result).toEqual(['ethereum', 'base', 'arcblock']);
  });

  it('should return arcblock for non-Ethereum addresses', () => {
    const result = resolveAddressChainTypes('did:abt:z1muQ3xqHQK2uiACHyChikobsiY5kLqtShA');
    expect(result).toEqual(['arcblock']);
  });
});

describe('formatCurrencyInfo', () => {
  it('should format amount with currency symbol when isToken is true', () => {
    const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
    const result = formatCurrencyInfo('10', paymentCurrency, null, true);
    expect(result).toBe('10 ETH');
  });

  it('should use fromUnitToToken to format amount when isToken is false', () => {
    const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
    // Mock the fromUnitToToken function to return a predictable value
    // In a real test, you might use jest.mock to mock this dependency
    // For this example, we'll assume fromUnitToToken converts correctly
    const result = formatCurrencyInfo('1000000000000000000', paymentCurrency, null);
    // In reality, this would be '1 ETH'
    expect(result).toContain('ETH');
  });

  it('should include payment method name for non-arcblock payment methods', () => {
    const paymentCurrency = { symbol: 'USD', decimal: 2 } as PaymentCurrency;
    const paymentMethod = { type: 'stripe', name: 'Credit Card' } as PaymentMethod;
    const result = formatCurrencyInfo('1000', paymentCurrency, paymentMethod, true);
    expect(result).toBe('1000 USD (Credit Card)');
  });

  it('should not include payment method name for arcblock payment methods', () => {
    const paymentCurrency = { symbol: 'ABT', decimal: 18 } as PaymentCurrency;
    const paymentMethod = { type: 'arcblock', name: 'ArcBlock' } as PaymentMethod;
    const result = formatCurrencyInfo('5', paymentCurrency, paymentMethod, true);
    expect(result).toBe('5 ABT');
  });

  it('should handle undefined or empty amount', () => {
    const paymentCurrency = { symbol: 'ETH', decimal: 18 } as PaymentCurrency;
    const result = formatCurrencyInfo('', paymentCurrency, null, true);
    expect(result).toBe('0 ETH');
  });

  it('should use default values when currency information is missing', () => {
    const paymentCurrency = {} as PaymentCurrency;
    const result = formatCurrencyInfo('10', paymentCurrency, null, true);
    expect(result).toBe('10 ');
  });
});

describe('getExplorerTxUrl', () => {
  it('should return empty string when explorerHost or txHash is not provided', () => {
    const result1 = getExplorerTxUrl({
      explorerHost: '',
      txHash: 'hash123',
      type: 'ethereum',
    });
    expect(result1).toBe('');

    const result2 = getExplorerTxUrl({
      explorerHost: 'https://explorer.example.com',
      txHash: '',
      type: 'ethereum',
    });
    expect(result2).toBe('');
  });

  it('should construct a proper URL for arcblock type', () => {
    const result = getExplorerTxUrl({
      explorerHost: 'https://explorer.example.com',
      txHash: 'hash123',
      type: 'arcblock',
    });
    expect(result).toBe('https://explorer.example.com/txs/hash123');
  });

  it('should construct a proper URL for non-arcblock types', () => {
    const result = getExplorerTxUrl({
      explorerHost: 'https://explorer.example.com',
      txHash: 'hash123',
      type: 'ethereum',
    });
    expect(result).toBe('https://explorer.example.com/tx/hash123');
  });

  it('should handle different blockchain types correctly', () => {
    const testCases = [
      { type: 'ethereum', expected: '/tx/' },
      { type: 'base', expected: '/tx/' },
      { type: 'bitcoin', expected: '/tx/' },
      { type: 'arcblock', expected: '/txs/' },
    ];

    testCases.forEach(({ type, expected }) => {
      const result = getExplorerTxUrl({
        explorerHost: 'https://explorer.example.com',
        txHash: 'hash123',
        type: type as any,
      });
      expect(result).toContain(expected);
    });
  });
});
