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 { createApp } from '../src/app.ts';
import { insertItem } from '../src/db/repos/items.ts';
import type { Application } from 'express';
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(app: Application): Promise<StartedServer> {
  const server = http.createServer(app);
  return new Promise((resolve) =>
    server.listen(0, '127.0.0.1', () =>
      resolve({ server, port: (server.address() as AddressInfo).port }),
    ),
  );
}

function makeRss(n: number): string {
  const items = Array.from({ length: n }, (_, i) => {
    const d = new Date(Date.UTC(2024, 0, n - i));
    return `<item><title>Item ${i + 1}</title><link>https://x.com/${i + 1}</link><guid>g${i + 1}</guid><pubDate>${d.toUTCString()}</pubDate><description>desc ${i + 1}</description></item>`;
  }).join('');
  return `<?xml version="1.0"?><rss version="2.0"><channel><title>t</title><link>https://x.com</link>${items}</channel></rss>`;
}

interface ApiResponse {
  status: number;
  json: any;
  setCookie: string;
}

async function req(
  base: string,
  cookie: string,
  method: string,
  path: string,
  body?: unknown,
  csrf?: string,
): Promise<ApiResponse> {
  const headers: Record<string, string> = {};
  if (cookie) headers.Cookie = cookie;
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  if (csrf) headers['X-CSRF-Token'] = csrf;
  const res = await fetch(base + path, {
    method,
    headers,
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  const setCookie = res.headers.get('set-cookie') ?? '';
  const text = await res.text();
  let json: any = null;
  try {
    json = JSON.parse(text);
  } catch {
    /* ignore */
  }
  return { status: res.status, json, setCookie };
}

test('sessions work over plain HTTP in production', async () => {
  const db = openDatabase(':memory:');
  runMigrations(db, migrationsDir);
  const app = createApp({
    config: { SESSION_SECRET: 'test-secret-at-least-16', NODE_ENV: 'production' },
    db,
  });
  const { server, port } = await startServer(app);
  const base = `http://127.0.0.1:${port}`;

  try {
    const reg = await req(base, '', 'POST', '/api/auth/register', {
      email: 'prod@example.com',
      password: 'password123',
    });
    assert.equal(reg.status, 201);
    // Served over HTTP, so the cookie must not be marked Secure or browsers drop it.
    assert.doesNotMatch(reg.setCookie, /;\s*secure/i);
    const cookie = reg.setCookie.split(';')[0];

    const me = await req(base, cookie, 'GET', '/api/auth/me');
    assert.equal(me.status, 200);
    assert.equal(me.json.email, 'prod@example.com');

    const login = await req(base, '', 'POST', '/api/auth/login', {
      email: 'prod@example.com',
      password: 'password123',
    });
    assert.equal(login.status, 200);
    const loginCookie = login.setCookie.split(';')[0];
    assert.equal((await req(base, loginCookie, 'GET', '/api/auth/me')).status, 200);

    // /me is called on every page load and must not hit the auth rate limit.
    let last: ApiResponse | undefined;
    for (let i = 0; i < 12; i++) last = await req(base, cookie, 'GET', '/api/auth/me');
    assert.equal(last!.status, 200);
  } finally {
    server.close();
    db.close();
  }
});

test('api auth, validation, pagination, isolation', async () => {
  // fake feed server
  const rss5 = makeRss(5);
  const feedServer = http.createServer((_req, res) => {
    res.setHeader('Content-Type', 'application/rss+xml');
    res.end(rss5);
  });
  await new Promise<void>((resolve) => feedServer.listen(0, '127.0.0.1', resolve));
  const feedPort = (feedServer.address() as AddressInfo).port;
  const feedUrl = `http://127.0.0.1:${feedPort}/feed.xml`;

  const db = openDatabase(':memory:');
  runMigrations(db, migrationsDir);
  const app = createApp({
    config: { SESSION_SECRET: 'test-secret-at-least-16', NODE_ENV: 'test' },
    db,
    guard: async () => {},
  });
  const { server, port } = await startServer(app);
  const base = `http://127.0.0.1:${port}`;

  try {
    // auth required
    const unauth = await req(base, '', 'GET', '/api/items');
    assert.equal(unauth.status, 401);

    // validation error
    const badReg = await req(base, '', 'POST', '/api/auth/register', {
      email: 'nope',
      password: 'short',
    });
    assert.equal(badReg.status, 400);

    // register user1
    const r1 = await req(base, '', 'POST', '/api/auth/register', {
      email: 'u1@example.com',
      password: 'password123',
    });
    assert.equal(r1.status, 201);
    const cookie1 = r1.setCookie.split(';')[0];
    const csrf1 = (await req(base, cookie1, 'GET', '/api/csrf')).json.token;

    // register user2
    const r2 = await req(base, '', 'POST', '/api/auth/register', {
      email: 'u2@example.com',
      password: 'password123',
    });
    assert.equal(r2.status, 201);
    const cookie2 = r2.setCookie.split(';')[0];
    const csrf2 = (await req(base, cookie2, 'GET', '/api/csrf')).json.token;

    // subscribe user1
    const sub1 = await req(base, cookie1, 'POST', '/api/subscriptions', { url: feedUrl }, csrf1);
    assert.equal(sub1.status, 201);
    const feedId = sub1.json.id;

    // subscribe user2
    const sub2 = await req(base, cookie2, 'POST', '/api/subscriptions', { url: feedUrl }, csrf2);
    assert.equal(sub2.status, 201);

    // list items user1
    const list1 = await req(base, cookie1, 'GET', '/api/items?limit=2');
    assert.equal(list1.status, 200);
    assert.equal(list1.json.items.length, 2);
    assert.equal(list1.json.items[0].read, false);

    // unread count user1
    const uc1 = await req(base, cookie1, 'GET', '/api/items/unread-count');
    assert.equal(uc1.json.count, 5);

    // mark first item read
    const firstId = list1.json.items[0].id;
    const mr = await req(base, cookie1, 'POST', `/api/items/${firstId}/read`, undefined, csrf1);
    assert.equal(mr.status, 204);

    // user1 unread count now 4
    const uc1b = await req(base, cookie1, 'GET', '/api/items/unread-count');
    assert.equal(uc1b.json.count, 4);

    // user2 still 5 (isolation)
    const uc2 = await req(base, cookie2, 'GET', '/api/items/unread-count');
    assert.equal(uc2.json.count, 5);

    // pagination stability: insert new item at top, then fetch next page using cursor from first list
    const cursor = list1.json.nextCursor;
    // insert a newer item into db directly
    const now = new Date().toISOString();
    insertItem(db, {
      feed_id: feedId,
      dedup_key: 'hash:newitem',
      guid: 'newguid',
      url: 'https://x.com/new',
      title: 'New Item',
      content_html: '<p>new</p>',
      content_text: 'new',
      author: null,
      published_at: '2025-01-01T00:00:00.000Z',
      fetched_at: now,
    });
    const page2 = await req(base, cookie1, 'GET', `/api/items?limit=2&cursor=${cursor}`);
    assert.equal(page2.status, 200);
    // should not include the newly inserted top item; should continue with old items 3,4
    assert.equal(page2.json.items.length, 2);
    assert.ok(!page2.json.items.some((i: { title: string }) => i.title === 'New Item'));

    // bulk mark read
    const ids = page2.json.items.map((i: { id: number }) => i.id);
    const bulk = await req(base, cookie1, 'POST', '/api/items/read', { ids }, csrf1);
    assert.equal(bulk.status, 204);

    // logout
    const lo = await req(base, cookie1, 'POST', '/api/auth/logout', undefined, csrf1);
    assert.equal(lo.status, 204);
  } finally {
    server.close();
    feedServer.close();
    db.close();
  }
});
