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

const { getCurrentUserMock } = vi.hoisted(() => ({
  getCurrentUserMock: vi.fn(),
}));
vi.mock('@/lib/auth', async (orig) => {
  const actual = (await orig()) as Record<string, unknown>;
  return { ...actual, getCurrentUser: getCurrentUserMock };
});
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));

import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
import { POST as postWorkout, GET as getWorkouts } from '@/app/api/workouts/route';
import { POST as postAuth } from '@/app/api/auth/route';

function jsonReq(url: string, body?: unknown, init?: RequestInit): NextRequest {
  const opts: RequestInit = {
    method: body !== undefined ? 'POST' : 'GET',
    headers: { 'content-type': 'application/json' },
    ...init,
  };
  if (body !== undefined) {
    opts.body = JSON.stringify(body);
  }
  return new NextRequest(url, opts as ConstructorParameters<typeof NextRequest>[1]);
}

/** A request carrying a raw (possibly malformed) string body. */
function rawReq(url: string, rawBody: string, init?: RequestInit): NextRequest {
  return new NextRequest(url, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: rawBody,
    ...init,
  } as ConstructorParameters<typeof NextRequest>[1]);
}

async function makeUser(email: string) {
  return prisma.user.create({
    data: { email, passwordHash: 'fake', isAdmin: false },
  });
}

beforeEach(async () => {
  await prisma.session.deleteMany();
  await prisma.exercise.deleteMany();
  await prisma.workout.deleteMany();
  await prisma.user.deleteMany();
  await prisma.instanceSettings.deleteMany();
  getCurrentUserMock.mockReset();
});

describe('malformed request bodies → 400 (not 500)', () => {
  it('POST /api/workouts returns 400 on a body that is not valid JSON', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(rawReq('http://x/api/workouts', '{ not valid json'));
    expect(res.status).toBe(400);
  });

  it('POST /api/workouts returns 400 on an empty body', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(rawReq('http://x/api/workouts', ''));
    expect(res.status).toBe(400);
  });
});

describe('POST /api/workouts date validation', () => {
  it('returns 400 on an unparseable date string', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', { name: 'X', date: 'not-a-date' }),
    );
    expect(res.status).toBe(400);
  });

  it('accepts a valid date-only string', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', { name: 'X', date: '2026-06-01' }),
    );
    expect(res.status).toBe(201);
  });
});

describe('GET /api/workouts pagination validation', () => {
  it('returns 400 on a negative offset (was a Prisma 500)', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(jsonReq('http://x/api/workouts?offset=-5'));
    expect(res.status).toBe(400);
  });

  it('returns 400 on a non-numeric limit', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(jsonReq('http://x/api/workouts?limit=abc'));
    expect(res.status).toBe(400);
  });

  it('returns 400 on limit=0 (new min-1 contract)', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(jsonReq('http://x/api/workouts?limit=0'));
    expect(res.status).toBe(400);
  });

  it('still serves valid pagination parameters', async () => {
    const alice = await makeUser('a@x');
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(jsonReq('http://x/api/workouts?limit=10&offset=0'));
    expect(res.status).toBe(200);
  });
});

describe('POST /api/auth rate limiting', () => {
  it('returns 429 + Retry-After after 10 attempts from one IP', async () => {
    // Unique key per run so the process-global limiter bucket is clean.
    const ip = `validation-test-${Math.random()}`;
    const attempt = () =>
      postAuth(
        new NextRequest('http://x/api/auth', {
          method: 'POST',
          headers: { 'content-type': 'application/json', 'x-forwarded-for': ip },
          body: JSON.stringify({ email: 'nobody@example.com', password: 'x' }),
        } as ConstructorParameters<typeof NextRequest>[1]),
      );

    for (let i = 0; i < 10; i++) {
      const res = await attempt();
      expect(res.status).toBe(401); // no such user
    }
    const blocked = await attempt();
    expect(blocked.status).toBe(429);
    expect(blocked.headers.get('retry-after')).toBeTruthy();
  });
});
