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 { GET as getExercises, POST as postExercise } from '@/app/api/exercises/route';
import { POST as postWorkout, GET as getWorkouts } from '@/app/api/workouts/route';

// `NextRequest` accepts a slightly stricter RequestInit (no `signal:
// null`), so cast the standard RequestInit to the constructor's
// expected shape.
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]);
}

async function makeUser(opts: { email: string; isAdmin?: boolean }) {
  return prisma.user.create({
    data: {
      email: opts.email,
      passwordHash: 'fake',
      isAdmin: opts.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('GET /api/exercises', () => {
  it('returns 401 when unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await getExercises(jsonReq('http://x/api/exercises'));
    expect(res.status).toBe(401);
  });

  it('returns only exercises owned by the requesting user', async () => {
    const alice = await makeUser({ email: 'a@x' });
    const bob = await makeUser({ email: 'b@x' });
    await prisma.exercise.createMany({
      data: [
        { userId: alice.id, name: 'Alice Squat', type: 'barbell', muscleGroups: '[]' },
        { userId: alice.id, name: 'Alice Bench', type: 'barbell', muscleGroups: '[]' },
        { userId: bob.id, name: 'Bob Curl', type: 'dumbbell', muscleGroups: '[]' },
      ],
    });

    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getExercises(jsonReq('http://x/api/exercises'));
    const body = await res.json();
    const names = body.map((e: { name: string }) => e.name).sort();
    expect(names).toEqual(['Alice Bench', 'Alice Squat']);
  });

  it('filters by query string substring', async () => {
    const alice = await makeUser({ email: 'a@x' });
    await prisma.exercise.createMany({
      data: [
        { userId: alice.id, name: 'Bench Press', type: 'barbell', muscleGroups: '[]' },
        { userId: alice.id, name: 'Cable Bench Fly', type: 'cable', muscleGroups: '[]' },
        { userId: alice.id, name: 'Squat', type: 'barbell', muscleGroups: '[]' },
      ],
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getExercises(
      jsonReq('http://x/api/exercises?q=Bench'),
    );
    const body = await res.json();
    expect(body.map((e: { name: string }) => e.name).sort()).toEqual([
      'Bench Press',
      'Cable Bench Fly',
    ]);
  });
});

describe('POST /api/exercises', () => {
  it('returns 401 when unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await postExercise(
      jsonReq('http://x/api/exercises', {
        name: 'X',
        type: 'barbell',
      }),
    );
    expect(res.status).toBe(401);
  });

  it('creates a new exercise and stamps isCustom=true', async () => {
    const alice = await makeUser({ email: 'a@x' });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postExercise(
      jsonReq('http://x/api/exercises', {
        name: 'My Custom Exercise',
        type: 'machine',
        muscleGroups: ['legs'],
      }),
    );
    expect(res.status).toBe(201);
    const ex = await prisma.exercise.findUnique({
      where: { userId_name: { userId: alice.id, name: 'My Custom Exercise' } },
    });
    expect(ex).toBeTruthy();
    expect(ex?.isCustom).toBe(true);
  });

  it('rejects duplicates with 400', async () => {
    const alice = await makeUser({ email: 'a@x' });
    await prisma.exercise.create({
      data: {
        userId: alice.id,
        name: 'Duplicate',
        type: 'barbell',
        muscleGroups: '[]',
      },
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postExercise(
      jsonReq('http://x/api/exercises', {
        name: 'Duplicate',
        type: 'barbell',
      }),
    );
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toMatch(/already exists/i);
  });

  it('defaults inputFields based on type (cardio gets duration+calories)', async () => {
    const alice = await makeUser({ email: 'a@x' });
    getCurrentUserMock.mockResolvedValue(alice);
    await postExercise(
      jsonReq('http://x/api/exercises', {
        name: 'Treadmill Run',
        type: 'cardio',
      }),
    );
    const ex = await prisma.exercise.findUnique({
      where: { userId_name: { userId: alice.id, name: 'Treadmill Run' } },
    });
    expect(JSON.parse(ex!.inputFields)).toEqual([
      'sets',
      'duration',
      'calories',
    ]);
  });

  it('returns 400 with details on validation failure', async () => {
    const alice = await makeUser({ email: 'a@x' });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postExercise(
      jsonReq('http://x/api/exercises', {
        name: '',
        type: 'barbell',
      }),
    );
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toMatch(/validation/i);
  });
});

describe('GET /api/workouts', () => {
  it('returns 401 when unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await getWorkouts(
      jsonReq('http://x/api/workouts'),
    );
    expect(res.status).toBe(401);
  });

  it('returns only workouts owned by the requesting user', async () => {
    const alice = await makeUser({ email: 'a@x' });
    const bob = await makeUser({ email: 'b@x' });
    await prisma.workout.createMany({
      data: [
        { userId: alice.id, date: new Date(), name: 'A1' },
        { userId: alice.id, date: new Date(), name: 'A2' },
        { userId: bob.id, date: new Date(), name: 'B1' },
      ],
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(
      jsonReq('http://x/api/workouts'),
    );
    const body = await res.json();
    expect(body.data.length).toBe(2);
    expect(body.data.every((w: { name: string }) => w.name?.startsWith('A'))).toBe(
      true,
    );
    expect(body.meta.total).toBe(2);
  });

  it('omits soft-deleted workouts (deletedAt set)', async () => {
    const alice = await makeUser({ email: 'a@x' });
    await prisma.workout.create({
      data: { userId: alice.id, date: new Date(), name: 'visible' },
    });
    await prisma.workout.create({
      data: {
        userId: alice.id,
        date: new Date(),
        name: 'tombstoned',
        deletedAt: new Date(),
      },
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await getWorkouts(
      jsonReq('http://x/api/workouts'),
    );
    const body = await res.json();
    expect(body.data.map((w: { name: string }) => w.name)).toEqual(['visible']);
  });
});

describe('POST /api/workouts', () => {
  it('returns 401 when unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', { name: 'X' }),
    );
    expect(res.status).toBe(401);
  });

  it('creates an empty workout with just a date', async () => {
    const alice = await makeUser({ email: 'a@x' });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', {
        name: 'Today',
        durationMinutes: 45,
      }),
    );
    expect(res.status).toBe(201);
    const workout = await res.json();
    expect(workout.name).toBe('Today');
    expect(workout.durationMinutes).toBe(45);
    expect(workout.userId).toBe(alice.id);
  });

  it('creates a workout with sets attached and persists set fields', async () => {
    const alice = await makeUser({ email: 'a@x' });
    const bench = await prisma.exercise.create({
      data: {
        userId: alice.id,
        name: 'Bench Press',
        type: 'barbell',
        muscleGroups: '[]',
      },
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', {
        name: 'Push Day',
        sets: [
          {
            exerciseId: bench.id,
            setNumber: 1,
            reps: 5,
            weight: 225,
            weightUnit: 'lbs',
            rpe: 7,
          },
          {
            exerciseId: bench.id,
            setNumber: 2,
            reps: 5,
            weight: 245,
            weightUnit: 'lbs',
            rpe: 8,
          },
        ],
      }),
    );
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.setLogs).toHaveLength(2);
    expect(body.setLogs[0].weight).toBe(225);
    expect(body.setLogs[1].rpe).toBe(8);
  });

  it('rejects negative reps via Zod with 400', async () => {
    const alice = await makeUser({ email: 'a@x' });
    const bench = await prisma.exercise.create({
      data: {
        userId: alice.id,
        name: 'Bench Press',
        type: 'barbell',
        muscleGroups: '[]',
      },
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const res = await postWorkout(
      jsonReq('http://x/api/workouts', {
        sets: [
          { exerciseId: bench.id, setNumber: 1, reps: -1, weightUnit: 'lbs' },
        ],
      }),
    );
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toMatch(/invalid/i);
  });
});
