import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import postgres, { type Sql } from 'postgres';
import { PostgresStorage } from '../src/index.js';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { ShadeSessionManager } from '@shade/core';
import type { IdentityKeyPair } from '@shade/core';

const crypto = new SubtleCryptoProvider();

/**
 * These tests require a running PostgreSQL instance.
 * Skip if SHADE_TEST_PG_URL is not set.
 *
 * To run locally:
 *   docker run -d --name shade-test-pg -e POSTGRES_PASSWORD=test -p 5999:5432 postgres:16
 *   SHADE_TEST_PG_URL=postgres://postgres:test@localhost:5999/postgres bun test postgres-storage
 */
const PG_URL = process.env.SHADE_TEST_PG_URL;

if (!PG_URL) {
  describe.skip('PostgresStorage (skipped — set SHADE_TEST_PG_URL to enable)', () => {
    test('placeholder', () => {});
  });
} else {
  describe('PostgresStorage', () => {
    let sql: Sql;
    let storage: PostgresStorage;

    beforeAll(async () => {
      sql = postgres(PG_URL!);
      // Clean slate
      await sql`DROP TABLE IF EXISTS shade_identity CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_config CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_signed_prekeys CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_one_time_prekeys CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_sessions CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_trusted_identities CASCADE`;
      await sql`DROP TABLE IF EXISTS shade_retired_identities CASCADE`;
      storage = await PostgresStorage.fromClient(sql);
    });

    afterAll(async () => {
      await sql.end();
    });

    test('identity save/load roundtrip', async () => {
      const ikp: IdentityKeyPair = {
        signingPublicKey: crypto.randomBytes(32),
        signingPrivateKey: crypto.randomBytes(32),
        dhPublicKey: crypto.randomBytes(32),
        dhPrivateKey: crypto.randomBytes(32),
      };
      await storage.saveIdentityKeyPair(ikp);
      const restored = await storage.getIdentityKeyPair();
      expect(restored!.signingPublicKey).toEqual(ikp.signingPublicKey);
      expect(restored!.dhPrivateKey).toEqual(ikp.dhPrivateKey);
    });

    test('registration id roundtrip', async () => {
      await storage.saveLocalRegistrationId(42);
      expect(await storage.getLocalRegistrationId()).toBe(42);
    });

    test('signed prekey crud', async () => {
      await storage.saveSignedPreKey({
        keyId: 1,
        keyPair: { publicKey: crypto.randomBytes(32), privateKey: crypto.randomBytes(32) },
        signature: crypto.randomBytes(64),
        timestamp: Date.now(),
      });
      const spk = await storage.getSignedPreKey(1);
      expect(spk!.keyId).toBe(1);
      expect(spk!.signature.length).toBe(64);

      await storage.removeSignedPreKey(1);
      expect(await storage.getSignedPreKey(1)).toBeNull();
    });

    test('one-time prekey count and crud', async () => {
      await storage.saveOneTimePreKey({
        keyId: 100,
        keyPair: { publicKey: crypto.randomBytes(32), privateKey: crypto.randomBytes(32) },
      });
      const count = await storage.getOneTimePreKeyCount();
      expect(count).toBeGreaterThanOrEqual(1);
      await storage.removeOneTimePreKey(100);
    });

    test('session save/load with skipped keys', async () => {
      const state = {
        remoteIdentityKey: crypto.randomBytes(32),
        rootKey: crypto.randomBytes(32),
        sendChain: { chainKey: crypto.randomBytes(32), counter: 7 },
        receiveChain: null,
        dhSend: { publicKey: crypto.randomBytes(32), privateKey: crypto.randomBytes(32) },
        dhReceive: null,
        previousSendCounter: 0,
        skippedKeys: new Map([['key:1', crypto.randomBytes(32)]]),
      };
      await storage.saveSession('bob', state);
      const restored = await storage.getSession('bob');
      expect(restored!.sendChain.counter).toBe(7);
      expect(restored!.skippedKeys.size).toBe(1);
      await storage.removeSession('bob');
    });

    test('constant-time trust comparison', async () => {
      const key = crypto.randomBytes(32);
      // TOFU
      expect(await storage.isTrustedIdentity('peer1', key)).toBe(true);
      await storage.saveTrustedIdentity('peer1', key);
      expect(await storage.isTrustedIdentity('peer1', key)).toBe(true);
      expect(await storage.isTrustedIdentity('peer1', crypto.randomBytes(32))).toBe(false);
    });

    test('retired identity history', async () => {
      const ikp: IdentityKeyPair = {
        signingPublicKey: crypto.randomBytes(32),
        signingPrivateKey: crypto.randomBytes(32),
        dhPublicKey: crypto.randomBytes(32),
        dhPrivateKey: crypto.randomBytes(32),
      };
      const now = Date.now();
      await storage.addRetiredIdentity({ keyPair: ikp, retiredAt: now });

      const retired = await storage.getRetiredIdentities();
      expect(retired.length).toBeGreaterThanOrEqual(1);
      const found = retired.find((r) => r.retiredAt === now);
      expect(found).toBeDefined();
      expect(found!.keyPair.signingPublicKey).toEqual(ikp.signingPublicKey);

      await storage.pruneRetiredIdentities(now + 1);
      const after = await storage.getRetiredIdentities();
      expect(after.find((r) => r.retiredAt === now)).toBeUndefined();
    });

    test('full E2EE conversation using PostgresStorage', async () => {
      // Clean storage for this test
      await sql`TRUNCATE shade_identity, shade_config, shade_signed_prekeys, shade_one_time_prekeys, shade_sessions, shade_trusted_identities`;

      const aliceStorage = storage;
      const bobSql = postgres(PG_URL!);
      const bobStorage = await PostgresStorage.fromClient(bobSql);

      try {
        const alice = new ShadeSessionManager(crypto, aliceStorage);
        const bob = new ShadeSessionManager(crypto, bobStorage);

        // Bob uses a separate fake storage since we only have one PG schema
        // For this smoke test, we just verify the storage layer itself works
        await alice.initialize();
        expect(await alice.getIdentityFingerprint()).toBeTruthy();
      } finally {
        await bobSql.end();
      }
    });
  });
}
