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 getExerciseDetail } from '@/app/api/exercises/[id]/route';

function req(url: string): NextRequest {
  return new NextRequest(url, {
    method: 'GET',
  } as ConstructorParameters<typeof NextRequest>[1]);
}

async function makeUserWithExerciseAndWorkouts(opts: {
  email: string;
  exerciseName: string;
  workoutCount: number;
}) {
  const user = await prisma.user.create({
    data: { email: opts.email, passwordHash: 'fake', isAdmin: false },
  });
  const exercise = await prisma.exercise.create({
    data: {
      userId: user.id,
      name: opts.exerciseName,
      type: 'barbell',
      muscleGroups: '[]',
    },
  });
  // Create N workouts spaced 1 day apart, descending. Each contains
  // 2 sets of the exercise + 1 set of an unrelated exercise (to
  // prove the route filters set logs to the queried exerciseId).
  const otherExercise = await prisma.exercise.create({
    data: {
      userId: user.id,
      name: `Other for ${opts.exerciseName}`,
      type: 'cable',
      muscleGroups: '[]',
    },
  });
  for (let i = 0; i < opts.workoutCount; i++) {
    const date = new Date(2026, 0, 1 + i); // ascending dates → newer is bigger
    await prisma.workout.create({
      data: {
        userId: user.id,
        date,
        setLogs: {
          create: [
            { exerciseId: exercise.id, setNumber: 1, reps: 5, weight: 100 + i },
            { exerciseId: exercise.id, setNumber: 2, reps: 5, weight: 100 + i },
            { exerciseId: otherExercise.id, setNumber: 1, reps: 10 },
          ],
        },
      },
    });
  }
  return { user, exercise, otherExercise };
}

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/[id] (paginated history)', () => {
  it('returns 401 unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await getExerciseDetail(req('http://x/api/exercises/anything'), {
      params: { id: 'anything' },
    });
    expect(res.status).toBe(401);
  });

  it('returns 404 when exercise belongs to another user', async () => {
    const me = await prisma.user.create({
      data: { email: 'me@x', passwordHash: 'fake', isAdmin: false },
    });
    const { exercise } = await makeUserWithExerciseAndWorkouts({
      email: 'them@x',
      exerciseName: 'TheirEx',
      workoutCount: 1,
    });
    getCurrentUserMock.mockResolvedValue(me);
    const res = await getExerciseDetail(req('http://x/api/exercises/' + exercise.id), {
      params: { id: exercise.id },
    });
    expect(res.status).toBe(404);
  });

  it('first page returns the latest N workouts ordered date-desc with hasMore=true when more exist', async () => {
    const { user, exercise } = await makeUserWithExerciseAndWorkouts({
      email: 'a@x',
      exerciseName: 'Bench',
      workoutCount: 30,
    });
    getCurrentUserMock.mockResolvedValue(user);
    const res = await getExerciseDetail(
      req(`http://x/api/exercises/${exercise.id}?offset=0&limit=10`),
      { params: { id: exercise.id } },
    );
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.history).toHaveLength(10);
    expect(body.hasMore).toBe(true);

    // Date-descending — the newest workout is first.
    const dates = body.history.map((h: { workout: { date: string } }) =>
      new Date(h.workout.date).getTime(),
    );
    for (let i = 1; i < dates.length; i++) {
      expect(dates[i - 1]).toBeGreaterThanOrEqual(dates[i]);
    }
  });

  it('second page returns the next slice and hasMore flips to false at the end', async () => {
    const { user, exercise } = await makeUserWithExerciseAndWorkouts({
      email: 'a@x',
      exerciseName: 'Bench',
      workoutCount: 18,
    });
    getCurrentUserMock.mockResolvedValue(user);

    const r1 = await (
      await getExerciseDetail(
        req(`http://x/api/exercises/${exercise.id}?offset=0&limit=10`),
        { params: { id: exercise.id } },
      )
    ).json();
    expect(r1.history).toHaveLength(10);
    expect(r1.hasMore).toBe(true);

    const r2 = await (
      await getExerciseDetail(
        req(`http://x/api/exercises/${exercise.id}?offset=10&limit=10`),
        { params: { id: exercise.id } },
      )
    ).json();
    expect(r2.history).toHaveLength(8); // 18 - 10
    expect(r2.hasMore).toBe(false);

    // No overlap between pages.
    const ids1 = new Set(
      r1.history.map((h: { workout: { id: string } }) => h.workout.id),
    );
    for (const h of r2.history) {
      expect(ids1.has(h.workout.id)).toBe(false);
    }
  });

  it('only returns set logs for the queried exerciseId (not unrelated sets in the same workout)', async () => {
    const { user, exercise } = await makeUserWithExerciseAndWorkouts({
      email: 'a@x',
      exerciseName: 'Bench',
      workoutCount: 1,
    });
    getCurrentUserMock.mockResolvedValue(user);
    const res = await getExerciseDetail(
      req(`http://x/api/exercises/${exercise.id}?offset=0&limit=25`),
      { params: { id: exercise.id } },
    );
    const body = await res.json();
    expect(body.history).toHaveLength(1);
    // Each workout in the seed had 2 sets of `exercise` + 1 set of
    // `otherExercise`. The response should only show the 2.
    expect(body.history[0].sets).toHaveLength(2);
    expect(
      body.history[0].sets.every(
        (s: { exerciseId: string }) => s.exerciseId === exercise.id,
      ),
    ).toBe(true);
  });

  it('omits soft-deleted workouts from the history', async () => {
    const { user, exercise } = await makeUserWithExerciseAndWorkouts({
      email: 'a@x',
      exerciseName: 'Bench',
      workoutCount: 5,
    });
    // Soft-delete the most recent workout
    const newest = await prisma.workout.findFirst({
      where: { userId: user.id },
      orderBy: { date: 'desc' },
    });
    await prisma.workout.update({
      where: { id: newest!.id },
      data: { deletedAt: new Date() },
    });
    getCurrentUserMock.mockResolvedValue(user);
    const res = await getExerciseDetail(
      req(`http://x/api/exercises/${exercise.id}`),
      { params: { id: exercise.id } },
    );
    const body = await res.json();
    expect(body.history).toHaveLength(4);
    expect(body.history.every((h: { workout: { id: string } }) => h.workout.id !== newest!.id)).toBe(
      true,
    );
  });
});
