// src/routes/passport.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import * as bcrypt from 'bcrypt';
import { Request, Response, NextFunction } from 'express';

// Define a type for the JWT verify callback function for type safety.
type VerifyCallback = (payload: { user_id: string }, done: (error: Error | null, user?: object | false) => void) => Promise<void>;

// FIX: Use vi.hoisted to declare variables that need to be accessed inside vi.mock
const { verifyCallbackWrapper } = vi.hoisted(() => {
  return {
    // We use a wrapper object to hold the callback reference
    // Initialize with a more specific type instead of `any`.
    verifyCallbackWrapper: {
      callback: null as VerifyCallback | null
    }
  };
});

// Mock the 'passport-jwt' module to capture the verify callback.
vi.mock('passport-jwt', () => ({
  // The Strategy constructor is mocked to capture its second argument
  Strategy: vi.fn(function(options, verify) {
    // FIX: Assign to the hoisted wrapper object
    verifyCallbackWrapper.callback = verify;
    return { name: 'jwt', authenticate: vi.fn() };
  }),
  ExtractJwt: { fromAuthHeaderAsBearerToken: vi.fn() },
}));

// FIX: Add a similar mock for 'passport-local' to capture its verify callback.
const { localStrategyCallbackWrapper } = vi.hoisted(() => {
  type LocalVerifyCallback = (req: Request, email: string, pass: string, done: (error: Error | null, user?: object | false, options?: { message: string }) => void) => Promise<void>;
  return {
    localStrategyCallbackWrapper: { callback: null as LocalVerifyCallback | null }
  };
});

vi.mock('passport-local', () => ({
  Strategy: vi.fn(function(options, verify) {
    localStrategyCallbackWrapper.callback = verify;
  }),
}));

import * as db from '../services/db/index.db';
import { UserProfile } from '../types';

// Mock dependencies before importing the passport configuration
vi.mock('../services/db/index.db', () => ({
  userRepo: {
    findUserByEmail: vi.fn(),
    findUserWithProfileByEmail: vi.fn(), // ADD THIS
    findUserProfileById: vi.fn(),
  },
  adminRepo: {
    incrementFailedLoginAttempts: vi.fn(),
    resetFailedLoginAttempts: vi.fn(),
    logActivity: vi.fn(),
  },
}));

const mockedDb = db as Mocked<typeof db>;

vi.mock('../services/logger.server', () => ({
  // This mock is used by the module under test and can be imported in the test file.
  logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));

// Mock bcrypt for password comparisons
vi.mock('bcrypt', () => ({
  compare: vi.fn(),
}));

// Mock the passport library
vi.mock('passport', () => {
  const mAuthenticate = vi.fn(() => (req: Request, res: Response, next: NextFunction) => next());
  return {
    // The `passport` module has a default export which is an object containing the methods.
    default: {
      use: vi.fn(),
      initialize: vi.fn(() => (req: Request, res: Response, next: NextFunction) => next()),
      authenticate: mAuthenticate,
    },
    // It also has named exports, so we mock `authenticate` here as well for completeness.
    authenticate: mAuthenticate,
  };
});

// Now, import the passport configuration which will use our mocks
import passport, { isAdmin, optionalAuth } from './passport.routes';
import { logger } from '../services/logger.server';

describe('Passport Configuration', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.resetModules();
  });

  describe('LocalStrategy (Isolated Callback Logic)', () => {
    // FIX: mockReq needs a 'log' property because the implementation uses req.log
    const mockReq = { ip: '127.0.0.1', log: logger } as unknown as Request;
    const done = vi.fn();

    it('should call done(null, user) on successful authentication', async () => {
      // Arrange
    const mockUser = { 
      user_id: 'user-123', 
      email: 'test@test.com', 
      password_hash: 'hashed_password', 
      failed_login_attempts: 0, 
      last_failed_login: null,
      points: 0,
      role: 'user' as const
    };
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
      vi.mocked(bcrypt.compare).mockResolvedValue(true as never);

      // Act
      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'test@test.com', 'password', done);
      }

      // Assert
      expect(mockedDb.userRepo.findUserWithProfileByEmail).toHaveBeenCalledWith('test@test.com', logger);
      expect(bcrypt.compare).toHaveBeenCalledWith('password', 'hashed_password');
      expect(mockedDb.adminRepo.resetFailedLoginAttempts).toHaveBeenCalledWith('user-123', '127.0.0.1', logger);
      expect(done).toHaveBeenCalledWith(null, mockUser);
    });

    it('should call done(null, false) if user is not found', async () => {
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(undefined);

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'notfound@test.com', 'password', done);
      }

      expect(done).toHaveBeenCalledWith(null, false, { message: 'Incorrect email or password.' });
    });

    it('should call done(null, false) and increment failed attempts on password mismatch', async () => {
    const mockUser = { 
      user_id: 'user-123', 
      email: 'test@test.com', 
      password_hash: 'hashed_password', 
      failed_login_attempts: 1, 
      last_failed_login: null,
      points: 0,
      role: 'user' as const
    };
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
      vi.mocked(bcrypt.compare).mockResolvedValue(false as never);

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'test@test.com', 'wrong_password', done);
      }

      expect(mockedDb.adminRepo.incrementFailedLoginAttempts).toHaveBeenCalledWith('user-123', logger);
      expect(mockedDb.adminRepo.logActivity).toHaveBeenCalledWith(expect.objectContaining({ action: 'login_failed_password' }), logger);
      expect(done).toHaveBeenCalledWith(null, false, { message: 'Incorrect email or password.' });
    });

    it('should call done(null, false) for an OAuth user (no password hash)', async () => {
    const mockUser = { 
      user_id: 'oauth-user', 
      email: 'oauth@test.com', 
      password_hash: null, 
      failed_login_attempts: 0, 
      last_failed_login: null,
      points: 0,
      role: 'user' as const
    };
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'oauth@test.com', 'any_password', done);
      }

      expect(done).toHaveBeenCalledWith(null, false, { message: 'This account was created using a social login. Please use Google or GitHub to sign in.' });
    });

    it('should call done(null, false) if account is locked', async () => {
      const mockUser = {
        user_id: 'locked-user',
        email: 'locked@test.com',
        password_hash: 'hashed_password',
        failed_login_attempts: 5,
      last_failed_login: new Date().toISOString(),
      points: 0,
      role: 'user' as const
      };
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'locked@test.com', 'any_password', done);
      }

      expect(done).toHaveBeenCalledWith(null, false, { message: 'Account is temporarily locked. Please try again in 15 minutes.' });
    });

    it('should allow login if lockout period has expired', async () => {
      const mockUser = {
        user_id: 'expired-lock-user',
        email: 'expired@test.com',
        password_hash: 'hashed_password',
        failed_login_attempts: 5,
      last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(),
      points: 0,
      role: 'user' as const
      };
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
      vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Correct password

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'expired@test.com', 'correct_password', done);
      }

      // Should proceed to successful login
      expect(mockedDb.adminRepo.resetFailedLoginAttempts).toHaveBeenCalled();
      expect(done).toHaveBeenCalledWith(null, expect.any(Object));
    });

    it('should call done(err) if the database lookup fails', async () => {
      const dbError = new Error('DB connection failed');
      vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockRejectedValue(dbError);

      if (localStrategyCallbackWrapper.callback) {
        await localStrategyCallbackWrapper.callback(mockReq, 'any@test.com', 'any_password', done);
      }

      expect(done).toHaveBeenCalledWith(dbError);
    });
  });

  describe('JwtStrategy (Isolated Callback Logic)', () => {
    it('should call done(null, userProfile) on successful authentication', async () => {
      // Arrange
      const jwtPayload = { user_id: 'user-123' };      
      const mockProfile = { user_id: 'user-123', role: 'user', points: 100, user: { user_id: 'user-123', email: 'test@test.com' } } as UserProfile;
      vi.mocked(mockedDb.userRepo.findUserProfileById).mockResolvedValue(mockProfile);
      const done = vi.fn();

      // Act: Invoke the captured callback from the wrapper
      // Use a non-null assertion `!` because we know the mock setup populates it.
      if (verifyCallbackWrapper.callback) {
        await verifyCallbackWrapper.callback(jwtPayload, done);
      }

      // Assert
      expect(mockedDb.userRepo.findUserProfileById).toHaveBeenCalledWith('user-123', logger);
      expect(done).toHaveBeenCalledWith(null, mockProfile);
    });

    it('should call done(null, false) when user is not found', async () => {
      // Arrange
      const jwtPayload = { user_id: 'non-existent-user' };
      // Per ADR-001, the repository method throws an error when the user is not found.
      // The JWT strategy's catch block will then handle this and call done(err, false).
      const notFoundError = new Error('User not found');
      vi.mocked(mockedDb.userRepo.findUserProfileById).mockRejectedValue(notFoundError);
      const done = vi.fn();

      // Act
      if (verifyCallbackWrapper.callback) {
        await verifyCallbackWrapper.callback(jwtPayload, done);
      }

      // Assert
      // The strategy's catch block passes the error to done().
      expect(done).toHaveBeenCalledWith(notFoundError, false);
    });

    it('should call done(err) if the database lookup fails', async () => {
      // Arrange
      const jwtPayload = { user_id: 'user-123' };
      const dbError = new Error('DB connection failed');
      vi.mocked(mockedDb.userRepo.findUserProfileById).mockRejectedValue(dbError);
      const done = vi.fn();

      // Act
      if (verifyCallbackWrapper.callback) {
        await verifyCallbackWrapper.callback(jwtPayload, done);
      }

      // Assert
      expect(done).toHaveBeenCalledWith(dbError, false);
    });
  });

  describe('isAdmin Middleware', () => {
    const mockNext: NextFunction = vi.fn();
    let mockRes: Partial<Response>;

    beforeEach(() => {
      // Reset response mock for each test
      mockRes = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      };
    });

    it('should call next() if user has "admin" role', () => {
      // Arrange
      const mockReq: Partial<Request> = {
        // Create a complete, type-safe mock UserProfile object.
        user: {
          user_id: 'admin-id',
          role: 'admin',
          points: 100,
          user: { user_id: 'admin-id', email: 'admin@test.com' }
        },
      };

      // Act
      isAdmin(mockReq as Request, mockRes as Response, mockNext);

      // Assert
      expect(mockNext).toHaveBeenCalledTimes(1);
      expect(mockRes.status).not.toHaveBeenCalled();
    });

    it('should return 403 Forbidden if user does not have "admin" role', () => {
      // Arrange
      const mockReq: Partial<Request> = {
        user: {
          user_id: 'user-id',
          role: 'user',
          points: 50,
          user: { user_id: 'user-id', email: 'user@test.com' }
        },
      };

      // Act
      isAdmin(mockReq as Request, mockRes as Response, mockNext);

      // Assert
      expect(mockNext).not.toHaveBeenCalled();
      expect(mockRes.status).toHaveBeenCalledWith(403);
      expect(mockRes.json).toHaveBeenCalledWith({ message: 'Forbidden: Administrator access required.' });
    });

    it('should return 403 Forbidden if req.user is missing', () => {
      // Arrange
      const mockReq = {} as Request; // No req.user

      // Act
      isAdmin(mockReq, mockRes as Response, mockNext);

      // Assert
      expect(mockNext).not.toHaveBeenCalled();
      expect(mockRes.status).toHaveBeenCalledWith(403);
    });
  });

  describe('optionalAuth Middleware', () => {
    const mockNext: NextFunction = vi.fn();
    let mockRes: Partial<Response>;

    beforeEach(() => {
      mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn() };
    });

    it('should populate req.user and call next() if authentication succeeds', () => {
      // Arrange
      const mockReq = {} as Request;
      const mockUser = { user_id: 'user-123' };
      // Mock passport.authenticate to call its callback with a user
      vi.mocked(passport.authenticate).mockImplementation(
        (_strategy, _options, callback) => () => callback?.(null, mockUser, undefined)
      );

      // Act
      optionalAuth(mockReq, mockRes as Response, mockNext);

      // Assert
      expect(mockReq.user).toEqual(mockUser);
      expect(mockNext).toHaveBeenCalledTimes(1);
    });

    it('should not populate req.user and still call next() if authentication fails', () => {
      // Arrange
      const mockReq = {} as Request;
      vi.mocked(passport.authenticate).mockImplementation(
        (_strategy, _options, callback) => () => callback?.(null, false, undefined)
      );

      optionalAuth(mockReq, mockRes as Response, mockNext);

      expect(mockReq.user).toBeUndefined();
      expect(mockNext).toHaveBeenCalledTimes(1);
    });

    it('should log info and call next() if authentication provides an info message', () => {
      // Arrange
      const mockReq = {} as Request;
      const mockInfo = { message: 'Token expired' };
      // Mock passport.authenticate to call its callback with an info object
      vi.mocked(passport.authenticate).mockImplementation(
        (_strategy, _options, callback) => () => callback?.(null, false, mockInfo)
      );

      // Act
      optionalAuth(mockReq, mockRes as Response, mockNext);

      // Assert
      // Pino logger convention: (obj, msg)
      expect(logger.info).toHaveBeenCalledWith({ info: 'Token expired' }, 'Optional auth info:');
      expect(mockNext).toHaveBeenCalledTimes(1);
    });
  });

  // ... (Keep other describe blocks: LocalStrategy, isAdmin Middleware, optionalAuth Middleware)
  // I'm omitting them here for brevity as they didn't have specific failures related to the hoisting issue,
  // but they should be preserved in the final file.
   describe('isAdmin Middleware', () => {
    const mockNext: NextFunction = vi.fn();
    let mockRes: Partial<Response>;

    beforeEach(() => {
      mockRes = {
        status: vi.fn().mockReturnThis(),
        json: vi.fn(),
      };
    });

    it('should call next() if user has "admin" role', () => {
      const mockReq: Partial<Request> = {
        user: {
          user_id: 'admin-id',
          role: 'admin',
          points: 100,
          user: { user_id: 'admin-id', email: 'admin@test.com' }
        }
      };
      isAdmin(mockReq as Request, mockRes as Response, mockNext);
      expect(mockNext).toHaveBeenCalledTimes(1);
    });

    it('should return 403 Forbidden if user does not have "admin" role', () => {
      const mockReq: Partial<Request> = {
        user: {
          user_id: 'user-id',
          role: 'user',
          points: 50,
          user: { user_id: 'user-id', email: 'user@test.com' }
        }
      };
      isAdmin(mockReq as Request, mockRes as Response, mockNext);
      expect(mockRes.status).toHaveBeenCalledWith(403);
    });
  });

  describe('optionalAuth Middleware', () => {
    const mockNext: NextFunction = vi.fn();
    let mockRes: Partial<Response>;
    beforeEach(() => {
       mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn() };
    });

    it('should populate req.user and call next() if authentication succeeds', () => {
      const mockReq = {} as Request;
      const mockUser = { user_id: 'user-123' };
      vi.mocked(passport.authenticate).mockImplementation(
        (_strategy, _options, callback) => () => callback?.(null, mockUser, undefined)
      );
      optionalAuth(mockReq, mockRes as Response, mockNext);
      expect(mockReq.user).toEqual(mockUser);
      expect(mockNext).toHaveBeenCalledTimes(1);
    });
  });
});