import Fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const repoMock = {
  list: vi.fn(),
  create: vi.fn(),
  getById: vi.fn(),
  update: vi.fn(),
  remove: vi.fn(),
  findActiveByTopic: vi.fn(),
  createDlqEntry: vi.fn(),
  listDlq: vi.fn(),
  getDlqEntry: vi.fn(),
  deleteDlqEntry: vi.fn(),
  purgeDlq: vi.fn(),
};

const busMock = {
  emit: vi.fn(),
  on: vi.fn(),
};

const storeBridgeMock = {
  listEvents: vi.fn(),
};

vi.mock('./repository.js', () => repoMock);
vi.mock('../../lib/event-bus.js', () => ({ bus: busMock }));
vi.mock('../../lib/event-store-bridge.js', () => storeBridgeMock);

async function buildApp(payload?: { sub: string; productId: string; role?: string }) {
  const { eventSubscriptionRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  if (payload) {
    app.addHook('onRequest', async req => {
      req.jwtPayload = payload;
    });
  }
  await app.register(eventSubscriptionRoutes, { prefix: '/api' });
  return app;
}

describe('eventSubscriptionRoutes', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

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

  // ── Subscription CRUD ───────────────────────────────────

  it('GET /event-subscriptions lists subscriptions', async () => {
    repoMock.list.mockResolvedValue([{ id: 'esub_1', topic: 'user.created' }]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/event-subscriptions' });
    expect(res.statusCode).toBe(200);
    expect(repoMock.list).toHaveBeenCalledWith('lysnrai', expect.objectContaining({ limit: 50 }));
  });

  it('POST /event-subscriptions creates a subscription', async () => {
    repoMock.create.mockResolvedValue({ id: 'esub_1', topic: 'user.created', status: 'active' });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/event-subscriptions',
      payload: {
        topic: 'user.created',
        handlerType: 'webhook',
        config: { url: 'https://example.com' },
      },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.create).toHaveBeenCalledWith(
      'lysnrai',
      expect.objectContaining({ topic: 'user.created', handlerType: 'webhook' }),
      'admin_1'
    );
  });

  it('POST /event-subscriptions rejects invalid input', async () => {
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/event-subscriptions',
      payload: {},
    });

    expect(res.statusCode).toBe(400);
  });

  it('GET /event-subscriptions/:id returns 404 for unknown', async () => {
    repoMock.getById.mockResolvedValue(undefined);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/event-subscriptions/esub_xxx' });
    expect(res.statusCode).toBe(404);
  });

  it('PATCH /event-subscriptions/:id updates a subscription', async () => {
    repoMock.update.mockResolvedValue({ id: 'esub_1', status: 'paused' });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'PATCH',
      url: '/api/event-subscriptions/esub_1',
      payload: { status: 'paused' },
    });

    expect(res.statusCode).toBe(200);
    expect(repoMock.update).toHaveBeenCalledWith(
      'esub_1',
      'lysnrai',
      expect.objectContaining({ status: 'paused' })
    );
  });

  it('DELETE /event-subscriptions/:id deletes a subscription', async () => {
    repoMock.remove.mockResolvedValue(true);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'DELETE', url: '/api/event-subscriptions/esub_1' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({ deleted: true });
  });

  it('DELETE /event-subscriptions/:id returns 404 for unknown', async () => {
    repoMock.remove.mockResolvedValue(false);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'DELETE', url: '/api/event-subscriptions/esub_xxx' });
    expect(res.statusCode).toBe(404);
  });

  // ── Auth ────────────────────────────────────────────────

  it('rejects unauthenticated requests', async () => {
    const app = await buildApp();
    const res = await app.inject({ method: 'GET', url: '/api/event-subscriptions' });
    expect(res.statusCode).toBe(403);
  });

  it('rejects non-admin users', async () => {
    const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'viewer' });
    const res = await app.inject({ method: 'GET', url: '/api/event-subscriptions' });
    expect(res.statusCode).toBe(403);
  });

  // ── DLQ ─────────────────────────────────────────────────

  it('GET /event-dlq lists DLQ entries', async () => {
    repoMock.listDlq.mockResolvedValue([{ id: 'dlq_1', topic: 'user.created' }]);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'GET', url: '/api/event-dlq' });
    expect(res.statusCode).toBe(200);
    expect(repoMock.listDlq).toHaveBeenCalled();
  });

  it('POST /event-dlq/:id/retry re-emits and removes entry', async () => {
    repoMock.getDlqEntry.mockResolvedValue({
      id: 'dlq_1',
      topic: 'user.created',
      payload: { userId: 'u1', email: 'a@b.com', plan: 'free', productId: 'lysnrai' },
    });
    repoMock.deleteDlqEntry.mockResolvedValue(true);
    busMock.emit.mockResolvedValue({ results: [] });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'POST', url: '/api/event-dlq/dlq_1/retry' });
    expect(res.statusCode).toBe(200);
    expect(busMock.emit).toHaveBeenCalledWith('user.created', expect.any(Object));
    expect(repoMock.deleteDlqEntry).toHaveBeenCalledWith('dlq_1', 'lysnrai');
  });

  it('POST /event-dlq/:id/retry returns 404 for unknown', async () => {
    repoMock.getDlqEntry.mockResolvedValue(undefined);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'POST', url: '/api/event-dlq/dlq_xxx/retry' });
    expect(res.statusCode).toBe(404);
  });

  it('POST /event-dlq/purge purges all entries', async () => {
    repoMock.purgeDlq.mockResolvedValue(5);
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({ method: 'POST', url: '/api/event-dlq/purge' });
    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({ purged: 5 });
  });

  // ── Event Replay ────────────────────────────────────────

  it('POST /events/replay replays stored events', async () => {
    storeBridgeMock.listEvents.mockResolvedValue([
      { id: 'evt_1', topic: 'user.created', payload: { userId: 'u1', productId: 'lysnrai' } },
      { id: 'evt_2', topic: 'user.created', payload: { userId: 'u2', productId: 'lysnrai' } },
    ]);
    busMock.emit.mockResolvedValue({ results: [] });
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/events/replay',
      payload: { topic: 'user.created', since: '2026-01-01' },
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toEqual({ replayed: 2, topic: 'user.created' });
    expect(busMock.emit).toHaveBeenCalledTimes(2);
  });

  it('POST /events/replay requires topic', async () => {
    const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });

    const res = await app.inject({
      method: 'POST',
      url: '/api/events/replay',
      payload: {},
    });

    expect(res.statusCode).toBe(400);
  });
});
