import assert from "node:assert/strict";
import test from "node:test";
import { DatabaseSync } from "node:sqlite";
import { readFileSync } from "node:fs";
import { refreshSharedSnapshots, readSharedSnapshots, readFeedStatuses } from "../lib/shared-liquidity.ts";
import { providerConfig } from "../lib/liquidity-provider.ts";
import { CHAINS } from "../lib/chains.ts";

function database() {
  const sqlite = new DatabaseSync(":memory:");
  sqlite.exec(readFileSync(new URL("../drizzle/0000_nasty_outlaw_kid.sql", import.meta.url), "utf8"));
  sqlite.exec(readFileSync(new URL("../drizzle/0001_perpetual_shiva.sql", import.meta.url), "utf8"));
  const prepare = (sql, values = []) => ({
    bind(...next) { return prepare(sql, next); },
    async first() { return sqlite.prepare(sql).get(...values) ?? null; },
    async all() { return { results: sqlite.prepare(sql).all(...values) }; },
    async run() { return sqlite.prepare(sql).run(...values); },
  });
  return { sqlite, db: { prepare, async batch(statements) { sqlite.exec("BEGIN"); try { const results = []; for (const s of statements) results.push(await s.run()); sqlite.exec("COMMIT"); return results; } catch (e) { sqlite.exec("ROLLBACK"); throw e; } } } };
}
const address = n => `0x${n.toString(16).padStart(40, "0")}`;
function fixture(network) {
  const included = [{ id: `${network}_${address(1000)}`, type: "token", attributes: { address: address(1000), name: "USD Coin", symbol: "USDC" } }, { id: `${network}_${address(1001)}`, type: "token", attributes: { address: address(1001), name: "Wrapped Ether", symbol: "WETH" } }];
  const pools = Array.from({ length: 80 }, (_, i) => ({ id: `${network}_${address(i + 1)}`, attributes: { address: address(i + 1), name: "USDC / WETH", reserve_in_usd: "100", volume_usd: { h24: "50" } }, relationships: { base_token: { data: { id: included[0].id } }, quote_token: { data: { id: included[1].id } }, dex: { data: { id: `uniswap-v3-${network}` } } } }));
  return { included, pools };
}

test("all chains share one quota; progress survives 429s and fresh isolates; bulk cycles update all three", async t => {
  const { db, sqlite } = database();
  const fetchBefore = globalThis.fetch;
  const env = { COINGECKO_PRO_API_KEY: "test-private-key", COINGECKO_API_PLAN: "analyst" };
  const calls = []; let limited = false;
  globalThis.fetch = async (input, init) => {
    const url = new URL(input); calls.push(url);
    assert.equal(url.hostname, "pro-api.coingecko.com");
    assert.equal(init.headers["x-cg-pro-api-key"], "test-private-key");
    assert.ok(!url.toString().includes("test-private-key"));
    if (limited) { limited = false; return new Response("limited", { status: 429, headers: { "Retry-After": "120" } }); }
    const network = url.pathname.split("/")[5];
    const f = fixture(network);
    const bulk = url.pathname.includes("/multi/");
    const page = Number(url.searchParams.get("page"));
    const selected = bulk ? f.pools.filter(p => url.pathname.split("/multi/")[1].split(",").includes(p.attributes.address)) : f.pools.slice((page - 1) * 20, page * 20);
    assert.ok(selected.length > 0);
    if (bulk) assert.ok(selected.length <= 50);
    return Response.json({ data: selected, included: f.included });
  };
  t.mock.timers.enable({ apis: ["Date"], now: Date.now() });
  try {
    await Promise.all(Array.from({ length: 100 }, () => refreshSharedSnapshots(db, env)));
    assert.equal(calls.length, 1, "100 concurrent visitors create exactly one upstream call across all chains");
    for (let i = 0; i < 2; i++) { t.mock.timers.tick(10_001); await refreshSharedSnapshots(db, env); }
    assert.deepEqual(calls.map(u => u.pathname.split("/")[5]), ["robinhood", "eth", "base"]);
    const firstObservation = JSON.parse(sqlite.prepare("SELECT state FROM liquidity_collectors WHERE chain = 'robinhood'").get().state).pages[0].observedAt;
    t.mock.timers.tick(10_001); limited = true;
    await refreshSharedSnapshots(db, env);
    const before = calls.length;
    t.mock.timers.tick(60_000);
    await Promise.all(Array.from({ length: 30 }, () => refreshSharedSnapshots(db, env)));
    assert.equal(calls.length, before, "Retry-After cools down all three networks, including fresh visitors");
    assert.equal((await readFeedStatuses(db, env)).base.state, "rate-limited");
    t.mock.timers.tick(60_001);
    const newIsolate = await import("../lib/shared-liquidity.ts?new-isolate");
    await newIsolate.refreshSharedSnapshots(db, env);
    assert.equal(JSON.parse(sqlite.prepare("SELECT state FROM liquidity_collectors WHERE chain = 'robinhood'").get().state).pages.length, 2, "Resume page 2 without losing page 1");
    for (let i = 0; i < 8; i++) { t.mock.timers.tick(10_001); await refreshSharedSnapshots(db, env); }
    const first = await readSharedSnapshots(db);
    for (const chain of CHAINS) { assert.equal(first[chain.id].totals.pools, 80); assert.equal(first[chain.id].chainId, chain.id); }
    assert.equal(first.robinhood.updatedAt, firstObservation, "No timestamp laundering across a cooldown");
    for (let i = 0; i < 6; i++) { t.mock.timers.tick(10_001); await refreshSharedSnapshots(db, env); }
    const latest = await readSharedSnapshots(db);
    for (const chain of CHAINS) { assert.ok(Date.parse(latest[chain.id].updatedAt) > Date.parse(first[chain.id].updatedAt)); assert.equal(latest[chain.id].totals.pools, 80); }
    assert.equal(calls.filter(u => u.pathname.includes("/multi/")).length, 6, "240 pools refresh in six Analyst calls");
  } finally { globalThis.fetch = fetchBefore; sqlite.close(); t.mock.timers.reset(); }
});

test("monthly credit ceilings apply to the whole site, never per chain", () => {
  for (const [env, credits] of [[{ COINGECKO_DEMO_API_KEY: "test" }, 10_000], [{ COINGECKO_PRO_API_KEY: "test" }, 100_000], [{ COINGECKO_PRO_API_KEY: "test", COINGECKO_API_PLAN: "analyst" }, 500_000]]) {
    assert.ok(31 * 86400_000 / providerConfig(env).interval < credits);
  }
  assert.equal(providerConfig().interval, 20_000);
});
