/**
 * Route-level tests for invitations module — Fastify inject.
 */

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

const repoMock = {
  list: vi.fn(),
  count: vi.fn(),
  getById: vi.fn(),
  create: vi.fn(),
  update: vi.fn(),
  remove: vi.fn(),
  redeem: vi.fn(),
};

const webhookMock = {
  dispatchInvitationRedeemed: vi.fn(),
};

vi.mock('./repository.js', () => repoMock);
vi.mock('../../lib/webhooks.js', () => webhookMock);
vi.mock('../../lib/request-context.js', () => ({
  getRequestProductId: () => 'lysnrai',
}));

const invitation = {
  id: 'inv_1',
  productId: 'lysnrai',
  code: 'WELCOME-1',
  description: 'Welcome offer',
  createdBy: 'admin',
  grantPlan: 'pro',
  grantTrialDays: 14,
  bonusTokens: 100,
  maxUses: 5,
  currentUses: 1,
  redeemedBy: ['user_1'],
  status: 'active',
  expiresAt: null,
  createdAt: '2026-02-16T00:00:00Z',
  updatedAt: '2026-02-16T00:00:00Z',
};

async function buildApp() {
  const { invitationRoutes } = await import('./routes.js');
  const app = Fastify({ logger: false });
  await app.register(invitationRoutes, { prefix: '/api' });
  return app;
}

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

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

  it('GET /invitations lists codes', async () => {
    repoMock.list.mockResolvedValue([invitation]);
    const app = await buildApp();

    const res = await app.inject({ method: 'GET', url: '/api/invitations?limit=10&offset=0' });

    expect(res.statusCode).toBe(200);
    const data = JSON.parse(res.body);
    expect(data.invitations).toHaveLength(1);
    expect(data.count).toBe(1);
  });

  it('GET /invitations/count returns total', async () => {
    repoMock.count.mockResolvedValue(3);
    const app = await buildApp();

    const res = await app.inject({ method: 'GET', url: '/api/invitations/count' });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body).count).toBe(3);
  });

  it('GET /invitations/:id returns 404 when not found', async () => {
    repoMock.getById.mockResolvedValue(null);
    const app = await buildApp();

    const res = await app.inject({ method: 'GET', url: '/api/invitations/missing' });

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

  it('POST /invitations creates code', async () => {
    repoMock.create.mockResolvedValue(invitation);
    const app = await buildApp();

    const res = await app.inject({
      method: 'POST',
      url: '/api/invitations',
      payload: {
        code: 'welcome-1',
        createdBy: 'admin',
        grantPlan: 'pro',
      },
    });

    expect(res.statusCode).toBe(201);
    expect(repoMock.create).toHaveBeenCalled();
  });

  it('PUT /invitations/:id updates code', async () => {
    repoMock.update.mockResolvedValue({ ...invitation, status: 'disabled' });
    const app = await buildApp();

    const res = await app.inject({
      method: 'PUT',
      url: '/api/invitations/inv_1',
      payload: { status: 'disabled' },
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body).status).toBe('disabled');
  });

  it('DELETE /invitations/:id returns 204', async () => {
    repoMock.remove.mockResolvedValue(true);
    const app = await buildApp();

    const res = await app.inject({ method: 'DELETE', url: '/api/invitations/inv_1' });

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

  it('POST /invitations/bulk returns 207 with mixed success', async () => {
    repoMock.create.mockResolvedValueOnce(invitation).mockRejectedValueOnce(new Error('duplicate'));
    const app = await buildApp();

    const res = await app.inject({
      method: 'POST',
      url: '/api/invitations/bulk',
      payload: [
        { code: 'first', createdBy: 'admin', grantPlan: 'pro' },
        { code: 'second', createdBy: 'admin', grantPlan: 'pro' },
      ],
    });

    expect(res.statusCode).toBe(207);
    const data = JSON.parse(res.body);
    expect(data.created).toBe(1);
    expect(data.failed).toBe(1);
  });

  it('POST /invitations/redeem redeems and dispatches webhook', async () => {
    repoMock.redeem.mockResolvedValue(invitation);
    const app = await buildApp();

    const res = await app.inject({
      method: 'POST',
      url: '/api/invitations/redeem',
      payload: { code: 'WELCOME-1', userId: 'user_2' },
    });

    expect(res.statusCode).toBe(200);
    expect(webhookMock.dispatchInvitationRedeemed).toHaveBeenCalled();
  });

  it('POST /invitations/redeem returns 400 when invalid', async () => {
    repoMock.redeem.mockResolvedValue(null);
    const app = await buildApp();

    const res = await app.inject({
      method: 'POST',
      url: '/api/invitations/redeem',
      payload: { code: 'BAD', userId: 'user_2' },
    });

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