import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { startWebServer } from './helpers/server';
import { initializeDatabase, ensureUserExists } from '../../src/db';

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

import { toIsoSql } from '../helpers/dates';

describe('Web API - GET /api/me/tasks', () => {
	const USER = '34600123456';
	const OTHER = '34600999888';
	const GROUP_OK = '123@g.us';
	const GROUP_BAD = '999@g.us';

	let dbPath: string;
	let server: Awaited<ReturnType<typeof startWebServer>> | null = null;
	let tmpDir: string;
	let sid = 'sid-test-tasks';

	beforeAll(async () => {
		tmpDir = mkdtempSync(join(tmpdir(), 'webtest-'));
		dbPath = join(tmpDir, 'tasks.db');

		const db = new Database(dbPath);
		initializeDatabase(db);

		// Asegurar usuarios
		const uid = ensureUserExists(USER, db)!;
		const oid = ensureUserExists(OTHER, db)!;

		// Sembrar grupos y gating
		db.prepare(`
			INSERT INTO groups (id, community_id, name, active, last_verified)
			VALUES (?, 'comm-1', 'Group OK', 1, strftime('%Y-%m-%d %H:%M:%f','now'))
		`).run(GROUP_OK);
		db.prepare(`
			INSERT INTO groups (id, community_id, name, active, last_verified)
			VALUES (?, 'comm-1', 'Group BAD', 1, strftime('%Y-%m-%d %H:%M:%f','now'))
		`).run(GROUP_BAD);

		db.prepare(`INSERT INTO allowed_groups (group_id, status, updated_at) VALUES (?, 'allowed', strftime('%Y-%m-%d %H:%M:%f','now'))`).run(GROUP_OK);
		db.prepare(`INSERT INTO allowed_groups (group_id, status, updated_at) VALUES (?, 'blocked', strftime('%Y-%m-%d %H:%M:%f','now'))`).run(GROUP_BAD);

		// Membresía activa solo en GROUP_OK
		const nowIso = toIsoSql(new Date());
		db.prepare(`
			INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at, last_role_change_at)
			VALUES (?, ?, 0, 1, ?, ?, ?)
		`).run(GROUP_OK, uid, nowIso, nowIso, nowIso);

		// Tareas en GROUP_OK: una con due_date, otra sin due_date
		const insertTask = db.prepare(`
			INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
			VALUES (?, ?, ?, ?, ?)
		`);
		const insertAssign = db.prepare(`
			INSERT INTO task_assignments (task_id, user_id, assigned_by)
			VALUES (?, ?, ?)
		`);

		const r1 = insertTask.run('alpha', '2025-01-02', GROUP_OK, uid, 101) as any;
		const t1 = Number(r1.lastInsertRowid);
		insertAssign.run(t1, uid, uid);

		const r2 = insertTask.run('beta_100% exacta', null, GROUP_OK, uid, 102) as any;
		const t2 = Number(r2.lastInsertRowid);
		insertAssign.run(t2, uid, uid);

		// Tarea en GROUP_BAD asignada al usuario (debe ser filtrada por gating)
		const r3 = insertTask.run('gamma filtrada', '2025-02-01', GROUP_BAD, oid, 103) as any;
		const t3 = Number(r3.lastInsertRowid);
		insertAssign.run(t3, uid, oid);

		// Crear sesión válida
		const hash = await sha256Hex(sid);
		db.prepare(`
			INSERT INTO web_sessions (session_hash, user_id, created_at, last_seen_at, expires_at)
			VALUES (?, ?, ?, ?, ?)
		`).run(hash, uid, nowIso, nowIso, toIsoSql(new Date(Date.now() + 60 * 60 * 1000)));

		db.close();

		// Arrancar servidor
		server = await startWebServer({
			port: 19101,
			env: { DB_PATH: dbPath, TZ: 'UTC' }
		});
	});

	afterAll(async () => {
		try { await server?.stop(); } catch {}
		try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
	});

	it('aplica gating y ordena por due_date asc con NULL al final', async () => {
		const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(res.status).toBe(200);
		const json = await res.json();
		const ids = json.items.map((it: any) => it.display_code ?? it.id);
		// Deben venir solo t1 (101) y t2 (102), en ese orden (t1 con fecha primero, luego NULL)
		expect(ids).toEqual([101, 102]);
	});

	it('filtra por búsqueda escapando % y _ correctamente', async () => {
		const q = encodeURIComponent('beta_100%');
		const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(res.status).toBe(200);
		const json = await res.json();
		expect(json.items.length).toBe(1);
		expect(json.items[0].description).toContain('beta_100%');
	});

	it('permite actualizar la descripción de una tarea con PATCH', async () => {
		// Localizar la tarea por búsqueda
		const q = encodeURIComponent('beta_100%');
		const listRes = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(listRes.status).toBe(200);
		const list = await listRes.json();
		expect(list.items.length).toBe(1);
		const taskId = list.items[0].id;

		// Actualizar descripción
		const newDesc = 'beta renombrada';
		const patchRes = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
			method: 'PATCH',
			headers: {
				Cookie: `sid=${sid}`,
				'content-type': 'application/json'
			},
			body: JSON.stringify({ description: newDesc })
		});
		expect(patchRes.status).toBe(200);
		const patched = await patchRes.json();
		expect(patched.task.description).toBe(newDesc);

		// Verificar en el listado
		const q2 = encodeURIComponent('renombrada');
		const listRes2 = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q2}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(listRes2.status).toBe(200);
		const list2 = await listRes2.json();
		expect(list2.items.length).toBe(1);
		expect(list2.items[0].description).toContain('renombrada');
	});

	it('rechaza descripciones vacías o demasiado largas', async () => {
		// Reutiliza la misma tarea localizada arriba (buscar por 'renombrada')
		const q = encodeURIComponent('renombrada');
		const listRes = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(listRes.status).toBe(200);
		const list = await listRes.json();
		expect(list.items.length).toBe(1);
		const taskId = list.items[0].id;

		// Vacía/espacios
		const resEmpty = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
			method: 'PATCH',
			headers: {
				Cookie: `sid=${sid}`,
				'content-type': 'application/json'
			},
			body: JSON.stringify({ description: '   ' })
		});
		expect(resEmpty.status).toBe(400);

		// Demasiado larga (>1000)
		const longText = 'a'.repeat(1001);
		const resLong = await fetch(`${server!.baseUrl}/api/tasks/${taskId}`, {
			method: 'PATCH',
			headers: {
				Cookie: `sid=${sid}`,
				'content-type': 'application/json'
			},
			body: JSON.stringify({ description: longText })
		});
		expect(resLong.status).toBe(400);
	});

	it('permite completar una tarea asignada y aparece en recent', async () => {
		// Buscar una tarea abierta por texto
		const q = encodeURIComponent('alpha');
		const listRes = await fetch(`${server!.baseUrl}/api/me/tasks?limit=10&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(listRes.status).toBe(200);
		const list = await listRes.json();
		expect(list.items.length).toBe(1);
		const taskId = list.items[0].id;

		// Completar
		const resComplete = await fetch(`${server!.baseUrl}/api/tasks/${taskId}/complete`, {
			method: 'POST',
			headers: { Cookie: `sid=${sid}` }
		});
		expect(resComplete.status).toBe(200);
		const done = await resComplete.json();
		expect(done.status === 'updated' || done.status === 'already').toBe(true);

		// Ver en recent
		const recentRes = await fetch(`${server!.baseUrl}/api/me/tasks?status=recent&limit=10`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(recentRes.status).toBe(200);
		const recent = await recentRes.json();
		const ids = recent.items.map((it: any) => it.id);
		expect(ids.includes(taskId)).toBe(true);
	});

	it('oculta tareas de grupo archivado aunque esté asignada y allowed', async () => {
		// Abrir la misma DB del servidor para sembrar datos adicionales
		const db2 = new Database(dbPath);
		const nowIso = toIsoSql(new Date());

		// Grupo activo inicialmente
		db2.prepare(`
			INSERT INTO groups (id, community_id, name, active, last_verified, archived)
			VALUES (?, 'comm-1', 'Group ARCH', 1, ?, 0)
		`).run('arch@g.us', nowIso);
		db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('arch@g.us','allowed',?)`).run(nowIso);
		db2.prepare(`
			INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
			VALUES ('arch@g.us', ?, 0, 1, ?, ?)
		`).run(USER, nowIso, nowIso);

		// Tarea asignada al usuario en ese grupo
		const r = db2.prepare(`
			INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
			VALUES ('delta archivada', '2025-03-01', 'arch@g.us', ?, 150)
		`).run(USER) as any;
		const tid = Number(r.lastInsertRowid);
		db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);

		// Archivar el grupo
		db2.prepare(`UPDATE groups SET archived = 1 WHERE id = 'arch@g.us'`).run();

		db2.close();

		const q = encodeURIComponent('delta');
		const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=50&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(res.status).toBe(200);
		const json = await res.json();
		const descs = json.items.map((it: any) => String(it.description));
		expect(descs.some((d: string) => d.includes('delta archivada'))).toBe(false);
	});

	it('oculta tareas de grupo inactivo (active=0) aunque esté asignada y allowed', async () => {
		const db2 = new Database(dbPath);
		const nowIso = toIsoSql(new Date());

		db2.prepare(`
			INSERT INTO groups (id, community_id, name, active, last_verified, archived)
			VALUES ('inact@g.us', 'comm-1', 'Group INACT', 1, ?, 0)
		`).run(nowIso);
		db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('inact@g.us','allowed',?)`).run(nowIso);
		db2.prepare(`
			INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
			VALUES ('inact@g.us', ?, 0, 1, ?, ?)
		`).run(USER, nowIso, nowIso);

		const r = db2.prepare(`
			INSERT INTO tasks (description, due_date, group_id, created_by, display_code)
			VALUES ('omega inactiva', '2025-04-01', 'inact@g.us', ?, 151)
		`).run(USER) as any;
		const tid = Number(r.lastInsertRowid);
		db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);

		// Desactivar el grupo
		db2.prepare(`UPDATE groups SET active = 0 WHERE id = 'inact@g.us'`).run();
		db2.close();

		const q = encodeURIComponent('omega inactiva');
		const res = await fetch(`${server!.baseUrl}/api/me/tasks?limit=50&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(res.status).toBe(200);
		const json = await res.json();
		expect(json.items.length).toBe(0);
	});

	it('recent no muestra tareas completadas de grupos archivados', async () => {
		const db2 = new Database(dbPath);
		const nowIso = toIsoSql(new Date());

		db2.prepare(`
			INSERT INTO groups (id, community_id, name, active, last_verified, archived)
			VALUES ('rec@g.us', 'comm-1', 'Group REC', 1, ?, 0)
		`).run(nowIso);
		db2.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, status, updated_at) VALUES ('rec@g.us','allowed',?)`).run(nowIso);
		db2.prepare(`
			INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
			VALUES ('rec@g.us', ?, 0, 1, ?, ?)
		`).run(USER, nowIso, nowIso);

		const r = db2.prepare(`
			INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at, display_code)
			VALUES ('epsilon reciente', '2025-05-01', 'rec@g.us', ?, 1, ?, 152)
		`).run(USER, nowIso) as any;
		const tid = Number(r.lastInsertRowid);
		db2.prepare(`INSERT INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?)`).run(tid, USER, USER);

		// Archivar el grupo
		db2.prepare(`UPDATE groups SET archived = 1 WHERE id = 'rec@g.us'`).run();
		db2.close();

		const q = encodeURIComponent('epsilon reciente');
		const res = await fetch(`${server!.baseUrl}/api/me/tasks?status=recent&limit=50&search=${q}`, {
			headers: { Cookie: `sid=${sid}` }
		});
		expect(res.status).toBe(200);
		const json = await res.json();
		expect(json.items.length).toBe(0);
	});
});
