import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { after, before, describe, test } from 'node:test';

import { closeDatabase, db } from '../db.js';
import {
  createIfAbsent,
  del,
  deleteKey,
  deleteKeyIfTokenMatches,
  get,
  getByPrefix,
  getByPrefixWithKeys,
  getValue,
  listByPrefix,
  mdel,
  mget,
  mset,
  set,
} from './kv-store.js';

const namespace = `__kv_repository_test__:${randomUUID()}:`;

async function cleanNamespace(): Promise<void> {
  await db.query(
    `DELETE FROM public.kv_store_7249dcd9
     WHERE left(key, char_length($1)) = $1`,
    [namespace],
  );
}

describe('PostgreSQL KV repository', { concurrency: false }, () => {
  before(async () => {
    const result = await db.query<{ table_name: string | null }>(
      `SELECT to_regclass('public.kv_store_7249dcd9')::text AS table_name`,
    );

    assert.equal(
      result.rows[0]?.table_name,
      'kv_store_7249dcd9',
      'Run the local database migrations before the KV repository tests.',
    );
  });

  after(async () => {
    try {
      await cleanNamespace();
    } finally {
      await closeDatabase();
    }
  });

  test('set, get and del preserve JSON values and missing-key semantics', async () => {
    const key = `${namespace}single:'; DROP TABLE kv_store_7249dcd9; --`;
    const value = {
      active: true,
      count: 3,
      nested: { labels: ['um', 'dois'] },
    };

    assert.equal(await get(`${namespace}missing`), undefined);

    await set(key, value);
    assert.deepEqual(await get(key), value);

    await set(key, 'updated');
    assert.equal(await get(key), 'updated');

    await del(key);
    await del(key);
    assert.equal(await get(key), undefined);
  });

  test('mset is atomic and mget preserves order, duplicates and missing slots', async () => {
    const first = `${namespace}batch:first`;
    const second = `${namespace}batch:second`;
    const third = `${namespace}batch:third`;
    const missing = `${namespace}batch:missing`;

    await mset(
      [first, second, third],
      [{ id: 1 }, { id: 2 }, { id: 3 }],
    );

    assert.deepEqual(await mget([third, missing, first, third]), [
      { id: 3 },
      undefined,
      { id: 1 },
      { id: 3 },
    ]);
    assert.deepEqual(await mget([]), []);

    const atomicFirst = `${namespace}atomic:first`;
    const atomicSecond = `${namespace}atomic:second`;
    await assert.rejects(
      mset([atomicFirst, atomicSecond], [{ stored: false }, undefined]),
      /representable as JSON/,
    );
    assert.equal(await get(atomicFirst), undefined);

    await assert.rejects(mset([atomicFirst], []), /one value for every key/);
    assert.equal(await get(atomicFirst), undefined);

    await mdel([first, third]);
    assert.deepEqual(await mget([first, second, third]), [
      undefined,
      { id: 2 },
      undefined,
    ]);
    await mdel([]);
  });

  test('prefix reads treat percent, underscore and backslash as literals', async () => {
    const cases = [
      {
        prefix: `${namespace}percent%`,
        nearKey: `${namespace}percent-not-literal`,
      },
      {
        prefix: `${namespace}underscore_`,
        nearKey: `${namespace}underscore-not-literal`,
      },
      {
        prefix: `${namespace}backslash\\`,
        nearKey: `${namespace}backslash-not-literal`,
      },
    ];

    for (const [index, current] of cases.entries()) {
      const matchingKey = `${current.prefix}match`;
      const matchingValue = { match: index };
      await mset(
        [matchingKey, current.nearKey],
        [matchingValue, { near: index }],
      );

      assert.deepEqual(await getByPrefix(current.prefix), [matchingValue]);
      assert.deepEqual(await getByPrefixWithKeys(current.prefix), [
        { key: matchingKey, value: matchingValue },
      ]);
      assert.deepEqual(await listByPrefix(current.prefix), [
        { key: matchingKey, value: matchingValue },
      ]);
    }

    const nullKey = `${namespace}nullable:value`;
    await set(nullKey, null);
    assert.deepEqual(await getByPrefix(`${namespace}nullable:`), []);
    assert.deepEqual(await getByPrefixWithKeys(`${namespace}nullable:`), []);
    assert.deepEqual(await listByPrefix(`${namespace}nullable:`), [
      { key: nullKey, value: null },
    ]);
  });

  test('createIfAbsent and conditional deletion remain atomic under contention', async () => {
    const lockKey = `${namespace}lock`;
    const candidates = Array.from({ length: 12 }, (_, index) => ({
      token: `token-${index}`,
      owner: index,
    }));
    const results = await Promise.all(
      candidates.map((candidate) => createIfAbsent(lockKey, candidate)),
    );
    const winnerIndex = results.findIndex((result) => result.created);

    assert.notEqual(winnerIndex, -1);
    assert.equal(
      results.filter((result) => result.created).length,
      1,
    );
    assert.deepEqual(await getValue(lockKey), candidates[winnerIndex]);
    assert.equal(await getValue(`${namespace}no-lock`), null);

    assert.equal(await deleteKeyIfTokenMatches(lockKey, 'wrong-token'), false);
    assert.deepEqual(await getValue(lockKey), candidates[winnerIndex]);
    assert.equal(
      await deleteKeyIfTokenMatches(lockKey, candidates[winnerIndex]!.token),
      true,
    );
    assert.equal(await deleteKeyIfTokenMatches(lockKey, 'wrong-token'), false);

    const ordinaryKey = `${namespace}delete-key`;
    await set(ordinaryKey, { exists: true });
    await deleteKey(ordinaryKey);
    await deleteKey(ordinaryKey);
    assert.equal(await getValue(ordinaryKey), null);
  });
});
