import assert from "node:assert/strict";
import test from "node:test";
import { aggregatePoolPages, isLiquidityPayload, fetchLiquidityDataDirect, retryTime, UpstreamBackoff } from "../lib/liquidity.ts";

const included = [
  { id: "a", type: "token", attributes: { address: "a", name: "Wrapped Ether", symbol: "WETH" } },
  { id: "b", type: "token", attributes: { address: "b", name: "Global Dollar", symbol: "USDG" } },
  { id: "c", type: "token", attributes: { address: "c", name: "Another Wrapped Ether", symbol: "WETH" } },
  { id: "uniswap-v3-robinhood", type: "dex", attributes: { name: "Uniswap V3 (Robinhood)" } },
];
const pool = (id, base, quote, volume) => ({ id, attributes: { address: id, name: `${base} / ${quote} 0.3%`, reserve_in_usd: "10", volume_usd: { h24: String(volume) }, transactions: { h24: { buys: 2, sells: 3 } } }, relationships: { base_token: { data: { id: base } }, quote_token: { data: { id: quote } }, dex: { data: { id: "uniswap-v3-robinhood" } } } });
const make = () => aggregatePoolPages([
  { included, data: [pool("p1", "a", "b", 100), pool("p2", "b", "a", 20)] },
  { included, data: [pool("p1", "a", "b", 100), pool("p3", "c", "b", 50)] },
]);

test("moving page boundaries do not double count pools; duplicate tickers remain separate", () => {
  const data = make();
  assert.equal(data.totals.pools, 3);
  assert.equal(data.totals.volume24h, 170);
  assert.equal(data.totals.liquidity, 30);
  assert.equal(data.totals.transactions24h, 15);
  assert.equal(data.edges.length, 2);
  assert.equal(data.nodes.filter(n => n.symbol === "WETH").length, 2);
  assert.equal(data.venues[0].poolCount, 3);
  assert.equal(data.nodes.reduce((sum, n) => sum + n.volume24h, 0), 2 * data.totals.volume24h);
  assert.equal(isLiquidityPayload(data), true);
});
test("malformed snapshots cannot replace the last good render", () => {
  for (const mutate of [
    d => { d.nodes[0].volume24h = Infinity; },
    d => { d.edges[0].source = "missing"; },
    d => { d.updatedAt = "invalid"; },
    d => { d.updatedAt = "2099-01-01T00:00:00Z"; },
    d => { d.nodes[0].category = "untrusted"; },
    d => { d.edges[0].venues = null; },
    d => { d.totals.pools = 500; },
  ]) { const data = make(); mutate(data); assert.equal(isLiquidityPayload(data), false); }
});

test("provider Retry-After accepts seconds and HTTP dates with a one-minute floor", () => {
  const now = Date.parse("2026-09-04T21:00:00Z");
  assert.equal(retryTime("120", now), now + 120_000);
  assert.equal(retryTime("Fri, 04 Sep 2026 21:05:00 GMT", now), now + 300_000);
  assert.equal(retryTime(null, now), now + 60_000);
  assert.equal(retryTime("invalid", now), now + 60_000);
});

test("rate limiting backs off across fresh worker isolates instead of multiplying requests", async () => {
  const fetchBefore = globalThis.fetch, cachesBefore = globalThis.caches;
  const entries = new Map();
  let requests = 0;
  globalThis.caches = { async open() { return {
    async match(request) { return entries.get(request.url)?.clone(); },
    async put(request, response) { entries.set(request.url, response.clone()); },
  }; } };
  globalThis.fetch = async () => { requests++; return new Response("limited", { status: 429, headers: { "Retry-After": "120" } }); };
  try {
    await assert.rejects(fetchLiquidityDataDirect(), error => error instanceof UpstreamBackoff && error.retryAt > Date.now() + 119_000);
    const otherIsolate = await import("../lib/liquidity.ts?another-isolate");
    await assert.rejects(otherIsolate.fetchLiquidityDataDirect(), /Upstream cooldown/);
    assert.equal(requests, 1);
    const controller = new AbortController(); controller.abort();
    await assert.rejects(fetchLiquidityDataDirect(controller.signal), { name: "AbortError" });
    assert.equal(requests, 1);
  } finally { globalThis.fetch = fetchBefore; globalThis.caches = cachesBefore; }
});


test("native ETH protocol aliases merge by identity while WETH remains separate", () => {
  const zero = "eth_0x" + "0".repeat(40), nativeAlias = "eth_0x" + "e".repeat(40);
  const page = { included: [...included, ...[zero, nativeAlias].map(id => ({ id, type: "token", attributes: { address: id.split("_")[1], name: "Ether", symbol: "ETH" } }))],
    data: [pool("native-v4", zero, "b", 10), pool("native-fluid", nativeAlias, "b", 20), pool("wrapped", "a", "b", 30)] };
  const data = aggregatePoolPages([page], new Date().toISOString(), "ethereum");
  assert.equal(data.nodes.filter(n => n.symbol === "ETH").length, 1);
  assert.equal(data.nodes.find(n => n.symbol === "ETH").volume24h, 30);
  assert.equal(data.nodes.filter(n => n.symbol === "WETH").length, 1);
  assert.equal(data.edges.length, 2);
  assert.equal(data.totals.pools, 3);
});
