import { describe, it, expect, afterAll } from 'bun:test';
import Database from 'bun:sqlite';
import { startWebServer } from './helpers/server';
import { createTempDb } from './helpers/db';

import { sha256Hex } from '../helpers/crypto';

import { toIsoSql, ymdUTC, addDays } from '../helpers/dates';



function pad4(n: number): string {
  const s = String(Math.floor(n));
  return s.length >= 4 ? s : '0'.repeat(4 - s.length) + s;
}

describe('ICS - aggregate feed', () => {
  const PORT = 19133;
  const BASE = `http://127.0.0.1:${PORT}`;
  const USER = '34600123456';
  const G1 = 'g1@g.us';
  const G2 = 'g2@g.us';
  const G3 = 'g3@g.us'; // no permitido
  const SID = 'sid-ics-aggregate-1';

  const tmp = createTempDb();
  const db: any = tmp.db as Database;

  // Sembrar datos mínimos
  db.exec(`INSERT OR IGNORE INTO users (id) VALUES ('${USER}')`);
  for (const [gid, name] of [
    [G1, 'Group A'],
    [G2, 'Group B'],
    [G3, 'Group C'],
  ] as const) {
    db.exec(
      `INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${gid}', 'comm', '${name}', 1)`
    );
  }
  db.exec(
    `INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G1}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
  );
  db.exec(
    `INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G2}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`
  );
  db.exec(
    `INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${G3}', 'blocked', '${toIsoSql()}', '${toIsoSql()}')`
  );
  for (const gid of [G1, G2]) {
    db.exec(
      `INSERT OR IGNORE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
       VALUES ('${gid}', '${USER}', 0, 1, '${toIsoSql()}', '${toIsoSql()}')`
    );
  }

  const today = new Date();
  const dueIn3 = ymdUTC(addDays(today, 3));
  const dueIn6 = ymdUTC(addDays(today, 6));
  const dueFar = ymdUTC(addDays(today, 400));

  const insTask = db.prepare(
    `INSERT INTO tasks (description, due_date, group_id, created_by, completed, created_at)
     VALUES (?, ?, ?, ?, ?, ?)`
  );
  const createdBy = USER;

  // G1 unassigned -> incluido
  const r1 = insTask.run('G1 unassigned', dueIn3, G1, createdBy, 0, toIsoSql());
  const t1 = Number(r1.lastInsertRowid);

  // G1 assigned -> excluido (aggregate sólo "sin responsable")
  const r2 = insTask.run('G1 assigned', dueIn3, G1, createdBy, 0, toIsoSql());
  const t2 = Number(r2.lastInsertRowid);
  db.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by, assigned_at)
              VALUES (?, ?, ?, ?)`).run(t2, USER, USER, toIsoSql());

  // G2 unassigned -> incluido
  const r3 = insTask.run('G2 unassigned', dueIn6, G2, createdBy, 0, toIsoSql());
  const t3 = Number(r3.lastInsertRowid);

  // G3 unassigned (no permitido) -> excluido
  const r4 = insTask.run('G3 unassigned', dueIn6, G3, createdBy, 0, toIsoSql());
  const t4 = Number(r4.lastInsertRowid);

  // G1 far future -> excluido por horizonte
  const r5 = insTask.run('G1 far', dueFar, G1, createdBy, 0, toIsoSql());
  const t5 = Number(r5.lastInsertRowid);

  const sidHashPromise = sha256Hex(SID);
  const serverPromise = startWebServer({
    port: PORT,
    env: {
      DB_PATH: tmp.path,
      WEB_BASE_URL: BASE,
    },
  });

  let server: Awaited<typeof serverPromise> | null = null;

  afterAll(async () => {
    try {
      await server?.stop();
    } catch {}
    try {
      tmp.cleanup();
    } catch {}
  });

  it('serves ICS for aggregate token with correct filtering, supports ETag, and returns 410 when revoked', async () => {
    server = await serverPromise;

    const sidHash = await sidHashPromise;
    db.exec(`
      INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at)
      VALUES ('sess-ics-aggregate', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(
      addDays(new Date(), 1)
    )}')
    `);

    // Obtener URLs de feeds
    const resFeeds = await fetch(`${BASE}/api/integrations/feeds`, {
      headers: { cookie: `sid=${SID}` },
    });
    expect(resFeeds.status).toBe(200);
    const feeds = await resFeeds.json();
    expect(feeds.aggregate && typeof feeds.aggregate.url === 'string').toBe(true);

    const aggregateUrl: string = feeds.aggregate.url;
    const token = new URL(aggregateUrl).pathname.split('/').pop()!.replace(/\.ics$/i, '');

    // Primera petición ICS
    const resIcs = await fetch(aggregateUrl);
    expect(resIcs.status).toBe(200);
    expect((resIcs.headers.get('content-type') || '').includes('text/calendar')).toBe(true);
    const body1 = await resIcs.text();

    // Incluidos: t1, t3
    expect(body1.includes(`[T${pad4(t1)}]`)).toBe(true);
    expect(body1.includes(`[T${pad4(t3)}]`)).toBe(true);

    // Excluidos: t2 (assigned), t4 (no permitido), t5 (far)
    expect(body1.includes(`[T${pad4(t2)}]`)).toBe(false);
    expect(body1.includes(`[T${pad4(t4)}]`)).toBe(false);
    expect(body1.includes(`[T${pad4(t5)}]`)).toBe(false);

    const etag = resIcs.headers.get('etag') || '';
    const res304 = await fetch(aggregateUrl, { headers: { 'if-none-match': etag } });
    expect(res304.status).toBe(304);

    const row = db
      .prepare(`SELECT last_used_at FROM calendar_tokens WHERE token_plain = ?`)
      .get(token) as any;
    expect(row && row.last_used_at).toBeTruthy();

    db.prepare(`UPDATE calendar_tokens SET revoked_at = ? WHERE token_plain = ?`).run(toIsoSql(), token);
    const resGone = await fetch(aggregateUrl);
    expect(resGone.status).toBe(410);
  });
});
