import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AuditCollector } from '../src/audit/collector.js';
import type { AuditEvent } from '../src/audit/types.js';

function mockMcpdClient() {
  return {
    post: vi.fn(async () => ({})),
    get: vi.fn(async () => ({})),
    put: vi.fn(async () => ({})),
    delete: vi.fn(async () => {}),
    forward: vi.fn(async () => ({ status: 200, body: {} })),
    withHeaders: vi.fn(() => mockMcpdClient()),
  };
}

function makeEvent(overrides: Partial<Omit<AuditEvent, 'projectName'>> = {}): Omit<AuditEvent, 'projectName'> {
  return {
    timestamp: new Date().toISOString(),
    sessionId: 'test-session',
    eventKind: 'stage_execution',
    source: 'mcplocal',
    verified: true,
    payload: { stage: 'passthrough', durationMs: 10 },
    ...overrides,
  };
}

describe('AuditCollector', () => {
  let client: ReturnType<typeof mockMcpdClient>;

  beforeEach(() => {
    vi.useFakeTimers();
    client = mockMcpdClient();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('queues events and flushes as batch on timer', async () => {
    const collector = new AuditCollector(client as never, 'myproject');

    collector.emit(makeEvent());
    collector.emit(makeEvent());
    collector.emit(makeEvent());

    expect(client.post).not.toHaveBeenCalled();

    // Advance past flush interval
    await vi.advanceTimersByTimeAsync(6_000);

    expect(client.post).toHaveBeenCalledTimes(1);
    const [path, batch] = client.post.mock.calls[0]!;
    expect(path).toBe('/api/v1/audit/events');
    expect(batch).toHaveLength(3);

    await collector.dispose();
  });

  it('flushes immediately when queue reaches batch size limit', async () => {
    const collector = new AuditCollector(client as never, 'myproject');

    // Emit BATCH_SIZE (50) events
    for (let i = 0; i < 50; i++) {
      collector.emit(makeEvent());
    }

    // Allow the flush microtask to complete
    await vi.advanceTimersByTimeAsync(0);

    expect(client.post).toHaveBeenCalledTimes(1);
    const [, batch] = client.post.mock.calls[0]!;
    expect(batch).toHaveLength(50);

    await collector.dispose();
  });

  it('auto-fills projectName on each event', async () => {
    const collector = new AuditCollector(client as never, 'ha-project');

    collector.emit(makeEvent());
    await collector.flush();

    const [, batch] = client.post.mock.calls[0]!;
    expect((batch as AuditEvent[])[0]!.projectName).toBe('ha-project');

    await collector.dispose();
  });

  it('handles mcpd POST failure gracefully (no throw)', async () => {
    client.post.mockRejectedValue(new Error('Network error'));
    const collector = new AuditCollector(client as never, 'myproject');

    collector.emit(makeEvent());
    // Should not throw
    await collector.flush();

    await collector.dispose();
  });

  it('does not flush when queue is empty', async () => {
    const collector = new AuditCollector(client as never, 'myproject');

    await vi.advanceTimersByTimeAsync(6_000);

    expect(client.post).not.toHaveBeenCalled();

    await collector.dispose();
  });

  it('dispose() flushes remaining events and clears timer', async () => {
    const collector = new AuditCollector(client as never, 'myproject');

    collector.emit(makeEvent());
    collector.emit(makeEvent());

    await collector.dispose();

    expect(client.post).toHaveBeenCalledTimes(1);
    const [, batch] = client.post.mock.calls[0]!;
    expect(batch).toHaveLength(2);

    // No further flushes after dispose
    collector.emit(makeEvent());
    await vi.advanceTimersByTimeAsync(10_000);
    // Timer is cleared, so the event stays in queue (no second post)
    expect(client.post).toHaveBeenCalledTimes(1);
  });
});
