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

// next/cache writes a no-op tag store outside a request — just stub it.
vi.mock('next/cache', () => ({
  revalidatePath: vi.fn(),
}));

// `getCurrentUser` reads cookies. Replace it with a controllable stub.
// vi.hoisted is required so the mock fn is available when vi.mock's
// factory runs (vitest hoists vi.mock to the top of the file).
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 };
});

import { prisma } from '@/lib/prisma';
import {
  setUserAdmin,
  adminResetPassword,
  deleteUser,
} from '@/app/main/admin/users/actions';

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

beforeEach(async () => {
  // Wipe everything between tests for isolation. SetLog/Workout/Session
  // are FK'd to User with onDelete: Cascade, so deleting users sweeps
  // most of it; we still nuke InstanceSettings for a clean slate.
  await prisma.session.deleteMany();
  await prisma.exercise.deleteMany();
  await prisma.workout.deleteMany();
  await prisma.user.deleteMany();
  await prisma.instanceSettings.deleteMany();
  getCurrentUserMock.mockReset();
});

describe('setUserAdmin', () => {
  it('rejects non-admin actor with Forbidden error', async () => {
    const target = await makeUser({ email: 't@x' });
    const actor = await makeUser({ email: 'a@x', isAdmin: false });
    getCurrentUserMock.mockResolvedValue(actor);

    const res = await setUserAdmin(target.id, true);
    expect(res.error).toBe('Forbidden');
  });

  it('promotes a regular user to admin', async () => {
    const admin = await makeUser({ email: 'a@x', isAdmin: true });
    const target = await makeUser({ email: 't@x', isAdmin: false });
    getCurrentUserMock.mockResolvedValue(admin);

    const res = await setUserAdmin(target.id, true);
    expect(res.success).toBe(true);
    const after = await prisma.user.findUnique({ where: { id: target.id } });
    expect(after?.isAdmin).toBe(true);
  });

  it('refuses to demote the LAST admin', async () => {
    const onlyAdmin = await makeUser({ email: 'a@x', isAdmin: true });
    getCurrentUserMock.mockResolvedValue(onlyAdmin);

    const res = await setUserAdmin(onlyAdmin.id, false);
    expect(res.error).toMatch(/last admin/i);
    const after = await prisma.user.findUnique({
      where: { id: onlyAdmin.id },
    });
    expect(after?.isAdmin).toBe(true);
  });

  it('allows demoting an admin when at least one other admin exists', async () => {
    const a1 = await makeUser({ email: 'a1@x', isAdmin: true });
    const a2 = await makeUser({ email: 'a2@x', isAdmin: true });
    getCurrentUserMock.mockResolvedValue(a1);

    const res = await setUserAdmin(a2.id, false);
    expect(res.success).toBe(true);
    const after = await prisma.user.findUnique({ where: { id: a2.id } });
    expect(after?.isAdmin).toBe(false);
  });
});

describe('deleteUser', () => {
  it('refuses to delete the last admin', async () => {
    const a1 = await makeUser({ email: 'a1@x', isAdmin: true });
    const target = await makeUser({ email: 'a2@x', isAdmin: true });
    // Delete a1 first via separate path to leave only `target` admin.
    await prisma.user.delete({ where: { id: a1.id } });

    const a3 = await makeUser({ email: 'a3@x', isAdmin: true });
    getCurrentUserMock.mockResolvedValue(a3);

    // Now actor a3 + target = 2 admins. Delete target => 1 admin left = ok.
    const res1 = await deleteUser(target.id);
    expect(res1.success).toBe(true);

    // Now only a3 admin remains. Try deleting another (non-admin) for setup.
    const u4 = await makeUser({ email: 'u4@x' });
    expect((await deleteUser(u4.id)).success).toBe(true);

    // a3 trying to delete itself is blocked by the self-delete guard,
    // not the last-admin guard, but verify the message.
    const selfRes = await deleteUser(a3.id);
    expect(selfRes.error).toMatch(/own account/i);
  });

  it('refuses self-deletion via admin UI', async () => {
    const a = await makeUser({ email: 'a@x', isAdmin: true });
    await makeUser({ email: 'b@x', isAdmin: true }); // satisfies last-admin guard
    getCurrentUserMock.mockResolvedValue(a);

    const res = await deleteUser(a.id);
    expect(res.error).toMatch(/own account/i);
  });

  it('cascades deletion to the user\'s exercises and workouts', async () => {
    const admin = await makeUser({ email: 'a@x', isAdmin: true });
    const target = await makeUser({ email: 't@x' });
    await prisma.exercise.create({
      data: {
        userId: target.id,
        name: 'Test exercise',
        muscleGroups: '[]',
        type: 'custom',
      },
    });
    await prisma.workout.create({
      data: { userId: target.id, date: new Date() },
    });
    getCurrentUserMock.mockResolvedValue(admin);

    const res = await deleteUser(target.id);
    expect(res.success).toBe(true);
    expect(
      await prisma.exercise.count({ where: { userId: target.id } }),
    ).toBe(0);
    expect(
      await prisma.workout.count({ where: { userId: target.id } }),
    ).toBe(0);
  });
});

describe('adminResetPassword', () => {
  it('hashes and stores a new password and revokes existing sessions', async () => {
    const admin = await makeUser({ email: 'a@x', isAdmin: true });
    const target = await makeUser({ email: 't@x', passwordHash: 'old-hash' });
    await prisma.session.createMany({
      data: [
        { userId: target.id, token: 'sess-1', expiresAt: new Date(Date.now() + 1e9) },
        { userId: target.id, token: 'sess-2', expiresAt: new Date(Date.now() + 1e9) },
      ],
    });
    getCurrentUserMock.mockResolvedValue(admin);

    const res = await adminResetPassword(target.id, 'newpassword123');
    expect(res.success).toBe(true);
    expect(res.revoked).toBe(2);

    const after = await prisma.user.findUnique({ where: { id: target.id } });
    expect(after?.passwordHash).not.toBe('old-hash');
    expect(after?.passwordHash.startsWith('$2')).toBe(true);

    const remaining = await prisma.session.count({
      where: { userId: target.id },
    });
    expect(remaining).toBe(0);
  });

  it('rejects passwords shorter than 8 chars', async () => {
    const admin = await makeUser({ email: 'a@x', isAdmin: true });
    const target = await makeUser({ email: 't@x' });
    getCurrentUserMock.mockResolvedValue(admin);

    const res = await adminResetPassword(target.id, 'short');
    expect(res.error).toMatch(/8 characters/);
  });
});
