import { test } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { openDatabase } from '../src/db/connection.ts';
import { runMigrations } from '../src/db/migrate.ts';
import { createFeed } from '../src/db/repos/feeds.ts';
import { createPoller } from '../src/poller/scheduler.ts';
import { bus } from '../src/events.ts';
import type { AddressInfo } from 'node:net';
import type { Server } from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const migrationsDir = path.join(__dirname, '..', 'src', 'db', 'migrations');

interface StartedServer {
  server: Server;
  port: number;
}

function startServer(handler: http.RequestListener): Promise<StartedServer> {
  const server = http.createServer(handler);
  return new Promise((resolve) =>
    server.listen(0, '127.0.0.1', () =>
      resolve({ server, port: (server.address() as AddressInfo).port }),
    ),
  );
}

test('poller ingests, dedupes, handles 304 and failures', async () => {
  const db = openDatabase(':memory:');
  runMigrations(db, migrationsDir);

  const rss = `<?xml version="1.0"?><rss version="2.0"><channel><title>t</title><link>https://x.com</link><item><title>a</title><link>https://x.com/a</link><guid>g1</guid><pubDate>Mon, 01 Jan 2024 00:00:00 GMT</pubDate></item></channel></rss>`;

  const { server, port } = await startServer((req, res) => {
    if (req.url === '/fail') {
      res.statusCode = 500;
      res.end('boom');
      return;
    }
    if (req.headers['if-none-match'] === '"etag1"') {
      res.statusCode = 304;
      res.end();
      return;
    }
    res.setHeader('Content-Type', 'application/rss+xml');
    res.setHeader('ETag', '"etag1"');
    res.end(rss);
  });

  const base = `http://127.0.0.1:${port}`;
  const now = new Date().toISOString();
  const feedOk = createFeed(db, { url: `${base}/ok`, next_poll_at: now, created_at: now })!;
  const feedFail = createFeed(db, { url: `${base}/fail`, next_poll_at: now, created_at: now })!;

  const poller = createPoller(db, bus, { guard: async () => {} });
  await poller.pollOnce();
  const itemsAfterFirst = db
    .prepare('SELECT COUNT(*) AS c FROM items WHERE feed_id = ?')
    .get(feedOk.id)! as { c: number };
  assert.equal(itemsAfterFirst.c, 1);

  // re-poll same feed: no duplicates
  // reset next_poll_at to now
  db.prepare('UPDATE feeds SET next_poll_at = ? WHERE id = ?').run(
    new Date().toISOString(),
    feedOk.id,
  );
  await poller.pollOnce();
  const itemsAfterSecond = db
    .prepare('SELECT COUNT(*) AS c FROM items WHERE feed_id = ?')
    .get(feedOk.id)! as { c: number };
  assert.equal(itemsAfterSecond.c, 1);

  // 304 skips work: etag stored; force due and send etag
  const etag = (
    db.prepare('SELECT etag FROM feeds WHERE id = ?').get(feedOk.id)! as { etag: string }
  ).etag;
  assert.equal(etag, '"etag1"');
  db.prepare('UPDATE feeds SET next_poll_at = ? WHERE id = ?').run(
    new Date().toISOString(),
    feedOk.id,
  );
  await poller.pollOnce();
  // still 1 item
  assert.equal(
    (
      db.prepare('SELECT COUNT(*) AS c FROM items WHERE feed_id = ?').get(feedOk.id)! as {
        c: number;
      }
    ).c,
    1,
  );

  // failing feed backed off
  const failRow = db
    .prepare('SELECT failure_count, last_error, next_poll_at FROM feeds WHERE id = ?')
    .get(feedFail.id)! as {
    failure_count: number;
    last_error: string | null;
    next_poll_at: string;
  };
  assert.ok(failRow.failure_count >= 1);
  assert.ok(failRow.last_error !== null);
  assert.match(failRow.last_error!, /HTTP 500/);
  // other feed still ok
  const okRow = db
    .prepare('SELECT failure_count, last_error FROM feeds WHERE id = ?')
    .get(feedOk.id)! as { failure_count: number; last_error: string | null };
  assert.equal(okRow.failure_count, 0);
  assert.equal(okRow.last_error, null);

  server.close();
  db.close();
});
