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

// Mock next/headers chain (cookies + headers used by the actions).
const { cookieStoreMock, headersStoreMock } = vi.hoisted(() => {
  const cookies = new Map<string, string>();
  return {
    cookieStoreMock: {
      get: vi.fn((name: string) =>
        cookies.has(name) ? { name, value: cookies.get(name)! } : undefined,
      ),
      set: vi.fn((name: string, value: string) => {
        cookies.set(name, value);
      }),
      delete: vi.fn((name: string) => {
        cookies.delete(name);
      }),
      _underlying: cookies,
    },
    headersStoreMock: new Headers({ 'x-forwarded-for': '127.0.0.1' }),
  };
});
vi.mock('next/headers', () => ({
  cookies: vi.fn(async () => cookieStoreMock),
  headers: vi.fn(async () => headersStoreMock),
}));
vi.mock('next/navigation', () => ({
  redirect: vi.fn((dest: string) => {
    throw new Error(`__redirect:${dest}`);
  }),
}));
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));

import { prisma } from '@/lib/prisma';
import { hashPassword } from '@/lib/auth';
import { signupAction } from '@/app/auth/signup/actions';
import { changePasswordAction } from '@/app/main/settings/changePasswordAction';
import { deleteMyAccountAction } from '@/app/main/settings/deleteAccountAction';

beforeEach(async () => {
  await prisma.session.deleteMany();
  await prisma.exercise.deleteMany();
  await prisma.workout.deleteMany();
  await prisma.user.deleteMany();
  await prisma.instanceSettings.deleteMany();
  cookieStoreMock._underlying.clear();
  cookieStoreMock.get.mockClear();
  cookieStoreMock.set.mockClear();
  cookieStoreMock.delete.mockClear();
  // Each test file gets its own per-IP rate-limit bucket key by virtue
  // of fresh module load via vitest forks. Within a single test file we
  // need to space out signup attempts across cases — easy because each
  // test uses a different email prefix and the rate limiter is per-IP,
  // not per-email. Bumping the IP per test sidesteps the cap entirely.
  headersStoreMock.set(
    'x-forwarded-for',
    `10.0.0.${Math.floor(Math.random() * 255)}`,
  );
});

describe('signupAction', () => {
  it('refuses when sign-ups are closed (default)', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: false },
    });
    const res = await signupAction(
      'new@example.com',
      'password123',
      'password123',
    );
    expect(res.error).toMatch(/not enabled/i);
    expect(await prisma.user.count()).toBe(0);
  });

  it('creates a new user, seeds preferences, and sets a session cookie when open', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: true },
    });
    const res = await signupAction(
      'fresh@example.com',
      'password123',
      'password123',
      'Fresh User',
    );
    expect(res.success).toBe(true);
    const user = await prisma.user.findUnique({
      where: { email: 'fresh@example.com' },
      include: { userPreferences: true },
    });
    expect(user).toBeTruthy();
    expect(user?.isAdmin).toBe(false);
    expect(user?.name).toBe('Fresh User');
    expect(user?.userPreferences).toBeTruthy();
    expect(cookieStoreMock.set).toHaveBeenCalledWith(
      'sessionToken',
      expect.any(String),
      expect.any(Object),
    );
  });

  it('rejects mismatched password confirmation', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: true },
    });
    const res = await signupAction('a@b.co', 'password123', 'different123');
    expect(res.error).toMatch(/do not match/i);
    expect(await prisma.user.count()).toBe(0);
  });

  it('rejects passwords shorter than 8 chars', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: true },
    });
    const res = await signupAction('a@b.co', 'short', 'short');
    expect(res.error).toMatch(/8 characters/i);
  });

  it('rejects malformed emails', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: true },
    });
    const res = await signupAction(
      'not-an-email',
      'password123',
      'password123',
    );
    expect(res.error).toMatch(/valid email/i);
  });

  it('does not leak existing-email signal in error message', async () => {
    await prisma.instanceSettings.create({
      data: { id: 1, signupsOpen: true },
    });
    await prisma.user.create({
      data: {
        email: 'taken@example.com',
        passwordHash: await hashPassword('whatever123'),
      },
    });
    const res = await signupAction(
      'taken@example.com',
      'password123',
      'password123',
    );
    expect(res.error).toBeDefined();
    // Generic message — no "already exists" / "in use" wording.
    expect(res.error).not.toMatch(/exist|use|taken|already/i);
  });
});

describe('changePasswordAction', () => {
  let userId: string;
  let token: string;

  beforeEach(async () => {
    const u = await prisma.user.create({
      data: {
        email: 'me@example.com',
        passwordHash: await hashPassword('original123'),
      },
    });
    userId = u.id;
    token = 'session-token-current';
    await prisma.session.create({
      data: { userId, token, expiresAt: new Date(Date.now() + 1e9) },
    });
    cookieStoreMock._underlying.set('sessionToken', token);
  });

  it('rotates the password and revokes other sessions', async () => {
    await prisma.session.createMany({
      data: [
        { userId, token: 'other-1', expiresAt: new Date(Date.now() + 1e9) },
        { userId, token: 'other-2', expiresAt: new Date(Date.now() + 1e9) },
      ],
    });

    const res = await changePasswordAction(
      'original123',
      'newpassword123',
      'newpassword123',
    );
    expect(res.success).toBe(true);
    expect(res.revoked).toBe(2);

    const remaining = await prisma.session.findMany({
      where: { userId },
      select: { token: true },
    });
    expect(remaining).toEqual([{ token }]);
  });

  it('rejects an incorrect current password', async () => {
    const res = await changePasswordAction(
      'wrongpassword',
      'newpassword123',
      'newpassword123',
    );
    expect(res.error).toMatch(/current password/i);
  });

  it('rejects when new equals current', async () => {
    const res = await changePasswordAction(
      'original123',
      'original123',
      'original123',
    );
    expect(res.error).toMatch(/differ/i);
  });
});

describe('deleteMyAccountAction', () => {
  it('refuses without the typed confirmation phrase', async () => {
    const u = await prisma.user.create({
      data: {
        email: 'del@example.com',
        passwordHash: await hashPassword('mypassword123'),
      },
    });
    await prisma.session.create({
      data: {
        userId: u.id,
        token: 'tok',
        expiresAt: new Date(Date.now() + 1e9),
      },
    });
    cookieStoreMock._underlying.set('sessionToken', 'tok');

    const res = await deleteMyAccountAction('mypassword123', 'i agree');
    expect(res?.error).toMatch(/exact phrase/i);
    expect(await prisma.user.findUnique({ where: { id: u.id } })).toBeTruthy();
  });

  it('refuses with wrong password even when phrase is correct', async () => {
    const u = await prisma.user.create({
      data: {
        email: 'del2@example.com',
        passwordHash: await hashPassword('mypassword123'),
      },
    });
    await prisma.session.create({
      data: {
        userId: u.id,
        token: 'tok',
        expiresAt: new Date(Date.now() + 1e9),
      },
    });
    cookieStoreMock._underlying.set('sessionToken', 'tok');

    const res = await deleteMyAccountAction('wrong-pwd', 'delete my account');
    expect(res?.error).toMatch(/incorrect/i);
    expect(await prisma.user.findUnique({ where: { id: u.id } })).toBeTruthy();
  });

  it('refuses when user is the last admin', async () => {
    const u = await prisma.user.create({
      data: {
        email: 'lonely-admin@example.com',
        passwordHash: await hashPassword('mypassword123'),
        isAdmin: true,
      },
    });
    await prisma.session.create({
      data: {
        userId: u.id,
        token: 'tok',
        expiresAt: new Date(Date.now() + 1e9),
      },
    });
    cookieStoreMock._underlying.set('sessionToken', 'tok');

    const res = await deleteMyAccountAction(
      'mypassword123',
      'delete my account',
    );
    expect(res?.error).toMatch(/last admin/i);
    expect(await prisma.user.findUnique({ where: { id: u.id } })).toBeTruthy();
  });

  it('deletes account + cascades + clears cookie when correct', async () => {
    const u = await prisma.user.create({
      data: {
        email: 'goodbye@example.com',
        passwordHash: await hashPassword('mypassword123'),
      },
    });
    await prisma.session.create({
      data: {
        userId: u.id,
        token: 'tok',
        expiresAt: new Date(Date.now() + 1e9),
      },
    });
    await prisma.workout.create({
      data: { userId: u.id, date: new Date() },
    });
    cookieStoreMock._underlying.set('sessionToken', 'tok');

    // The action throws via `redirect()` on success — our mock redirect
    // throws a recognizable string. Catch it.
    let redirected = false;
    try {
      await deleteMyAccountAction('mypassword123', 'delete my account');
    } catch (e) {
      if ((e as Error).message.startsWith('__redirect:')) {
        redirected = true;
      } else {
        throw e;
      }
    }
    expect(redirected).toBe(true);
    expect(await prisma.user.findUnique({ where: { id: u.id } })).toBeNull();
    expect(await prisma.workout.count()).toBe(0);
    expect(cookieStoreMock.delete).toHaveBeenCalledWith('sessionToken');
  });
});
