/**
 * Tests for GET/POST /api/invitations and PATCH/DELETE /api/invitations/[id].
 *
 * Covers:
 * - GET 401 unauthorized
 * - GET 200 lists codes with total
 * - POST 403 for non-admin
 * - POST 400 for invalid grantPlan
 * - POST 201 creates code with defaults
 * - POST 201 creates code with custom code
 * - PATCH 403 for non-admin
 * - PATCH 404 for missing code
 * - PATCH 200 toggles status
 * - DELETE 403 for non-super_admin
 * - DELETE 404 for missing code
 * - DELETE 200 success
 */

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

// ── Mocks ──────────────────────────────────────────────────────────────

const mockGetCurrentUser = vi.fn();
vi.mock('@/lib/auth-server', () => ({
  getCurrentUser: (...args: unknown[]) => mockGetCurrentUser(...args),
  getCurrentUserFromRequest: (...args: unknown[]) => mockGetCurrentUser(...args),
  requireAdmin: (...args: unknown[]) => mockGetCurrentUser(...args),
}));

const mockListInvitations = vi.fn();
const mockCountInvitations = vi.fn();
const mockCreateInvitation = vi.fn();
const mockUpdateInvitation = vi.fn();
const mockDeleteInvitation = vi.fn();
vi.mock('@/lib/growth-client', () => ({
  listInvitations: (...args: unknown[]) => mockListInvitations(...args),
  countInvitations: (...args: unknown[]) => mockCountInvitations(...args),
  createInvitation: (...args: unknown[]) => mockCreateInvitation(...args),
  updateInvitation: (...args: unknown[]) => mockUpdateInvitation(...args),
  deleteInvitation: (...args: unknown[]) => mockDeleteInvitation(...args),
}));

import { GET, POST } from '@/app/api/invitations/route';
import { PATCH, DELETE } from '@/app/api/invitations/[id]/route';

// ── Helpers ────────────────────────────────────────────────────────────

const adminUser = {
  id: 'usr_admin_1',
  email: 'admin@example.com',
  name: 'Admin',
  role: 'super_admin',
  plan: 'enterprise',
};

const viewerUser = {
  id: 'usr_viewer_1',
  email: 'viewer@example.com',
  name: 'Viewer',
  role: 'viewer',
  plan: 'free',
};

async function callGET(token = 'Bearer admin_tok') {
  const { NextRequest } = await import('next/server');
  return GET(
    new NextRequest(
      new Request('http://localhost:3001/api/invitations', {
        headers: { Authorization: token },
      })
    )
  );
}

async function callPOST(body: object, token = 'Bearer admin_tok') {
  const { NextRequest } = await import('next/server');
  return POST(
    new NextRequest(
      new Request('http://localhost:3001/api/invitations', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: token,
        },
        body: JSON.stringify(body),
      })
    )
  );
}

async function callPATCH(id: string, body: object, token = 'Bearer admin_tok') {
  const { NextRequest } = await import('next/server');
  return PATCH(
    new NextRequest(
      new Request(`http://localhost:3001/api/invitations/${id}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          Authorization: token,
        },
        body: JSON.stringify(body),
      })
    ),
    { params: Promise.resolve({ id }) }
  );
}

async function callDELETE(id: string, token = 'Bearer admin_tok') {
  const { NextRequest } = await import('next/server');
  return DELETE(
    new NextRequest(
      new Request(`http://localhost:3001/api/invitations/${id}`, {
        method: 'DELETE',
        headers: { Authorization: token },
      })
    ),
    { params: Promise.resolve({ id }) }
  );
}

// ── Tests ──────────────────────────────────────────────────────────────

describe('GET /api/invitations', () => {
  beforeEach(() => vi.clearAllMocks());

  it('returns 401 when not authenticated', async () => {
    mockGetCurrentUser.mockResolvedValue(null);
    const res = await callGET();
    expect(res.status).toBe(401);
  });

  it('returns codes and total', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockListInvitations.mockResolvedValue({
      invitations: [{ id: 'inv_1', code: 'LYSNR-ABC' }],
      count: 1,
    });
    mockCountInvitations.mockResolvedValue({ count: 1 });

    const res = await callGET();
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.codes).toHaveLength(1);
    expect(data.total).toBe(1);
  });
});

describe('POST /api/invitations', () => {
  beforeEach(() => vi.clearAllMocks());

  it('returns 403 for non-admin', async () => {
    mockGetCurrentUser.mockResolvedValue(viewerUser);
    const res = await callPOST({});
    expect(res.status).toBe(403);
  });

  it('returns 400 for invalid grantPlan', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    const res = await callPOST({ grantPlan: 'invalid' });
    expect(res.status).toBe(400);
  });

  it('creates code with defaults', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockCreateInvitation.mockImplementation((body: Record<string, unknown>) => ({
      ...body,
      id: 'inv_new',
      status: 'active',
      currentUses: 0,
      redeemedBy: [],
    }));

    const res = await callPOST({ description: 'Beta invite' });
    expect(res.status).toBe(201);
    const data = await res.json();
    expect(data.code).toMatch(/^INVITE-/);
  });

  it('creates code with custom code', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockCreateInvitation.mockImplementation((body: Record<string, unknown>) => ({
      ...body,
      id: 'inv_new',
    }));

    const res = await callPOST({ code: 'my-custom-code' });
    expect(res.status).toBe(201);
    const data = await res.json();
    expect(data.code).toBe('MY-CUSTOM-CODE');
  });
});

describe('PATCH /api/invitations/[id]', () => {
  beforeEach(() => vi.clearAllMocks());

  it('returns 403 for non-admin', async () => {
    mockGetCurrentUser.mockResolvedValue(viewerUser);
    const res = await callPATCH('inv_1', { status: 'disabled' });
    expect(res.status).toBe(403);
  });

  it('returns 500 when update fails', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockUpdateInvitation.mockResolvedValue(null);
    const res = await callPATCH('inv_missing', { status: 'disabled' });
    expect(res.status).toBe(500);
  });

  it('toggles status successfully', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockUpdateInvitation.mockResolvedValue({ id: 'inv_1', status: 'disabled' });

    const res = await callPATCH('inv_1', { status: 'disabled' });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.status).toBe('disabled');
  });
});

describe('DELETE /api/invitations/[id]', () => {
  beforeEach(() => vi.clearAllMocks());

  it('returns 403 for non-super_admin', async () => {
    mockGetCurrentUser.mockResolvedValue({ ...adminUser, role: 'admin' });
    const res = await callDELETE('inv_1');
    expect(res.status).toBe(403);
  });

  it('deletes successfully', async () => {
    mockGetCurrentUser.mockResolvedValue(adminUser);
    mockDeleteInvitation.mockResolvedValue(undefined);
    const res = await callDELETE('inv_1');
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.success).toBe(true);
  });
});
