#!/usr/bin/env node

/**
 * Minimal tests for CCIP Relay Service.
 * Run with: npm test
 * Does not start the relay or connect to RPC.
 * For full tests, set PRIVATE_KEY (or RELAYER_PRIVATE_KEY) and run; config load will then validate.
 */

import { existsSync } from 'fs';
import { mkdtemp, rm } from 'fs/promises';
import { join, dirname } from 'path';
import { tmpdir } from 'os';
import { fileURLToPath } from 'url';
import { ethers } from 'ethers';
import { MessageQueue } from './src/MessageQueue.js';
import { RelayService } from './src/RelayService.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

function assert(condition, message) {
  if (!condition) {
    console.error('FAIL:', message);
    process.exit(1);
  }
}

console.log('Relay service structure tests...');

assert(existsSync(join(__dirname, 'src', 'config.js')), 'src/config.js should exist');
assert(existsSync(join(__dirname, 'src', 'RelayService.js')), 'src/RelayService.js should exist');
assert(existsSync(join(__dirname, 'src', 'healthServer.js')), 'src/healthServer.js should exist');
assert(existsSync(join(__dirname, 'index.js')), 'index.js should exist');

const logger = {
  info() {},
  warn() {},
  error() {},
  debug() {},
};

const relay = new RelayService({
  sourceChain: {
    name: 'Chain 138',
    chainId: 138,
    rpcUrl: 'http://example.invalid',
    routerAddress: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
    bridgeAddress: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
  },
  destinationChain: {
    name: 'Ethereum Mainnet',
    chainId: 1,
    rpcUrl: 'http://example.invalid',
    relayRouterAddress: '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA',
    relayBridgeAddress: '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
    relayBridgeAllowlist: ['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5'],
    chainSelector: 5009297550715157269n,
    deliveryMode: 'router',
  },
  tokenMapping: {},
  sourceChainSelector: 138n,
  relayer: { privateKey: '', address: '' },
  monitoring: {
    startBlock: 'latest',
    pollInterval: 5000,
    confirmationBlocks: 1,
    finalityDelayBlocks: 2,
    replayWindowBlocks: 32,
  },
  retry: { maxRetries: 3, retryDelay: 5000 },
  skipMessageIds: new Set(),
}, logger);

const cwReceiver = ethers.AbiCoder.defaultAbiCoder().encode(
  ['address'],
  ['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5']
);
const scoped = relay.evaluateMessageScope({
  sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
  receiver: cwReceiver,
});
assert(scoped.inScope === true, 'cW message should match the configured worker scope');
assert(
  scoped.targetBridge === '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
  'cW target bridge should decode from receiver bytes'
);

const wethReceiver = ethers.AbiCoder.defaultAbiCoder().encode(
  ['address'],
  ['0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939']
);
const rejected = relay.evaluateMessageScope({
  sender: '0xcacfd227A040002e49e2e01626363071324f820a',
  receiver: wethReceiver,
});
assert(rejected.inScope === false, 'WETH message should be rejected by the cW worker scope');

const tempDir = await mkdtemp(join(tmpdir(), 'relay-queue-test-'));
const queueStatePath = join(tempDir, 'queue-state.json');
const queue = new MessageQueue(logger, { persistencePath: queueStatePath });
await queue.init();
const queuedMessage = {
  messageId: '0xtest-message',
  sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
  receiver: cwReceiver,
  data: '0x',
  tokenAmounts: []
};
await queue.add(queuedMessage);
const inFlightMessage = await queue.getNext();
assert(inFlightMessage?.messageId === queuedMessage.messageId, 'getNext should return the queued message');
await queue.retry(queuedMessage.messageId);
const retriedMessage = await queue.getNext();
assert(retriedMessage?.messageId === queuedMessage.messageId, 'retry should requeue the original message payload');
await queue.resetRetryCount(queuedMessage.messageId);
await queue.retry(queuedMessage.messageId, { increment: false });
assert((await queue.getRetryCount(queuedMessage.messageId)) === 0, 'deferred requeue should not consume retry budget');

const persistentQueue = new MessageQueue(logger, { persistencePath: queueStatePath });
await persistentQueue.init();
assert(
  persistentQueue.getStats().queueSize === 1,
  'persisted queue snapshot should reload queued messages after restart'
);

const probeRelay = new RelayService({
  sourceChain: {
    name: 'Chain 138',
    chainId: 138,
    rpcUrl: 'http://example.invalid',
    routerAddress: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
    bridgeAddress: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
  },
  destinationChain: {
    name: 'Ethereum Mainnet',
    chainId: 1,
    rpcUrl: 'http://example.invalid',
    relayRouterAddress: '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA',
    relayBridgeAddress: '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5',
    relayBridgeAllowlist: ['0x2bF74583206A49Be07E0E8A94197C12987AbD7B5'],
    chainSelector: 5009297550715157269n,
    deliveryMode: 'router',
  },
  tokenMapping: {},
  sourceChainSelector: 138n,
  relayer: { privateKey: '0x' + '11'.repeat(32), address: '' },
  monitoring: {
    startBlock: 'latest',
    pollInterval: 5000,
    confirmationBlocks: 1,
    finalityDelayBlocks: 2,
    replayWindowBlocks: 32,
  },
  retry: { maxRetries: 3, retryDelay: 1 },
  queuePersistence: { path: join(tempDir, 'probe-queue-state.json') },
  skipMessageIds: new Set(),
}, logger);
await probeRelay.messageQueue.init();
await probeRelay.messageQueue.add({
  messageId: '0xprobe-fail',
  destinationChainSelector: 5009297550715157269n,
  sender: '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7',
  receiver: cwReceiver,
  data: '0x',
  tokenAmounts: [{ token: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', amount: 1n, amountType: 0 }],
});
probeRelay.destinationRelayRouter = { paused: async () => false };
probeRelay.destinationProvider = {};
probeRelay.getDestinationTxOptions = async () => ({ gasLimit: 1000000n });
probeRelay.ensureTargetBridgeInventory = async () => {
  throw new Error('temporary eth_call failure');
};
const deferredMessage = await probeRelay.messageQueue.getNext();
await probeRelay.relayMessage(deferredMessage);
assert(
  probeRelay.lastError?.scope === 'bridge_inventory_probe',
  'inventory probe failures should be tracked under bridge_inventory_probe'
);
assert(
  (await probeRelay.messageQueue.getRetryCount('0xprobe-fail')) === 0,
  'inventory probe deferrals should not consume retry budget'
);
assert(
  probeRelay.messageQueue.getStats().queueSize === 1,
  'inventory probe deferrals should requeue the original message'
);

await rm(tempDir, { recursive: true, force: true });

console.log('OK: relay service structure valid');
process.exit(0);
