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 getPrograms,
  POST as createProgram,
} from '@/app/api/programs/route';
import {
  GET as getProgram,
  PATCH as patchProgram,
  DELETE as deleteProgram,
} from '@/app/api/programs/[id]/route';
import { POST as startDay } from '@/app/api/programs/[id]/days/[dayId]/start/route';

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

async function makeUserAndExercises(opts: {
  email: string;
  exerciseNames: string[];
}) {
  const user = await prisma.user.create({
    data: { email: opts.email, passwordHash: 'fake', isAdmin: false },
  });
  const exercises = [];
  for (const name of opts.exerciseNames) {
    exercises.push(
      await prisma.exercise.create({
        data: {
          userId: user.id,
          name,
          type: 'barbell',
          muscleGroups: '[]',
        },
      }),
    );
  }
  return { user, exercises };
}

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

describe('POST /api/programs', () => {
  it('returns 401 unauthenticated', async () => {
    getCurrentUserMock.mockResolvedValue(null);
    const res = await createProgram(
      jsonReq('http://x/api/programs', {
        name: 'X',
        type: 'hypertrophy',
        durationWeeks: 4,
        startDate: new Date().toISOString(),
      }),
    );
    expect(res.status).toBe(401);
  });

  it('creates a program with the full nested tree in one transaction', async () => {
    const { user, exercises } = await makeUserAndExercises({
      email: 'a@x',
      exerciseNames: ['Bench Press', 'Squat'],
    });
    getCurrentUserMock.mockResolvedValue(user);

    const res = await createProgram(
      jsonReq('http://x/api/programs', {
        name: 'Test 4-week',
        type: 'hypertrophy',
        durationWeeks: 4,
        startDate: '2026-05-10',
        weeks: [
          {
            weekNumber: 1,
            phase: 'Volume',
            days: [
              {
                dayOfWeek: 1,
                name: 'Push',
                exercises: [
                  {
                    exerciseId: exercises[0].id,
                    order: 0,
                    sets: 4,
                    repsMin: 6,
                    repsMax: 10,
                  },
                ],
              },
              {
                dayOfWeek: 3,
                name: 'Pull',
                exercises: [],
              },
            ],
          },
          {
            weekNumber: 2,
            days: [
              {
                dayOfWeek: 1,
                name: 'Push',
                exercises: [
                  {
                    exerciseId: exercises[0].id,
                    order: 0,
                    sets: 4,
                    repsMin: 6,
                    repsMax: 10,
                  },
                  {
                    exerciseId: exercises[1].id,
                    order: 1,
                    sets: 5,
                    repsMin: 5,
                    repsMax: 5,
                  },
                ],
              },
            ],
          },
        ],
      }),
    );
    expect(res.status).toBe(201);

    const programs = await prisma.program.findMany({
      where: { userId: user.id },
      include: {
        weeks: {
          include: {
            days: { include: { exercises: true } },
          },
        },
      },
    });
    expect(programs).toHaveLength(1);
    expect(programs[0].weeks).toHaveLength(2);
    const week2 = programs[0].weeks.find((w) => w.weekNumber === 2);
    expect(week2!.days[0].exercises).toHaveLength(2);
  });

  it('rejects exerciseIds that belong to a different user', async () => {
    const { exercises: aliceExs } = await makeUserAndExercises({
      email: 'alice@x',
      exerciseNames: ['Alice Squat'],
    });
    const { user: bob } = await makeUserAndExercises({
      email: 'bob@x',
      exerciseNames: [],
    });
    getCurrentUserMock.mockResolvedValue(bob);

    const res = await createProgram(
      jsonReq('http://x/api/programs', {
        name: 'Bob trying to use Alice exercise',
        type: 'hypertrophy',
        durationWeeks: 1,
        startDate: '2026-05-10',
        weeks: [
          {
            weekNumber: 1,
            days: [
              {
                dayOfWeek: 1,
                exercises: [{ exerciseId: aliceExs[0].id, order: 0, sets: 3 }],
              },
            ],
          },
        ],
      }),
    );
    expect(res.status).toBe(400);
  });
});

describe('GET /api/programs + GET /api/programs/[id]', () => {
  it('lists programs scoped to the actor and returns full tree on detail', async () => {
    const { user, exercises } = await makeUserAndExercises({
      email: 'a@x',
      exerciseNames: ['Bench'],
    });
    getCurrentUserMock.mockResolvedValue(user);
    await createProgram(
      jsonReq('http://x/api/programs', {
        name: 'Plan A',
        type: 'hypertrophy',
        durationWeeks: 1,
        startDate: '2026-05-10',
        weeks: [
          {
            weekNumber: 1,
            days: [
              {
                dayOfWeek: 1,
                exercises: [{ exerciseId: exercises[0].id, order: 0, sets: 3 }],
              },
            ],
          },
        ],
      }),
    );

    const list = await (await getPrograms()).json();
    expect(list).toHaveLength(1);
    const programId = list[0].id;

    const detail = await (
      await getProgram(jsonReq(`http://x/api/programs/${programId}`), {
        params: { id: programId },
      })
    ).json();
    expect(detail.weeks[0].days[0].exercises[0].exercise.name).toBe('Bench');
  });

  it('GET detail returns 404 for another user\'s program', async () => {
    const { user: aliceForOtherTest } = await makeUserAndExercises({
      email: 'alice@x',
      exerciseNames: [],
    });
    const aliceProg = await prisma.program.create({
      data: {
        userId: aliceForOtherTest.id,
        name: 'Alice plan',
        type: 'hypertrophy',
        durationWeeks: 1,
        startDate: new Date(),
      },
    });
    const { user: bob } = await makeUserAndExercises({
      email: 'bob@x',
      exerciseNames: [],
    });
    getCurrentUserMock.mockResolvedValue(bob);
    const res = await getProgram(
      jsonReq(`http://x/api/programs/${aliceProg.id}`),
      { params: { id: aliceProg.id } },
    );
    expect(res.status).toBe(404);
  });
});

describe('PATCH /api/programs/[id] (replace tree)', () => {
  it('replaces the entire weeks tree atomically', async () => {
    const { user, exercises } = await makeUserAndExercises({
      email: 'a@x',
      exerciseNames: ['Bench', 'Squat'],
    });
    getCurrentUserMock.mockResolvedValue(user);
    const created = await (
      await createProgram(
        jsonReq('http://x/api/programs', {
          name: 'Original',
          type: 'hypertrophy',
          durationWeeks: 4,
          startDate: '2026-05-10',
          weeks: [
            {
              weekNumber: 1,
              days: [
                {
                  dayOfWeek: 1,
                  exercises: [
                    { exerciseId: exercises[0].id, order: 0, sets: 3 },
                  ],
                },
              ],
            },
          ],
        }),
      )
    ).json();

    const patchRes = await patchProgram(
      jsonReq(
        `http://x/api/programs/${created.id}`,
        {
          name: 'Updated name',
          weeks: [
            {
              weekNumber: 1,
              days: [
                {
                  dayOfWeek: 2,
                  name: 'Pull',
                  exercises: [
                    { exerciseId: exercises[1].id, order: 0, sets: 5 },
                  ],
                },
              ],
            },
          ],
        },
        'PATCH',
      ),
      { params: { id: created.id } },
    );
    expect(patchRes.status).toBe(200);

    const after = await prisma.program.findUnique({
      where: { id: created.id },
      include: {
        weeks: {
          include: {
            days: { include: { exercises: true } },
          },
        },
      },
    });
    expect(after!.name).toBe('Updated name');
    expect(after!.weeks[0].days).toHaveLength(1);
    expect(after!.weeks[0].days[0].dayOfWeek).toBe(2);
    expect(after!.weeks[0].days[0].exercises[0].exerciseId).toBe(
      exercises[1].id,
    );
  });
});

describe('DELETE /api/programs/[id]', () => {
  it('cascades to weeks/days/exercises and refuses cross-user', async () => {
    const { user, exercises } = await makeUserAndExercises({
      email: 'a@x',
      exerciseNames: ['Bench'],
    });
    getCurrentUserMock.mockResolvedValue(user);
    const created = await (
      await createProgram(
        jsonReq('http://x/api/programs', {
          name: 'Will be deleted',
          type: 'hypertrophy',
          durationWeeks: 1,
          startDate: '2026-05-10',
          weeks: [
            {
              weekNumber: 1,
              days: [
                {
                  dayOfWeek: 1,
                  exercises: [
                    { exerciseId: exercises[0].id, order: 0, sets: 3 },
                  ],
                },
              ],
            },
          ],
        }),
      )
    ).json();

    expect(await prisma.programWeek.count()).toBeGreaterThan(0);
    expect(await prisma.programExercise.count()).toBeGreaterThan(0);

    const res = await deleteProgram(
      jsonReq(`http://x/api/programs/${created.id}`, undefined, 'DELETE'),
      { params: { id: created.id } },
    );
    expect(res.status).toBe(200);
    expect(await prisma.programWeek.count()).toBe(0);
    expect(await prisma.programExercise.count()).toBe(0);
  });
});

describe('POST /api/programs/[id]/days/[dayId]/start', () => {
  it('creates a workout pre-populated with empty SetLogs from the program day', async () => {
    const { user, exercises } = await makeUserAndExercises({
      email: 'a@x',
      exerciseNames: ['Bench', 'Squat'],
    });
    getCurrentUserMock.mockResolvedValue(user);
    const created = await (
      await createProgram(
        jsonReq('http://x/api/programs', {
          name: 'Plan',
          type: 'hypertrophy',
          durationWeeks: 1,
          startDate: '2026-05-10',
          weeks: [
            {
              weekNumber: 1,
              days: [
                {
                  dayOfWeek: 1,
                  name: 'Push Day',
                  exercises: [
                    { exerciseId: exercises[0].id, order: 0, sets: 4 },
                    { exerciseId: exercises[1].id, order: 1, sets: 3 },
                  ],
                },
              ],
            },
          ],
        }),
      )
    ).json();

    const day = await prisma.programDay.findFirst({
      where: { week: { programId: created.id } },
    });

    const startRes = await startDay(
      jsonReq(
        `http://x/api/programs/${created.id}/days/${day!.id}/start`,
        {},
      ),
      { params: { id: created.id, dayId: day!.id } },
    );
    expect(startRes.status).toBe(201);
    const workout = await startRes.json();

    // 4 sets of Bench + 3 sets of Squat = 7 SetLogs
    expect(workout.setLogs).toHaveLength(7);
    expect(workout.programDayId).toBe(day!.id);
    expect(workout.name).toBe('Push Day');
    // SetLogs should be empty (no reps/weight) — user fills them in
    for (const sl of workout.setLogs) {
      expect(sl.reps).toBeNull();
      expect(sl.weight).toBeNull();
    }
  });

  it('refuses if the program day belongs to a different user', async () => {
    const { user: alice, exercises: aliceExs } = await makeUserAndExercises({
      email: 'alice@x',
      exerciseNames: ['Alice Bench'],
    });
    getCurrentUserMock.mockResolvedValue(alice);
    const aliceProg = await (
      await createProgram(
        jsonReq('http://x/api/programs', {
          name: 'Alice Plan',
          type: 'hypertrophy',
          durationWeeks: 1,
          startDate: '2026-05-10',
          weeks: [
            {
              weekNumber: 1,
              days: [
                {
                  dayOfWeek: 1,
                  exercises: [
                    { exerciseId: aliceExs[0].id, order: 0, sets: 3 },
                  ],
                },
              ],
            },
          ],
        }),
      )
    ).json();
    const aliceDay = await prisma.programDay.findFirst({
      where: { week: { programId: aliceProg.id } },
    });

    const { user: bob } = await makeUserAndExercises({
      email: 'bob@x',
      exerciseNames: [],
    });
    getCurrentUserMock.mockResolvedValue(bob);
    const res = await startDay(
      jsonReq(
        `http://x/api/programs/${aliceProg.id}/days/${aliceDay!.id}/start`,
      ),
      { params: { id: aliceProg.id, dayId: aliceDay!.id } },
    );
    expect(res.status).toBe(404);
  });
});
