import { createServer } from 'http';
import express from 'express';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';

const mockGetTokens = jest.fn();
const mockGetToken = jest.fn();
const mockSearchTokens = jest.fn();
const mockGetMarketData = jest.fn();
const mockGetPoolsByToken = jest.fn();
const mockGetPool = jest.fn();
const mockGetLiveDodoPools = jest.fn();
const mockResolveTokenDisplay = jest.fn();
const mockResolvePoolTokenDisplays = jest.fn();
const mockGetTokenByContract = jest.fn();
const mockGetOHLCV = jest.fn();
const mockGetHistoricalReferencePrice = jest.fn();

jest.mock('../../database/repositories/token-repo', () => ({
  TokenRepository: jest.fn().mockImplementation(() => ({
    getTokens: mockGetTokens,
    getToken: mockGetToken,
    searchTokens: mockSearchTokens,
  })),
}));

jest.mock('../../database/repositories/market-data-repo', () => ({
  MarketDataRepository: jest.fn().mockImplementation(() => ({
    getMarketData: mockGetMarketData,
  })),
}));

jest.mock('../../database/repositories/pool-repo', () => ({
  PoolRepository: jest.fn().mockImplementation(() => ({
    getPoolsByToken: mockGetPoolsByToken,
    getPool: mockGetPool,
  })),
}));

jest.mock('../../indexer/ohlcv-generator', () => ({
  OHLCVGenerator: jest.fn().mockImplementation(() => ({
    getOHLCV: mockGetOHLCV,
  })),
}));

const mockGetMarketDataAdapter = jest.fn();

jest.mock('../../adapters/coingecko-adapter', () => ({
  CoinGeckoAdapter: jest.fn().mockImplementation(() => ({
    getTokenByContract: mockGetTokenByContract,
    getMarketData: mockGetMarketDataAdapter,
    getHistoricalReferencePrice: mockGetHistoricalReferencePrice,
    getReferenceMarketData: jest.fn().mockResolvedValue(null),
    getTrending: jest.fn().mockResolvedValue([]),
  })),
}));

jest.mock('../../adapters/cmc-adapter', () => ({
  CoinMarketCapAdapter: jest.fn().mockImplementation(() => ({
    getTokenByContract: mockGetTokenByContract,
    getMarketData: mockGetMarketDataAdapter,
  })),
}));

jest.mock('../../adapters/dexscreener-adapter', () => ({
  DexScreenerAdapter: jest.fn().mockImplementation(() => ({
    getTokenByContract: mockGetTokenByContract,
    getMarketData: mockGetMarketDataAdapter,
  })),
}));

jest.mock('../../services/live-dodo-fallback', () => ({
  getLiveDodoPools: (...args: unknown[]) => mockGetLiveDodoPools(...args),
}));

jest.mock('../../services/token-display', () => ({
  resolveTokenDisplay: (...args: unknown[]) => mockResolveTokenDisplay(...args),
  resolvePoolTokenDisplays: (...args: unknown[]) => mockResolvePoolTokenDisplays(...args),
}));

jest.mock('../middleware/cache');

jest.mock('../../services/chain138-onchain-reference-prices', () => ({
  refreshChain138LiveReferencePrices: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('../../services/blockscout-token-meta', () => ({
  fetchBlockscoutTokenMeta: jest.fn().mockResolvedValue(null),
}));

const tokensRoutes = require('./tokens').default as typeof import('./tokens').default;

function createApp() {
  const app = express();
  app.use(express.json());
  app.use('/api/v1', tokensRoutes);
  return app;
}

async function startServer(app: express.Application): Promise<{ server: ReturnType<typeof createServer>; baseUrl: string }> {
  const server = createServer(app);
  await new Promise<void>((resolve) => server.listen(0, () => resolve()));
  const port = (server.address() as { port: number }).port;
  return { server, baseUrl: `http://127.0.0.1:${port}` };
}

describe('Tokens API', () => {
  let server: ReturnType<typeof createServer>;
  let baseUrl: string;

  beforeAll(async () => {
    const app = createApp();
    const started = await startServer(app);
    server = started.server;
    baseUrl = started.baseUrl;
  });

  beforeEach(() => {
    jest.clearAllMocks();
    mockGetMarketDataAdapter.mockResolvedValue(null);
    mockGetTokens.mockResolvedValue([]);
    mockGetToken.mockResolvedValue(null);
    mockSearchTokens.mockResolvedValue([]);
    mockGetMarketData.mockResolvedValue(null);
    mockGetPoolsByToken.mockResolvedValue([]);
    mockGetPool.mockResolvedValue(null);
    mockGetOHLCV.mockResolvedValue([]);
    mockGetLiveDodoPools.mockResolvedValue([]);
    mockResolveTokenDisplay.mockResolvedValue({
      address: '',
      name: 'Unknown Token',
      symbol: 'UNKNOWN',
      decimals: 18,
      source: 'fallback',
    });
    mockResolvePoolTokenDisplays.mockResolvedValue({
      token0: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
      token1: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
    });
    mockGetTokenByContract.mockResolvedValue(null);
    mockGetHistoricalReferencePrice.mockResolvedValue(null);
  });

  afterAll(async () => {
    await new Promise<void>((resolve, reject) => {
      server.close((error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    });
  });

  it('lists canonical 138 tokens with stable and ETH-family fallback pricing when db market data is missing', async () => {
    const usdt = getCanonicalTokenBySymbol(138, 'USDT');
    const weth = getCanonicalTokenBySymbol(138, 'WETH');
    const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');

    expect(usdt?.addresses[138]).toBeTruthy();
    expect(weth?.addresses[138]).toBeTruthy();
    expect(weth10?.addresses[138]).toBeTruthy();

    const res = await fetch(`${baseUrl}/api/v1/tokens?chainId=138&limit=400`);
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body.source).toBe('canonical');

    const findByAddress = (address?: string) =>
      body.tokens.find((token: Record<string, any>) => token.address === address?.toLowerCase());

    expect(findByAddress(usdt?.addresses[138])).toMatchObject({
      symbol: 'USDT',
      decimals: 6,
      market: expect.objectContaining({
        priceUsd: 1,
        volume24h: 0,
        liquidityUsd: 0,
      }),
    });

    expect(findByAddress(weth?.addresses[138])).toMatchObject({
      symbol: 'WETH',
      decimals: 18,
      market: expect.objectContaining({
        priceUsd: 2490,
      }),
    });

    expect(findByAddress(weth10?.addresses[138])).toMatchObject({
      symbol: 'WETH10',
      decimals: 18,
      market: expect.objectContaining({
        priceUsd: 2490,
      }),
    });
  });

  it('fills missing priceUsd on token detail responses while preserving repository market fields', async () => {
    const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
    expect(weth10?.addresses[138]).toBeTruthy();
    const weth10Address = String(weth10?.addresses[138]).toLowerCase();

    mockGetMarketData.mockResolvedValue({
      chainId: 138,
      tokenAddress: weth10Address,
      priceUsd: undefined,
      volume24h: 1234,
      volume7d: 5678,
      volume30d: 9012,
      liquidityUsd: 3456,
      holdersCount: 78,
      transfers24h: 9,
      lastUpdated: new Date('2026-04-16T00:00:00.000Z'),
    });

    const res = await fetch(`${baseUrl}/api/v1/tokens/${weth10Address}?chainId=138`);
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body.token).toMatchObject({
      symbol: 'WETH10',
      decimals: 18,
      market: expect.objectContaining({
        priceUsd: 2490,
        volume24h: 1234,
        liquidityUsd: 3456,
      }),
      hasDodoPool: false,
    });
    expect(body.token.canonicalLiquidity).toBeUndefined();
  });

  it('normalizes raw 6-decimal liquidity rows on token detail responses', async () => {
    const cusdc = getCanonicalTokenBySymbol(138, 'cUSDC');
    expect(cusdc?.addresses[138]).toBeTruthy();
    const cusdcAddress = String(cusdc?.addresses[138]).toLowerCase();

    mockGetToken.mockResolvedValue({
      chainId: 138,
      address: cusdcAddress,
      name: 'USD Coin (Compliant)',
      symbol: 'cUSDC',
      decimals: 6,
      totalSupply: '38601011267000000',
      verified: true,
    });
    mockGetMarketData.mockResolvedValue({
      chainId: 138,
      tokenAddress: cusdcAddress,
      priceUsd: 1,
      volume24h: 0,
      volume7d: 0,
      volume30d: 26890.7,
      liquidityUsd: 5180095723066127,
      holdersCount: 0,
      transfers24h: 0,
      lastUpdated: new Date('2026-05-10T01:43:11.733Z'),
    });

    const res = await fetch(`${baseUrl}/api/v1/tokens/${cusdcAddress}?chainId=138`);
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body.token).toMatchObject({
      symbol: 'cUSDC',
      decimals: 6,
      market: expect.objectContaining({
        priceUsd: 1,
        liquidityUsd: 5180095723.066127,
      }),
    });
  });

  it('returns historical price snapshots for a token at a requested timestamp', async () => {
    const weth = getCanonicalTokenBySymbol(138, 'WETH');
    expect(weth?.addresses[138]).toBeTruthy();
    const wethAddress = String(weth?.addresses[138]).toLowerCase();

    mockGetOHLCV
      .mockResolvedValueOnce([
        {
          timestamp: new Date('2026-04-26T01:30:00.000Z'),
          open: 2488,
          high: 2491,
          low: 2487,
          close: 2490,
          volume: 10,
          volumeUsd: 24900,
        },
      ]);

    const res = await fetch(
      `${baseUrl}/api/v1/tokens/${wethAddress}/price-at?chainId=138&timestamp=${encodeURIComponent('2026-04-26T01:33:02.000Z')}`
    );
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body).toMatchObject({
      chainId: 138,
      tokenAddress: wethAddress,
      requestedTimestamp: '2026-04-26T01:33:02.000Z',
      effectiveTimestamp: '2026-04-26T01:30:00.000Z',
      priceUsd: 2490,
      source: 'ohlcv_5m',
    });
  });

  it('returns pricing context with current and locked historical snapshots', async () => {
    const weth = getCanonicalTokenBySymbol(138, 'WETH');
    expect(weth?.addresses[138]).toBeTruthy();
    const wethAddress = String(weth?.addresses[138]).toLowerCase();

    mockGetOHLCV.mockResolvedValueOnce([
      {
        timestamp: new Date('2026-04-26T01:30:00.000Z'),
        open: 2488,
        high: 2491,
        low: 2487,
        close: 2490,
        volume: 10,
        volumeUsd: 24900,
      },
    ]);

    mockGetMarketData.mockResolvedValue({
      chainId: 138,
      tokenAddress: wethAddress,
      priceUsd: 2490,
      volume24h: 1000,
      volume7d: 2000,
      volume30d: 3000,
      liquidityUsd: 4000,
      holdersCount: 5,
      transfers24h: 6,
      lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
    });

    const res = await fetch(
      `${baseUrl}/api/v1/tokens/${wethAddress}/pricing-context?chainId=138&timestamp=${encodeURIComponent('2026-04-26T01:33:02.000Z')}`
    );
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body).toMatchObject({
      chainId: 138,
      tokenAddress: wethAddress,
      current: {
        chainId: 138,
        tokenAddress: wethAddress,
        priceUsd: 2490,
        stale: false,
      },
      historical: {
        chainId: 138,
        tokenAddress: wethAddress,
        requestedTimestamp: '2026-04-26T01:33:02.000Z',
        effectiveTimestamp: '2026-04-26T01:30:00.000Z',
        priceUsd: 2490,
        source: 'ohlcv_5m',
        locked: true,
      },
    });
    expect(body.current.asOf).toEqual(expect.any(String));
    expect(body.current.sourceLayer).toEqual(expect.any(String));
  });

  it('falls back to canonical CoinGecko history for native reference assets when local OHLCV is unavailable', async () => {
    const weth = getCanonicalTokenBySymbol(138, 'WETH');
    expect(weth?.addresses[138]).toBeTruthy();
    const wethAddress = String(weth?.addresses[138]).toLowerCase();

    mockGetHistoricalReferencePrice.mockResolvedValue({
      priceUsd: 2484.12,
      effectiveTimestamp: new Date('2026-04-26T01:31:00.000Z'),
    });

    const res = await fetch(
      `${baseUrl}/api/v1/tokens/${wethAddress}/pricing-context?chainId=138&timestamp=${encodeURIComponent('2026-04-26T01:33:02.000Z')}`
    );
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body.historical).toMatchObject({
      chainId: 138,
      tokenAddress: wethAddress,
      requestedTimestamp: '2026-04-26T01:33:02.000Z',
      effectiveTimestamp: '2026-04-26T01:31:00.000Z',
      priceUsd: 2484.12,
      source: 'coingecko_history',
      locked: true,
    });
  });

  it('does not lock stale OHLCV candles that are too far before the requested timestamp', async () => {
    const usdt = getCanonicalTokenBySymbol(138, 'USDT');
    expect(usdt?.addresses[138]).toBeTruthy();
    const usdtAddress = String(usdt?.addresses[138]).toLowerCase();

    mockGetOHLCV.mockResolvedValue([
      {
        timestamp: new Date('2026-04-11T00:00:00.000Z'),
        open: 1,
        high: 1,
        low: 1,
        close: 1,
        volume: 15,
        volumeUsd: 15,
      },
    ]);

    mockGetHistoricalReferencePrice.mockResolvedValue(null);
    mockGetMarketData.mockResolvedValue({
      chainId: 138,
      tokenAddress: usdtAddress,
      priceUsd: 1,
      volume24h: 0,
      volume7d: 0,
      volume30d: 0,
      liquidityUsd: 0,
      holdersCount: 0,
      transfers24h: 0,
      lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
    });

    const res = await fetch(
      `${baseUrl}/api/v1/tokens/${usdtAddress}/pricing-context?chainId=138&timestamp=${encodeURIComponent('2026-04-26T01:33:02.000Z')}`
    );
    expect(res.status).toBe(200);

    const body = (await res.json()) as Record<string, any>;
    expect(body.historical).toMatchObject({
      chainId: 138,
      tokenAddress: usdtAddress,
      requestedTimestamp: '2026-04-26T01:33:02.000Z',
      source: 'current_market_fallback',
      locked: false,
    });
  });

  it('returns distinct market-batch snapshots per requested address set', async () => {
    const cusdt = getCanonicalTokenBySymbol(138, 'cUSDT');
    const weth = getCanonicalTokenBySymbol(138, 'WETH');
    expect(cusdt?.addresses[138]).toBeTruthy();
    expect(weth?.addresses[138]).toBeTruthy();

    const cusdtAddress = String(cusdt?.addresses[138]).toLowerCase();
    const wethAddress = String(weth?.addresses[138]).toLowerCase();

    mockGetMarketData.mockImplementation(async (_chainId: number, address: string) => {
      if (address === cusdtAddress) {
        return {
          chainId: 138,
          tokenAddress: cusdtAddress,
          priceUsd: 1,
          volume24h: 0,
          volume7d: 0,
          volume30d: 0,
          liquidityUsd: 1_000_000,
          holdersCount: 0,
          transfers24h: 0,
          lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
        };
      }
      if (address === wethAddress) {
        return {
          chainId: 138,
          tokenAddress: wethAddress,
          priceUsd: 1680,
          volume24h: 0,
          volume7d: 0,
          volume30d: 0,
          liquidityUsd: 5_000_000,
          holdersCount: 0,
          transfers24h: 0,
          lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
        };
      }
      return null;
    });

    const first = await fetch(`${baseUrl}/api/v1/tokens/market-batch`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ chainId: 138, addresses: [cusdtAddress] }),
    });
    expect(first.status).toBe(200);
    const firstBody = (await first.json()) as Record<string, any>;
    expect(firstBody.requested).toBe(1);
    expect(firstBody.snapshots?.[0]?.address).toBe(cusdtAddress);

    const second = await fetch(`${baseUrl}/api/v1/tokens/market-batch`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ chainId: 138, addresses: [wethAddress] }),
    });
    expect(second.status).toBe(200);
    const secondBody = (await second.json()) as Record<string, any>;
    expect(secondBody.requested).toBe(1);
    expect(secondBody.snapshots?.[0]?.address).toBe(wethAddress);
    expect(secondBody.snapshots?.[0]?.priceUsd).toBe(1680);
  });
});
