import { createServer } from 'http';
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
import configRoutes from './config';

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

function createApp() {
  const app = express();
  app.use('/api/v1', configRoutes);
  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('Config API runtime networks loader', () => {
  let server: ReturnType<typeof createServer>;
  let baseUrl: string;
  const originalNetworksPath = process.env.NETWORKS_JSON_PATH;
  const originalNetworksUrl = process.env.NETWORKS_JSON_URL;

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

  afterEach(() => {
    if (originalNetworksPath === undefined) {
      delete process.env.NETWORKS_JSON_PATH;
    } else {
      process.env.NETWORKS_JSON_PATH = originalNetworksPath;
    }
    if (originalNetworksUrl === undefined) {
      delete process.env.NETWORKS_JSON_URL;
    } else {
      process.env.NETWORKS_JSON_URL = originalNetworksUrl;
    }
  });

  afterAll((done) => {
    server.close(done);
  });

  it('serves networks from a runtime file when configured', async () => {
    const tempPath = path.join('/tmp', `token-aggregation-networks-${Date.now()}.json`);
    await fs.writeFile(
      tempPath,
      JSON.stringify({
        version: { major: 9, minor: 9, patch: 9 },
        chains: [
          {
            chainId: '0x8a',
            chainIdDecimal: 138,
            chainName: 'Dynamic Chain 138',
            rpcUrls: ['https://dynamic-rpc.example'],
            nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
            blockExplorerUrls: ['https://dynamic-explorer.example'],
            oracles: [{ name: 'ETH/USD', address: '0x1111111111111111111111111111111111111111' }],
          },
        ],
      })
    );
    process.env.NETWORKS_JSON_PATH = tempPath;

    try {
      const res = await fetch(`${baseUrl}/api/v1/networks`);
      expect(res.status).toBe(200);
      const body = (await res.json()) as Record<string, any>;
      expect(body.source).toBe('runtime-file');
      expect(body.version).toBe('9.9.9');
      expect(body.networks).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            chainName: 'Dynamic Chain 138',
          }),
        ])
      );

      const cfgRes = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
      expect(cfgRes.status).toBe(200);
      const cfgBody = (await cfgRes.json()) as Record<string, any>;
      expect(cfgBody.source).toBe('runtime-file');
      expect(cfgBody.oracles).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            name: 'ETH/USD',
          }),
        ])
      );
    } finally {
      await fs.unlink(tempPath).catch(() => undefined);
    }
  });

  it('uses the remote networks source for config when NETWORKS_JSON_URL is set', async () => {
    const remotePayload = {
      version: '7.7.7',
      networks: [
        {
          chainId: '0x8a',
          chainIdDecimal: 138,
          chainName: 'Remote Chain 138',
          rpcUrls: ['https://remote-rpc.example'],
          nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
          blockExplorerUrls: ['https://remote-explorer.example'],
          oracles: [{ name: 'BTC/USD', address: '0x2222222222222222222222222222222222222222' }],
        },
      ],
    };
    const remoteServer = createServer((_req, res) => {
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify(remotePayload));
    });
    await new Promise<void>((resolve) => remoteServer.listen(0, () => resolve()));
    const remotePort = (remoteServer.address() as { port: number }).port;
    process.env.NETWORKS_JSON_URL = `http://127.0.0.1:${remotePort}/networks.json`;

    try {
      const res = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
      expect(res.status).toBe(200);
      const body = (await res.json()) as Record<string, any>;
      expect(body.source).toBe('remote-url');
      expect(body.version).toBe('7.7.7');
      expect(body.oracles).toEqual(
        expect.arrayContaining([
          expect.objectContaining({
            name: 'BTC/USD',
          }),
        ])
      );
    } finally {
      await new Promise<void>((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve())));
    }
  });

  it('merges built-in oracles when runtime networks file omits them', async () => {
    const tempPath = path.join('/tmp', `token-aggregation-networks-no-oracles-${Date.now()}.json`);
    await fs.writeFile(
      tempPath,
      JSON.stringify({
        version: { major: 1, minor: 2, patch: 0 },
        chains: [
          {
            chainId: '0x8a',
            chainIdDecimal: 138,
            chainName: 'DeFi Oracle Meta Mainnet',
            rpcUrls: ['https://rpc-http-pub.d-bis.org'],
            nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
            blockExplorerUrls: ['https://explorer.d-bis.org'],
            iconUrls: ['https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png'],
          },
        ],
      })
    );
    process.env.NETWORKS_JSON_PATH = tempPath;

    try {
      const cfgRes = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
      expect(cfgRes.status).toBe(200);
      const cfgBody = (await cfgRes.json()) as Record<string, any>;
      expect(cfgBody.oracles).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ name: 'ETH/USD (aggregator)' }),
        ])
      );

      const networksRes = await fetch(`${baseUrl}/api/v1/networks`);
      const networksBody = (await networksRes.json()) as Record<string, any>;
      const chain138 = networksBody.networks.find((entry: Record<string, any>) => entry.chainIdDecimal === 138);
      expect(chain138.oracles?.length).toBeGreaterThan(0);
      expect(chain138.iconUrls).toEqual(
        expect.arrayContaining(['https://explorer.d-bis.org/token-icons/chain-138.png'])
      );
    } finally {
      await fs.unlink(tempPath).catch(() => undefined);
    }
  });

  it('serves wallet-facing MetaMask aliases with absolute token images', async () => {
    const networksRes = await fetch(`${baseUrl}/api/v1/config/networks`);
    expect(networksRes.status).toBe(200);
    const networksBody = (await networksRes.json()) as Record<string, any>;
    expect(networksBody.networks).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          chainIdDecimal: 138,
          iconUrls: expect.arrayContaining([
            'https://explorer.d-bis.org/token-icons/chain-138.png',
            'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
          ]),
        }),
      ])
    );

    const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=138`);
    expect(metamaskRes.status).toBe(200);
    const metamaskBody = (await metamaskRes.json()) as Record<string, any>;
    expect(metamaskBody.addEthereumChain).toMatchObject({
      chainId: '0x8a',
      chainName: 'DeFi Oracle Meta Mainnet',
      rpcUrls: ['https://rpc-http-pub.d-bis.org'],
    });
    expect(metamaskBody.addEthereumChain).not.toHaveProperty('chainIdDecimal');
    expect(metamaskBody.addEthereumChain).not.toHaveProperty('oracles');
    expect(metamaskBody.watchAssets).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: 'ERC20',
          options: expect.objectContaining({
            symbol: 'cUSDC',
            image: expect.stringMatching(/^https:\/\/d-bis\.org\/tokens\/cusdc\.svg\?v=20260510$/),
          }),
        }),
        expect.objectContaining({
          type: 'ERC20',
          options: expect.objectContaining({
            address: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
            symbol: 'LINK',
            image: expect.stringMatching(/^https:\/\/d-bis\.org\/tokens\/link\.svg\?v=20260510$/),
          }),
        }),
        expect.objectContaining({
          type: 'ERC20',
          options: expect.objectContaining({
            symbol: 'WETH',
            image: expect.stringMatching(/^https:\/\/d-bis\.org\/tokens\/weth\.svg\?v=20260510$/),
          }),
        }),
      ])
    );
    const placeholderSymbols = ['cAVAX', 'cBNB', 'cETH', 'cETHL2', 'cPOL', 'cCRO', 'cXDAI', 'cCELO', 'cWEMIX'];
    for (const symbol of placeholderSymbols) {
      expect(
        metamaskBody.watchAssets.some((entry: Record<string, any>) => entry.options?.symbol === symbol),
      ).toBe(false);
    }
    expect(metamaskBody.watchAssets).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          type: 'ERC20',
          options: expect.objectContaining({
            address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b',
            symbol: 'cUSDC',
          }),
          metadata: expect.objectContaining({
            catalogSymbol: 'cUSDC',
          }),
        }),
      ])
    );
    const cusdcEntries = metamaskBody.watchAssets.filter(
      (entry: Record<string, any>) => entry.options?.symbol === 'cUSDC',
    );
    const cusdtEntries = metamaskBody.watchAssets.filter(
      (entry: Record<string, any>) => entry.options?.symbol === 'cUSDT',
    );
    expect(cusdcEntries).toHaveLength(1);
    expect(cusdtEntries).toHaveLength(1);
    expect(
      metamaskBody.watchAssets.some(
        (entry: Record<string, any>) => entry.metadata?.catalogSymbol === 'cUSDC_V2',
      ),
    ).toBe(false);
    expect(
      metamaskBody.watchAssets.some(
        (entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2',
      ),
    ).toBe(false);
  });

  it('excludes hub cUSDC/cUSDT peg references from Ethereum mainnet watchAssets', async () => {
    const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=1`);
    expect(metamaskRes.status).toBe(200);
    const metamaskBody = (await metamaskRes.json()) as Record<string, any>;
    const symbols = metamaskBody.watchAssets.map((entry: Record<string, any>) => entry.options?.symbol);
    expect(symbols).not.toContain('cUSDC');
    expect(symbols).not.toContain('cUSDT');
    expect(symbols).toEqual(
      expect.arrayContaining(['cWUSDC', 'cWUSDT'])
    );
  });
});
