import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { Database } from 'bun:sqlite';
import { initializeDatabase } from '../../../src/db';
import { ResponseQueue } from '../../../src/services/response-queue';
import { toIsoSql } from '../../helpers/dates';
import { setDb, resetDb } from '../../../src/db/locator';

let testDb: Database;
let envBackup: NodeJS.ProcessEnv;

describe('ResponseQueue (persistent add)', () => {
  beforeAll(() => {
    envBackup = { ...process.env };
    testDb = new Database(':memory:');
    initializeDatabase(testDb);
    // Inyectar DB de pruebas vía locator
    setDb(testDb);
  });

  afterAll(() => {
    process.env = envBackup;
    // No cerramos ni restablecemos la DB aquí; se hará al final del bloque de reintentos.
  });

  beforeEach(() => {
    // Limpiar tabla entre tests
    testDb.exec('DELETE FROM response_queue');
    // Valor por defecto del número del bot (se puede cambiar en tests)
    process.env.CHATBOT_PHONE_NUMBER = '1234567890';
  });

  test('should persist queued responses to database', async () => {
    const before = testDb.query("SELECT COUNT(*) as count FROM response_queue").get() as any;
    expect(before.count).toBe(0);

    await ResponseQueue.add([
      { recipient: '111', message: 'hola 1' },
      { recipient: '222', message: 'hola 2' },
    ]);

    const after = testDb.query("SELECT COUNT(*) as count FROM response_queue").get() as any;
    expect(after.count).toBe(2);

    const rows = testDb.query("SELECT recipient, message, status FROM response_queue ORDER BY id").all() as any[];
    expect(rows[0].recipient).toBe('111');
    expect(rows[0].message).toBe('hola 1');
    expect(rows[0].status).toBe('queued');
    expect(rows[1].recipient).toBe('222');
    expect(rows[1].message).toBe('hola 2');
    expect(rows[1].status).toBe('queued');
  });

  test('should skip messages addressed to the bot number', async () => {
    process.env.CHATBOT_PHONE_NUMBER = '555111222';

    await ResponseQueue.add([
      { recipient: '555111222', message: 'no debe encolarse' },
      { recipient: '333', message: 'debe encolarse' },
    ]);

    const count = testDb.query("SELECT COUNT(*) as count FROM response_queue").get() as any;
    expect(count.count).toBe(1);

    const row = testDb.query("SELECT recipient, message FROM response_queue").get() as any;
    expect(row.recipient).toBe('333');
    expect(row.message).toBe('debe encolarse');
  });

  test('should ignore entries without recipient or message', async () => {
    await ResponseQueue.add([
      // inválidos:
      { recipient: '', message: 'sin destinatario' } as any,
      { recipient: '444', message: '' } as any,
      // válido:
      { recipient: '444', message: 'ok' },
    ]);

    const rows = testDb.query("SELECT recipient, message FROM response_queue ORDER BY id").all() as any[];
    expect(rows.length).toBe(1);
    expect(rows[0].recipient).toBe('444');
    expect(rows[0].message).toBe('ok');
  });

  test('should persist mentions in metadata when provided', async () => {
    await ResponseQueue.add([
      { recipient: '555', message: 'hola con menciones', mentions: ['111@s.whatsapp.net', '222@s.whatsapp.net'] },
    ]);

    const row = testDb.query("SELECT metadata FROM response_queue ORDER BY id DESC LIMIT 1").get() as any;
    expect(row).toBeTruthy();
    const meta = JSON.parse(row.metadata);
    expect(Array.isArray(meta.mentioned)).toBe(true);
    expect(meta.mentioned).toEqual(['111@s.whatsapp.net', '222@s.whatsapp.net']);
  });

  test('should throw if database error occurs (e.g., missing table)', async () => {
    // Provocar error: eliminar tabla
    testDb.exec('DROP TABLE response_queue');

    await expect(ResponseQueue.add([{ recipient: '999', message: 'x' }]))
      .rejects
      .toBeTruthy();

    // Restaurar esquema para no afectar otros tests (forzar recreación de tablas)
    testDb.exec('DROP TABLE IF EXISTS schema_migrations');
    initializeDatabase(testDb);
  });
});

describe('ResponseQueue (retries/backoff)', () => {
  // Reutiliza la misma DB inyectada en el bloque anterior
  afterAll(() => {
    // Restablecer locator y cerrar la DB de prueba
    resetDb();
    testDb.close();
  });

  beforeEach(() => {
    // Limpiar tabla entre tests
    testDb.exec('DELETE FROM response_queue');
  });

  function isoNow(): string {
    return toIsoSql(new Date());
  }
  function isoFuture(ms: number): string {
    return toIsoSql(new Date(Date.now() + ms));
  }

  test('claimNextBatch should respect next_attempt_at (only eligible items are claimed)', () => {
    const readyAt = isoNow();
    const laterAt = isoFuture(60_000);

    testDb.prepare(`
      INSERT INTO response_queue (recipient, message, next_attempt_at)
      VALUES ('111', 'ready', ?)
    `).run(readyAt);

    testDb.prepare(`
      INSERT INTO response_queue (recipient, message, next_attempt_at)
      VALUES ('222', 'later', ?)
    `).run(laterAt);

    const claimed = (ResponseQueue as any).claimNextBatch(10) as any[];
    expect(Array.isArray(claimed)).toBe(true);
    expect(claimed.length).toBe(1);
    expect(claimed[0].recipient).toBe('111');
    expect(claimed[0].message).toBe('ready');

    const rows = testDb.query(`SELECT recipient, status FROM response_queue ORDER BY id`).all() as any[];
    expect(rows[0].status).toBe('processing');
    expect(rows[1].status).toBe('queued');
  });

  test('markFailed should set failed status, increment attempts and store status code and error (4xx definitive)', () => {
    const now = isoNow();
    testDb.prepare(`
      INSERT INTO response_queue (recipient, message, next_attempt_at)
      VALUES ('333', 'bad request', ?)
    `).run(now);

    const claimed = (ResponseQueue as any).claimNextBatch(1) as any[];
    expect(claimed.length).toBe(1);
    const item = claimed[0];

    (ResponseQueue as any).markFailed(item.id, 'bad request', 400, (item.attempts || 0) + 1);

    const row = testDb.query(`SELECT status, attempts, last_status_code, last_error FROM response_queue WHERE id = ?`).get(item.id) as any;
    expect(row.status).toBe('failed');
    expect(Number(row.attempts)).toBe(1);
    expect(Number(row.last_status_code)).toBe(400);
    expect(String(row.last_error)).toContain('bad request');
  });

  test('requeueWithBackoff should set queued status, increment attempts and schedule next_attempt_at (5xx retry)', () => {
    const now = isoNow();
    testDb.prepare(`
      INSERT INTO response_queue (recipient, message, next_attempt_at)
      VALUES ('444', 'server error', ?)
    `).run(now);

    const claimed = (ResponseQueue as any).claimNextBatch(1) as any[];
    expect(claimed.length).toBe(1);
    const item = claimed[0];
    const attemptsNow = (item.attempts || 0) + 1;

    const futureWhen = isoFuture(60_000);
    (ResponseQueue as any).requeueWithBackoff(item.id, attemptsNow, futureWhen, 500, 'server error');

    const row = testDb.query(`SELECT status, attempts, next_attempt_at, last_status_code, last_error FROM response_queue WHERE id = ?`).get(item.id) as any;
    expect(row.status).toBe('queued');
    expect(Number(row.attempts)).toBe(attemptsNow);
    expect(String(row.next_attempt_at)).toBe(futureWhen);
    expect(Number(row.last_status_code)).toBe(500);
    expect(String(row.last_error)).toContain('server error');

    // No debería ser reclamable aún
    const claimedAgain = (ResponseQueue as any).claimNextBatch(1) as any[];
    expect(claimedAgain.length).toBe(0);
  });

  test('computeDelayMs should return values in [0, max] for several attempts', () => {
    const attempts = [1, 2, 3, 4, 5];
    for (const a of attempts) {
      const max = Math.min(
        (ResponseQueue as any).MAX_BACKOFF_MS,
        (ResponseQueue as any).BASE_BACKOFF_MS * (2 ** Math.max(0, a - 1))
      );
      // correr varias veces para validar límites (sin probar distribución)
      for (let i = 0; i < 5; i++) {
        const d = (ResponseQueue as any).computeDelayMs(a);
        expect(typeof d).toBe('number');
        expect(d).toBeGreaterThanOrEqual(0);
        expect(d).toBeLessThanOrEqual(max);
      }
    }
  });

  test('add() should persist next_attempt_at (scheduler field) along with queued items', async () => {
    await (ResponseQueue as any).add([
      { recipient: '555', message: 'hola con next' },
    ]);

    const row = testDb.query(`SELECT next_attempt_at, status FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any;
    expect(row).toBeTruthy();
    expect(row.status).toBe('queued');
    expect(row.next_attempt_at).toBeTruthy(); // formato texto no vacío
  });
});
