import { describe, test, expect } from 'bun:test';
import { createObserver, StateAggregator } from '../src/index.js';
import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web';
import { ShadeSessionManager, ShadeEventEmitter } from '@shade/core';
import { PrekeyServerEvents } from '@shade/server';

const crypto = new SubtleCryptoProvider();
const TEST_TOKEN = 'test-token-must-be-at-least-16-chars';

describe('StateAggregator', () => {
  test('aggregates client events into snapshot', async () => {
    const events = new ShadeEventEmitter();
    const agg = new StateAggregator(events);

    const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await alice.initialize();
    await alice.generateOneTimePreKeys(10);

    const snap = agg.snapshot();
    expect(snap.identity.fingerprint).toBeTruthy();
    expect(snap.identity.registrationId).toBeGreaterThan(0);
    expect(snap.prekeys.oneTimeRemaining).toBe(10);
    expect(snap.prekeys.lastGenerated).toBeGreaterThan(0);
  });

  test('tracks sessions across encrypt/decrypt', async () => {
    const events = new ShadeEventEmitter();
    const agg = new StateAggregator(events);

    const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await alice.initialize();
    await bob.initialize();

    const otpks = await bob.generateOneTimePreKeys(5);
    const bundle = await bob.createPreKeyBundle();
    bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
    await alice.initSessionFromBundle('bob', bundle);

    const env = await alice.encrypt('bob', 'hello');
    await bob.decrypt('alice', env);

    const snap = agg.snapshot();
    const aliceToBob = snap.sessions.find((s) => s.address === 'bob');
    expect(aliceToBob).toBeDefined();
    expect(aliceToBob!.messageCountSent).toBe(1);
  });

  test('tracks server events', () => {
    const serverEvents = new PrekeyServerEvents();
    const agg = new StateAggregator(undefined, serverEvents);

    serverEvents.emit('server.identity_registered', { address: 'alice', identityKeyHash: 'abc' });
    serverEvents.emit('server.identity_registered', { address: 'bob', identityKeyHash: 'def' });
    serverEvents.emit('server.bundle_fetched', { address: 'alice', hadOneTimePreKey: true });
    serverEvents.emit('server.bundle_fetched', { address: 'alice', hadOneTimePreKey: false });
    serverEvents.emit('server.identity_deleted', { address: 'alice' });

    const snap = agg.snapshot();
    expect(snap.server.registeredIdentities.has('bob')).toBe(true);
    expect(snap.server.registeredIdentities.has('alice')).toBe(false);
    expect(snap.server.totalBundleFetches).toBe(2);
    expect(snap.server.totalDeleted).toBe(1);
  });
});

describe('Observer routes', () => {
  test('refuses requests without token', async () => {
    const events = new ShadeEventEmitter();
    const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });

    const res = await observer.request('/api/state');
    expect(res.status).toBe(401);
  });

  test('accepts requests with valid bearer token', async () => {
    const events = new ShadeEventEmitter();
    const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });

    const mgr = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await mgr.initialize();

    const res = await observer.request('/api/state', {
      headers: { Authorization: `Bearer ${TEST_TOKEN}` },
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.identity.fingerprint).toBeTruthy();
  });

  test('refuses requests with wrong token', async () => {
    const events = new ShadeEventEmitter();
    const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });

    const res = await observer.request('/api/state', {
      headers: { Authorization: 'Bearer wrong-token-also-long-enough' },
    });
    expect(res.status).toBe(401);
  });

  test('accepts token via query string for SSE', async () => {
    const events = new ShadeEventEmitter();
    const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });

    // Just check that the auth middleware accepts the query token
    const res = await observer.request(`/api/state?token=${TEST_TOKEN}`);
    expect(res.status).toBe(200);
  });

  test('refuses startup with too-short token', () => {
    expect(() => createObserver({ token: 'short' })).toThrow();
  });

  test('health endpoint works without auth', async () => {
    const observer = createObserver({ token: TEST_TOKEN });
    const res = await observer.request('/health');
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.status).toBe('ok');
  });

  test('snapshot reflects state after operations', async () => {
    const events = new ShadeEventEmitter();
    const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });

    const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
    await alice.initialize();
    await bob.initialize();
    const otpks = await bob.generateOneTimePreKeys(3);
    const bundle = await bob.createPreKeyBundle();
    bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
    await alice.initSessionFromBundle('bob', bundle);

    const env = await alice.encrypt('bob', 'hi');
    await bob.decrypt('alice', env);

    const res = await observer.request('/api/state', {
      headers: { Authorization: `Bearer ${TEST_TOKEN}` },
    });
    const body = await res.json();
    expect(body.sessions.length).toBeGreaterThan(0);
    // Bob started with 3 OTPKs; Alice consumed one via X3DH PreKeyMessage decrypt
    expect(body.prekeys.oneTimeRemaining).toBe(2);
  });

  test('placeholder dashboard renders when no dist', async () => {
    const observer = createObserver({ token: TEST_TOKEN });
    const res = await observer.request('/dashboard/');
    expect(res.status).toBe(200);
    const html = await res.text();
    expect(html).toContain('Shade Observer');
  });
});
