import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { tmpdir } from 'os';
import { join } from 'path';
import { unlinkSync } from 'fs';
import { SqliteInboxStore } from '../src/sqlite-inbox-store.js';

function randBytes(n: number): Uint8Array {
  const buf = new Uint8Array(n);
  globalThis.crypto.getRandomValues(buf);
  return buf;
}

function tempDbPath(): string {
  return join(
    tmpdir(),
    `shade-inbox-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`,
  );
}

describe('SqliteInboxStore', () => {
  let dbPath: string;
  let store: SqliteInboxStore;

  beforeEach(() => {
    dbPath = tempDbPath();
    store = new SqliteInboxStore(dbPath);
  });

  afterEach(() => {
    store.close();
    try {
      unlinkSync(dbPath);
    } catch {}
    try {
      unlinkSync(dbPath + '-wal');
    } catch {}
    try {
      unlinkSync(dbPath + '-shm');
    } catch {}
  });

  test('owner save + get', async () => {
    const key = randBytes(32);
    await store.saveAddressOwner('bob', key);
    const got = await store.getAddressOwner('bob');
    expect(got).not.toBeNull();
    expect(got!).toEqual(key);
  });

  test('putBlob is idempotent on (address, msgId)', async () => {
    const ct = randBytes(64);
    const a = await store.putBlob({
      address: 'bob',
      msgId: 'a'.repeat(64),
      ciphertext: ct,
      expiresAt: Date.now() + 60_000,
    });
    expect(a.created).toBe(true);
    const b = await store.putBlob({
      address: 'bob',
      msgId: 'a'.repeat(64),
      ciphertext: ct,
      expiresAt: Date.now() + 60_000,
    });
    expect(b.created).toBe(false);
    expect(b.receivedAt).toBe(a.receivedAt);
  });

  test('fetchBlobs respects sinceCursor and expires_at', async () => {
    const now = Date.now();
    await store.putBlob({
      address: 'bob',
      msgId: '1'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now + 60_000,
    });
    await store.putBlob({
      address: 'bob',
      msgId: '2'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now - 1000, // already expired
    });
    await store.putBlob({
      address: 'bob',
      msgId: '3'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now + 60_000,
    });

    const all = await store.fetchBlobs({
      address: 'bob',
      sinceCursor: 0,
      now,
      limit: 100,
    });
    // Expired one filtered out.
    expect(all.length).toBe(2);
    expect(all.map((r) => r.msgId).sort()).toEqual(['1'.repeat(64), '3'.repeat(64)]);

    // Cursor advances strictly.
    const half = await store.fetchBlobs({
      address: 'bob',
      sinceCursor: all[0]!.receivedAt,
      now,
      limit: 100,
    });
    expect(half.length).toBe(1);
    expect(half[0]!.msgId).toBe(all[1]!.msgId);
  });

  test('purgeExpired removes only expired rows', async () => {
    const now = Date.now();
    await store.putBlob({
      address: 'bob',
      msgId: '1'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now - 1,
    });
    await store.putBlob({
      address: 'bob',
      msgId: '2'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now + 60_000,
    });
    const removed = await store.purgeExpired(now);
    expect(removed).toBe(1);
    const remaining = await store.fetchBlobs({
      address: 'bob',
      sinceCursor: 0,
      now,
      limit: 10,
    });
    expect(remaining.length).toBe(1);
    expect(remaining[0]!.msgId).toBe('2'.repeat(64));
  });

  test('persists across reopen', async () => {
    const key = randBytes(32);
    await store.saveAddressOwner('bob', key);
    const ct = randBytes(64);
    await store.putBlob({
      address: 'bob',
      msgId: '5'.repeat(64),
      ciphertext: ct,
      expiresAt: Date.now() + 60_000,
    });
    store.close();
    store = new SqliteInboxStore(dbPath);
    const got = await store.getAddressOwner('bob');
    expect(got).toEqual(key);
    const blobs = await store.fetchBlobs({
      address: 'bob',
      sinceCursor: 0,
      now: Date.now(),
      limit: 10,
    });
    expect(blobs.length).toBe(1);
    expect(blobs[0]!.ciphertext).toEqual(ct);
  });

  test('deleteAddress drops owner + blobs', async () => {
    const key = randBytes(32);
    await store.saveAddressOwner('bob', key);
    await store.putBlob({
      address: 'bob',
      msgId: '7'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: Date.now() + 60_000,
    });
    await store.deleteAddress('bob');
    expect(await store.getAddressOwner('bob')).toBeNull();
    const blobs = await store.fetchBlobs({
      address: 'bob',
      sinceCursor: 0,
      now: Date.now(),
      limit: 10,
    });
    expect(blobs.length).toBe(0);
  });

  test('countBlobs ignores expired entries', async () => {
    const now = Date.now();
    await store.putBlob({
      address: 'bob',
      msgId: '1'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now - 1,
    });
    await store.putBlob({
      address: 'bob',
      msgId: '2'.repeat(64),
      ciphertext: randBytes(8),
      expiresAt: now + 60_000,
    });
    expect(await store.countBlobs('bob', now)).toBe(1);
  });
});
