// src/routes/auth.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import { Request, Response, NextFunction } from 'express';
import cookieParser from 'cookie-parser';
import * as bcrypt from 'bcrypt'; 
import { createMockUserProfile, createMockUserWithPasswordHash } from '../tests/utils/mockFactories';
import { mockLogger } from '../tests/utils/mockLogger';

// --- FIX: Hoist passport mocks to be available for vi.mock ---
const passportMocks = vi.hoisted(() => {
  type PassportCallback = (error: Error | null, user?: Express.User | false, info?: { message: string }) => void;

  const authenticateMock = (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: Response, next: NextFunction) => {
    // Simulate LocalStrategy logic based on request body
    if (req.body.password === 'wrong_password') {
      return callback(null, false, { message: 'Incorrect email or password.' });
    }
    if (req.body.email === 'locked@test.com') {
      return callback(null, false, { message: 'Account is temporarily locked. Please try again in 15 minutes.' });
    }
    if (req.body.email === 'notfound@test.com') {
      return callback(null, false, { message: 'Login failed' });
    }
    // Specific case for strategy error
    if (req.body.email === 'dberror@test.com') {
      return callback(new Error('Database connection failed'), false);
    }
    
    // Default success case
    const user = createMockUserProfile({ user_id: 'user-123', user: { user_id: 'user-123', email: req.body.email } });
    
    // If a callback is provided (custom callback signature), call it
    if (callback) {
        return callback(null, user);
    }
    
    // Standard middleware signature: attach user and call next
    req.user = user;
    next();
  };

  return { authenticateMock };
});

// --- 2. Module Mocks ---

// Mock the local passport.routes module to control its behavior.
vi.mock('./passport.routes', () => ({
  default: {
    authenticate: vi.fn().mockImplementation(passportMocks.authenticateMock),
    use: vi.fn(),
    initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
    session: () => (req: Request, res: Response, next: NextFunction) => next(),
  },
  // Also mock named exports if they were used in auth.routes.ts, though they are not currently.
  isAdmin: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
  optionalAuth: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
}));

// Mock the DB connection pool to control transactional behavior
const { mockPool } = vi.hoisted(() => {
  const client = {
    query: vi.fn(),
    release: vi.fn(),
  };
  return {
    mockPool: { 
      connect: vi.fn(() => Promise.resolve(client)),
    },
    mockClient: client,
  };
});
// Mock the Service Layer directly.
// We use async import inside the factory to properly hoist the UniqueConstraintError class usage.
vi.mock('../services/db/index.db', async () => {
  const { UniqueConstraintError } = await import('../services/db/errors.db');
  return {
    userRepo: {
      findUserByEmail: vi.fn(),
      createUser: vi.fn(),
      saveRefreshToken: vi.fn(),
      createPasswordResetToken: vi.fn(),
      getValidResetTokens: vi.fn(),
      updateUserPassword: vi.fn(),
      deleteResetToken: vi.fn(),
      findUserByRefreshToken: vi.fn(),
      deleteRefreshToken: vi.fn(),
    },
    adminRepo: {
      logActivity: vi.fn(),
    },
    UniqueConstraintError: UniqueConstraintError,
  };
});

vi.mock('../services/db/connection.db', () => ({
  getPool: () => mockPool,
}));


// Mock the logger
vi.mock('../services/logger.server', () => ({
  logger: mockLogger,
}));

// Mock the email service
vi.mock('../services/emailService.server', () => ({
  sendPasswordResetEmail: vi.fn(),
}));

// Mock bcrypt
vi.mock('bcrypt', async (importOriginal) => {
  const actual = await importOriginal<typeof bcrypt>();
  return { ...actual, compare: vi.fn() };
});

// Import the router AFTER mocks are established
import authRouter from './auth.routes';
import * as db from '../services/db/index.db'; // This was a duplicate, fixed.

import { UniqueConstraintError } from '../services/db/errors.db'; // Import actual class for instanceof checks

// --- 4. App Setup ---
// We need to inject cookie-parser BEFORE the router is mounted.
// Since createTestApp mounts the router immediately, we pass middleware to it if supported,
// or we construct the app manually here to ensure correct order.
// Assuming createTestApp doesn't support pre-middleware injection easily, we will
// create a standard express app here for full control, or modify createTestApp usage if possible.
// Looking at createTestApp.ts (inferred), it likely doesn't take middleware.
// Let's manually build the app for this test file to ensure cookieParser runs first.

import express from 'express';
import { errorHandler } from '../middleware/errorHandler'; // Assuming this exists

const app = express();
app.use(express.json());
app.use(cookieParser()); // Mount BEFORE router

// Middleware to inject the mock logger into req
app.use((req, res, next) => {
  req.log = mockLogger;
  next();
});

app.use('/api/auth', authRouter);
app.use(errorHandler); // Mount AFTER router

// --- 5. Tests ---
describe('Auth Routes (/api/auth)', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.restoreAllMocks(); // Restore spies on prototypes
  });

  describe('POST /register', () => {
    const newUserEmail = 'newuser@test.com';
    const strongPassword = 'a-Very-Strong-Password-123!';

    it('should successfully register a new user with a strong password', async () => {
      // Arrange:
      const mockNewUser = createMockUserProfile({ user_id: 'new-user-id', user: { user_id: 'new-user-id', email: newUserEmail }, full_name: 'Test User' });

      // FIX: Mock the method on the imported singleton instance `userRepo` directly,
      // as this is what the route handler uses. Spying on the prototype does not
      // affect this already-created instance.
      vi.mocked(db.userRepo.createUser).mockResolvedValue(mockNewUser);
      vi.mocked(db.userRepo.saveRefreshToken).mockResolvedValue(undefined);
      vi.mocked(db.adminRepo.logActivity).mockResolvedValue(undefined);

      // Act
      const response = await supertest(app)
        .post('/api/auth/register')
        .send({
          email: newUserEmail,
          password: strongPassword,
          full_name: 'Test User',
        });

      // Assert
      expect(response.status).toBe(201);
      expect(response.body.message).toBe('User registered successfully!');
      expect(response.body.user.email).toBe(newUserEmail);
      expect(response.body.token).toBeTypeOf('string'); // This was a duplicate, fixed.
      expect(db.userRepo.createUser).toHaveBeenCalled();
    });

    it('should reject registration with a weak password', async () => {
      const weakPassword = 'password';

      const response = await supertest(app)
        .post('/api/auth/register')
        .send({
          email: 'anotheruser@test.com',
          password: weakPassword,
        });

      expect(response.status).toBe(400);
      // The validation middleware returns errors in an array.
      // We check if any of the error messages contain the expected text.
      interface ZodError {
        message: string;
      }
      const errorMessages = response.body.errors?.map((e: ZodError) => e.message).join(' ');
      expect(errorMessages).toMatch(/Password is too weak/i);
    });

    it('should reject registration if the email already exists', async () => {
      // Create an error object that includes the 'code' property for simulating a PG unique violation.
      // This is more type-safe than casting to 'any'.
      const dbError = new UniqueConstraintError('User with that email already exists.') as UniqueConstraintError & { code: string };
      dbError.code = '23505';

      vi.mocked(db.userRepo.createUser).mockRejectedValue(dbError);

      const response = await supertest(app)
        .post('/api/auth/register')
        .send({ email: newUserEmail, password: strongPassword });

      expect(response.status).toBe(409); // 409 Conflict
      expect(response.body.message).toBe('User with that email already exists.');
      expect(db.userRepo.createUser).toHaveBeenCalled();
    });

    it('should return 500 if a generic database error occurs during registration', async () => {
      const dbError = new Error('DB connection lost'); 
      vi.mocked(db.userRepo.createUser).mockRejectedValue(dbError);

      const response = await supertest(app)
        .post('/api/auth/register')
        .send({ email: 'fail@test.com', password: strongPassword });

      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB connection lost'); // The errorHandler will forward the message
    });

    it('should return 400 for an invalid email format', async () => {
      const response = await supertest(app)
        .post('/api/auth/register')
        .send({ email: 'not-an-email', password: strongPassword });

      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toBe('A valid email is required.');
    });

    it('should return 400 for a password that is too short', async () => {
      const response = await supertest(app)
        .post('/api/auth/register')
        .send({ email: newUserEmail, password: 'short' });

      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toBe('Password must be at least 8 characters long.');
    });
  });

  describe('POST /login', () => {
    it('should successfully log in a user and return a token and cookie', async () => {
      // Arrange:
      const loginCredentials = { email: 'test@test.com', password: 'password123' };
      vi.mocked(db.userRepo.saveRefreshToken).mockResolvedValue(undefined);

      // Act
      const response = await supertest(app)
        .post('/api/auth/login')
        .send(loginCredentials);

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.user).toEqual({ user_id: 'user-123', email: loginCredentials.email });
      expect(response.body.token).toBeTypeOf('string');
      expect(response.headers['set-cookie']).toBeDefined();
    });

    it('should reject login for incorrect credentials', async () => {
      const response = await supertest(app)
        .post('/api/auth/login')
        .send({ email: 'test@test.com', password: 'wrong_password' });

      expect(response.status).toBe(401);
      expect(response.body.message).toBe('Incorrect email or password.');
    });

    it('should reject login for a locked account', async () => {
      const response = await supertest(app)
        .post('/api/auth/login')
        .send({ email: 'locked@test.com', password: 'password123' });

      expect(response.status).toBe(401);
      expect(response.body.message).toBe('Account is temporarily locked. Please try again in 15 minutes.');
    });

    it('should return 401 if user is not found', async () => {
        const response = await supertest(app)
        .post('/api/auth/login') // This was a duplicate, fixed.
        .send({ email: 'notfound@test.com', password: 'password123' });

      expect(response.status).toBe(401);
    });

    it('should return 500 if saving the refresh token fails', async () => {
      // Arrange:
      const loginCredentials = { email: 'test@test.com', password: 'password123' };
      vi.mocked(db.userRepo.saveRefreshToken).mockRejectedValue(new Error('DB write failed'));

      // Act
      const response = await supertest(app).post('/api/auth/login').send(loginCredentials);

      // Assert
      expect(response.status).toBe(500);
    });

    it('should return 500 if passport strategy returns an error', async () => {
      // This test covers the `if (err)` block in the passport.authenticate callback.
      // The mock implementation for passport.authenticate is configured to return an error
      // when the email is 'dberror@test.com'.

      const response = await supertest(app)
        .post('/api/auth/login')
        .send({ email: 'dberror@test.com', password: 'any_password' });

      expect(response.status).toBe(500);
      expect(response.body.message).toBe('Database connection failed');
    });

    it('should set a long-lived cookie when rememberMe is true', async () => {
      // Arrange
      const loginCredentials = { email: 'test@test.com', password: 'password123', rememberMe: true };
      vi.mocked(db.userRepo.saveRefreshToken).mockResolvedValue(undefined);

      // Act
      const response = await supertest(app)
        .post('/api/auth/login')
        .send(loginCredentials);

      // Assert
      expect(response.status).toBe(200);
      const setCookieHeader = response.headers['set-cookie'];
      expect(setCookieHeader[0]).toContain('Max-Age=2592000'); // 30 days in seconds
    });
  });

  describe('POST /forgot-password', () => {
    it('should send a reset link if the user exists', async () => {
      // Arrange
      vi.mocked(db.userRepo.findUserByEmail).mockResolvedValue(createMockUserWithPasswordHash({ user_id: 'user-123', email: 'test@test.com' }));
      vi.mocked(db.userRepo.createPasswordResetToken).mockResolvedValue(undefined);

      // Act
      const response = await supertest(app)
        .post('/api/auth/forgot-password')
        .send({ email: 'test@test.com' });

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.message).toContain('a password reset link has been sent'); // This was a duplicate, fixed.
      expect(response.body.token).toBeTypeOf('string');
    });

    it('should return a generic success message even if the user does not exist', async () => {
      vi.mocked(db.userRepo.findUserByEmail).mockResolvedValue(undefined);

      const response = await supertest(app)
        .post('/api/auth/forgot-password')
        .send({ email: 'nouser@test.com' });

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('a password reset link has been sent');
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.userRepo.findUserByEmail).mockRejectedValue(new Error('DB connection failed'));
      const response = await supertest(app)
        .post('/api/auth/forgot-password')
        .send({ email: 'any@test.com' });

      expect(response.status).toBe(500);
    });

    it('should still return 200 OK if the email service fails', async () => {
      // Arrange
      vi.mocked(db.userRepo.findUserByEmail).mockResolvedValue(createMockUserWithPasswordHash({ user_id: 'user-123', email: 'test@test.com' }));
      vi.mocked(db.userRepo.createPasswordResetToken).mockResolvedValue(undefined);
      // Mock the email service to fail
      const { sendPasswordResetEmail } = await import('../services/emailService.server');
      vi.mocked(sendPasswordResetEmail).mockRejectedValue(new Error('SMTP server down'));

      // Act
      const response = await supertest(app)
        .post('/api/auth/forgot-password')
        .send({ email: 'test@test.com' });

      // Assert: The route should not fail even if the email does.
      expect(response.status).toBe(200);
    });

    it('should return 400 for an invalid email format', async () => {
      const response = await supertest(app)
        .post('/api/auth/forgot-password')
        .send({ email: 'invalid-email' });

      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toBe('A valid email is required.');
    });
  });

  describe('POST /reset-password', () => {
    it('should reset the password with a valid token and strong password', async () => {
      const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000) };
      vi.mocked(db.userRepo.getValidResetTokens).mockResolvedValue([tokenRecord]); // This was a duplicate, fixed.
      vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Token matches
      vi.mocked(db.userRepo.updateUserPassword).mockResolvedValue(undefined);
      vi.mocked(db.userRepo.deleteResetToken).mockResolvedValue(undefined);
      vi.mocked(db.adminRepo.logActivity).mockResolvedValue(undefined);

      const response = await supertest(app)
        .post('/api/auth/reset-password')
        .send({ token: 'valid-token', newPassword: 'a-Very-Strong-Password-789!' });

      expect(response.status).toBe(200);
      expect(response.body.message).toBe('Password has been reset successfully.');
    });

    it('should reject with an invalid or expired token', async () => {
      vi.mocked(db.userRepo.getValidResetTokens).mockResolvedValue([]); // No valid tokens found

      const response = await supertest(app)
        .post('/api/auth/reset-password')
        .send({ token: 'invalid-token', newPassword: 'a-Very-Strong-Password-123!' }); // Use strong password to pass validation

      expect(response.status).toBe(400);
      expect(response.body.message).toBe('Invalid or expired password reset token.');
    });

    it('should return 400 for a weak new password', async () => {
      const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000) };
      vi.mocked(db.userRepo.getValidResetTokens).mockResolvedValue([tokenRecord]);
      vi.mocked(bcrypt.compare).mockResolvedValue(true as never);

      const response = await supertest(app).post('/api/auth/reset-password').send({ token: 'valid-token', newPassword: 'weak' });
      expect(response.status).toBe(400);
    });

    it('should return 400 if token is missing', async () => {
      const response = await supertest(app)
        .post('/api/auth/reset-password')
        .send({ newPassword: 'a-Very-Strong-Password-789!' });

      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toMatch(/Token is required|Required/i);
    });
  });

  describe('POST /refresh-token', () => {
    it('should issue a new access token with a valid refresh token cookie', async () => {
      const mockUser = createMockUserWithPasswordHash({ user_id: 'user-123', email: 'test@test.com' });
      vi.mocked(db.userRepo.findUserByRefreshToken).mockResolvedValue(mockUser);

      const response = await supertest(app)
        .post('/api/auth/refresh-token')
        .set('Cookie', 'refreshToken=valid-refresh-token');

      expect(response.status).toBe(200);
      expect(response.body.token).toBeTypeOf('string');
    });

    it('should return 401 if no refresh token cookie is provided', async () => {
      const response = await supertest(app).post('/api/auth/refresh-token');
      expect(response.status).toBe(401);
      expect(response.body.message).toBe('Refresh token not found.');
    });
    
     it('should return 403 if refresh token is invalid', async () => {
        // Mock finding no user for this token, which should trigger the 403 logic
        vi.mocked(db.userRepo.findUserByRefreshToken).mockResolvedValue(undefined as any);
        
        const response = await supertest(app)
        .post('/api/auth/refresh-token')
        .set('Cookie', 'refreshToken=invalid-token');
        
        expect(response.status).toBe(403);
     });

    it('should return 500 if the database call fails', async () => {
      // Arrange
      vi.mocked(db.userRepo.findUserByRefreshToken).mockRejectedValue(new Error('DB Error'));

      // Act
      const response = await supertest(app)
        .post('/api/auth/refresh-token')
        .set('Cookie', 'refreshToken=any-token');
      expect(response.status).toBe(500);
      expect(response.body.message).toMatch(/DB Error/);
    });
  });

  describe('POST /logout', () => {
    it('should clear the refresh token cookie and return a success message', async () => {
      // Arrange
      vi.mocked(db.userRepo.deleteRefreshToken).mockResolvedValue(undefined);

      // Act
      const response = await supertest(app)
        .post('/api/auth/logout')
        .set('Cookie', 'refreshToken=some-valid-token');

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.message).toBe('Logged out successfully.');

      // Check that the 'set-cookie' header is trying to expire the cookie
      const setCookieHeader = response.headers['set-cookie'];
      expect(setCookieHeader).toBeDefined();
      expect(setCookieHeader[0]).toContain('refreshToken=;');
      expect(setCookieHeader[0]).toContain('Expires=Thu, 01 Jan 1970');
    });

    it('should still return 200 OK even if deleting the refresh token from DB fails', async () => {
      // Arrange
      const dbError = new Error('DB connection lost');
      vi.mocked(db.userRepo.deleteRefreshToken).mockRejectedValue(dbError);
      const { logger } = await import('../services/logger.server');

      // Act
      const response = await supertest(app)
        .post('/api/auth/logout')
        .set('Cookie', 'refreshToken=some-token');

      // Assert
      expect(response.status).toBe(200);
      expect(logger.error).toHaveBeenCalledWith(
        expect.objectContaining({ error: dbError }),
        'Failed to delete refresh token from DB during logout.'
      );
    });
  });
});