import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest';
import { POST } from '@/app/api/auth/register/route';
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';

// Mock the database
vi.mock('@/lib/db', () => ({
  db: {
    user: {
      findUnique: vi.fn(),
      create: vi.fn(),
    },
    verificationToken: {
      create: vi.fn(),
    },
  },
}));

// Mock bcrypt
vi.mock('bcryptjs', () => ({
  default: {
    hash: vi.fn(() => Promise.resolve('hashed_password')),
  },
}));

// Mock rate limiter
vi.mock('@/lib/rate-limit', () => ({
  checkRateLimit: vi.fn(() => ({
    allowed: true,
    remaining: 10,
    resetAt: Date.now() + 900000,
  })),
  getClientIp: vi.fn(() => '127.0.0.1'),
  AUTH_RATE_LIMITS: {
    register: { max: 5, windowSeconds: 900 },
  },
}));

// Mock crypto
vi.mock('crypto', () => ({
  default: {
    randomBytes: vi.fn(() => ({
      toString: () => 'mock_token_12345',
    })),
  },
}));

// Mock email sending
vi.mock('@/lib/email/send', () => ({
  sendVerificationEmail: vi.fn(() => Promise.resolve({ success: true, messageId: 'mock_id' })),
}));

describe('/api/auth/register', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env = { ...originalEnv };
    delete process.env.ALLOWED_SIGNUP_EMAILS;
  });

  afterAll(() => {
    process.env = originalEnv;
  });

  it('should create a new user with valid data', async () => {
    // Arrange
    const mockUser = {
      id: '1',
      email: 'test@example.com',
      name: 'Test User',
      plan: 'free',
      createdAt: new Date(),
    };

    vi.mocked(db.user.findUnique).mockResolvedValue(null); // User doesn't exist
    vi.mocked(db.user.create).mockResolvedValue({ ...mockUser, passwordHash: 'hashed' } as any);
    vi.mocked(db.verificationToken.create).mockResolvedValue({} as any);

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'password123',
        name: 'Test User',
      }),
    });

    // Act
    const response = await POST(request);
    const data = await response.json();

    // Assert
    expect(response.status).toBe(201);
    expect(data.message).toContain('Account created successfully');
    expect(data.requiresVerification).toBe(true);
    expect(data.user.email).toBe('test@example.com');
    expect(db.user.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        email: 'test@example.com',
        name: 'Test User',
        passwordHash: 'hashed_password',
      }),
      select: expect.any(Object),
    });
    expect(db.verificationToken.create).toHaveBeenCalled();
  });

  it('should reject registration with missing fields', async () => {
    // Arrange
    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'test@example.com',
        // Missing password and name
      }),
    });

    // Act
    const response = await POST(request);
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('required');
    expect(db.user.create).not.toHaveBeenCalled();
  });

  it('should reject registration with invalid email format', async () => {
    // Arrange
    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'invalid-email',
        password: 'password123',
        name: 'Test User',
      }),
    });

    // Act
    const response = await POST(request);
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('email');
    expect(db.user.create).not.toHaveBeenCalled();
  });

  it('should reject registration with password < 8 characters', async () => {
    // Arrange
    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'short',
        name: 'Test User',
      }),
    });

    // Act
    const response = await POST(request);
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('8 characters');
    expect(db.user.create).not.toHaveBeenCalled();
  });

  it('should reject registration if user already exists', async () => {
    // Arrange
    vi.mocked(db.user.findUnique).mockResolvedValue({
      id: '1',
      email: 'existing@example.com',
    } as any);

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'existing@example.com',
        password: 'password123',
        name: 'Test User',
      }),
    });

    // Act
    const response = await POST(request);
    const data = await response.json();

    // Assert
    expect(response.status).toBe(400);
    expect(data.error).toContain('Unable to create account');
    expect(db.user.create).not.toHaveBeenCalled();
  });

  it('should block non-allowlisted email when ALLOWED_SIGNUP_EMAILS is set', async () => {
    process.env.ALLOWED_SIGNUP_EMAILS = 'allowed@example.com,another@example.com';

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'blocked@example.com',
        password: 'password123',
        name: 'Blocked User',
      }),
    });

    const response = await POST(request);
    const data = await response.json();

    expect(response.status).toBe(403);
    expect(data.error).toContain('closed');
    expect(db.user.create).not.toHaveBeenCalled();
  });

  it('should allow allowlisted email when ALLOWED_SIGNUP_EMAILS is set', async () => {
    process.env.ALLOWED_SIGNUP_EMAILS = 'allowed@example.com';

    vi.mocked(db.user.findUnique).mockResolvedValue(null);
    vi.mocked(db.user.create).mockResolvedValue({
      id: '1', email: 'allowed@example.com', name: 'Allowed', plan: 'free', createdAt: new Date(), passwordHash: 'hashed',
    } as any);
    vi.mocked(db.verificationToken.create).mockResolvedValue({} as any);

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'allowed@example.com',
        password: 'password123',
        name: 'Allowed User',
      }),
    });

    const response = await POST(request);
    expect(response.status).toBe(201);
  });

  it('should allow all signups when ALLOWED_SIGNUP_EMAILS is not set', async () => {
    // env var not set (cleared in beforeEach)
    vi.mocked(db.user.findUnique).mockResolvedValue(null);
    vi.mocked(db.user.create).mockResolvedValue({
      id: '1', email: 'anyone@example.com', name: 'Anyone', plan: 'free', createdAt: new Date(), passwordHash: 'hashed',
    } as any);
    vi.mocked(db.verificationToken.create).mockResolvedValue({} as any);

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'anyone@example.com',
        password: 'password123',
        name: 'Anyone',
      }),
    });

    const response = await POST(request);
    expect(response.status).toBe(201);
  });

  it('should create a verification token with correct expiry', async () => {
    // Arrange
    const mockUser = {
      id: '1',
      email: 'test@example.com',
      name: 'Test User',
      plan: 'free',
      createdAt: new Date(),
    };

    vi.mocked(db.user.findUnique).mockResolvedValue(null);
    vi.mocked(db.user.create).mockResolvedValue({ ...mockUser, passwordHash: 'hashed' } as any);
    vi.mocked(db.verificationToken.create).mockResolvedValue({} as any);

    const request = new NextRequest('http://localhost:3000/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'password123',
        name: 'Test User',
      }),
    });

    // Act
    await POST(request);

    // Assert
    expect(db.verificationToken.create).toHaveBeenCalledWith({
      data: {
        identifier: 'test@example.com',
        token: 'mock_token_12345',
        expires: expect.any(Date),
      },
    });

    // Check that expiry is ~24 hours from now
    const createCall = vi.mocked(db.verificationToken.create).mock.calls[0][0];
    const expires = createCall.data.expires as Date;
    const now = new Date();
    const hoursDiff = (expires.getTime() - now.getTime()) / (1000 * 60 * 60);
    expect(hoursDiff).toBeGreaterThan(23);
    expect(hoursDiff).toBeLessThan(25);
  });
});
