import axios from 'axios';
import { TokenDataProvider } from '../../src/libs/exchange-rate/token-data-provider';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

// Mock the token-address-mapping module
jest.mock('../../src/libs/exchange-rate/token-address-mapping', () => ({
  getTokenInfo: jest.fn((symbol: string) => {
    const symbolUpper = symbol.toUpperCase();
    const mapping: Record<string, any> = {
      ABT: {
        symbol: 'ABT',
        address: '0xB98d4C97425d9908E66E53A6fDf673ACcA0BE986',
        name: 'ArcBlock Token',
        decimals: 18,
      },
      ETH: {
        symbol: 'ETH',
        address: '0x0000000000000000000000000000000000000000',
        name: 'Ethereum',
        decimals: 18,
      },
    };
    return mapping[symbolUpper];
  }),
}));

describe('TokenDataProvider', () => {
  let provider: TokenDataProvider;

  beforeEach(() => {
    provider = new TokenDataProvider();
    jest.clearAllMocks();
  });

  describe('fetch', () => {
    it('should fetch exchange rate successfully', async () => {
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              name: 'ArcBlock Token',
              price: {
                USD: 0.15,
              },
              updatedAt: '2024-01-01T00:00:00Z',
            },
          ],
          total: 1,
          page: 1,
          size: 10,
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      const result = await provider.fetch('ABT');

      expect(result.rate).toBe('0.15');
      expect(result.timestamp_ms).toBeGreaterThan(0);
      expect(result.metadata?.source).toBe('token-data');
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://token-data.arcblock.io/api/token-price-by-address',
        expect.objectContaining({
          params: {
            addresses: '0xb98d4c97425d9908e66e53a6fdf673acca0be986',
          },
          timeout: 10000,
        })
      );
    });

    it('should handle alternative response format with symbol endpoint', async () => {
      // ETH has zero address, so it should use symbol endpoint
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ETH',
              name: 'Ethereum',
              price: {
                USD: 2000.5,
              },
              updatedAt: '2024-01-01T00:00:00Z',
            },
          ],
          total: 1,
          page: 1,
          size: 10,
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      const result = await provider.fetch('ETH');

      expect(result.rate).toBe('2000.5');
      expect(result.timestamp_ms).toBeGreaterThan(0);
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://token-data.arcblock.io/api/token-price-by-symbol',
        expect.objectContaining({
          params: {
            symbols: 'ETH',
          },
        })
      );
    });

    it('should throw error for invalid price', async () => {
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              price: {
                USD: -1,
              },
            },
          ],
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      await expect(provider.fetch('ABT')).rejects.toThrow('Invalid price value');
    });

    it('should throw error for missing USD price', async () => {
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              price: {},
            },
          ],
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      await expect(provider.fetch('ABT')).rejects.toThrow('No valid USD price found');
    });

    it('should throw error for zero price', async () => {
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              price: {
                USD: 0,
              },
            },
          ],
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      // Zero is falsy, so it gets caught by the !usdPrice check first
      await expect(provider.fetch('ABT')).rejects.toThrow('No valid USD price found');
    });

    it('should throw error for empty data array', async () => {
      const mockResponse = {
        data: {
          data: [],
          total: 0,
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      await expect(provider.fetch('ABT')).rejects.toThrow('No token data found for ABT');
    });

    it('should throw SymbolNotSupportedError for unknown token', async () => {
      // Re-mock getTokenInfo to return undefined for UNKNOWN
      const { getTokenInfo } = jest.requireMock('../../src/libs/exchange-rate/token-address-mapping');
      getTokenInfo.mockReturnValueOnce(undefined);

      await expect(provider.fetch('UNKNOWN')).rejects.toThrow('Symbol UNKNOWN is not supported by token-data');
    });

    it('should handle 500 server error', async () => {
      mockedAxios.get.mockRejectedValue({
        response: { status: 500 },
      });

      await expect(provider.fetch('ABT')).rejects.toThrow('token-data server error: 500');
    });

    it('should handle timeout error', async () => {
      mockedAxios.get.mockRejectedValue({
        code: 'ECONNABORTED',
        message: 'timeout',
      });

      await expect(provider.fetch('ABT')).rejects.toThrow('token-data request timeout after 10000ms');
    });

    it('should handle connection error', async () => {
      mockedAxios.get.mockRejectedValue({
        code: 'ECONNREFUSED',
        message: 'Connection refused',
      });

      await expect(provider.fetch('ABT')).rejects.toThrow('Cannot connect to token-data');
    });

    it('should use custom base URL from config', async () => {
      const customProvider = new TokenDataProvider({
        base_url: 'https://custom.example.com',
      });

      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              price: {
                USD: 0.15,
              },
            },
          ],
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      await customProvider.fetch('ABT');

      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://custom.example.com/api/token-price-by-address',
        expect.anything()
      );
    });

    it('should convert symbol to uppercase in token lookup', async () => {
      const mockResponse = {
        data: {
          data: [
            {
              symbol: 'ABT',
              price: {
                USD: 0.15,
              },
            },
          ],
        },
      };

      mockedAxios.get.mockResolvedValue(mockResponse);

      await provider.fetch('abt');

      // The getTokenInfo mock is called with 'abt', but it handles uppercase internally
      expect(mockedAxios.get).toHaveBeenCalledWith(
        expect.stringContaining('token-price-by-address'),
        expect.anything()
      );
    });
  });
});
