const { authenticateToken, requireVerifiedEmail } = require('../../../middleware/auth');
const jwt = require('jsonwebtoken');

jest.mock('jsonwebtoken');
jest.mock('../../../models', () => ({
  User: {
    findByPk: jest.fn()
  }
}));

const { User } = require('../../../models');

describe('Auth Middleware', () => {
  let req, res, next;

  beforeEach(() => {
    req = {
      cookies: {}
    };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn()
    };
    next = jest.fn();
    jest.clearAllMocks();
    process.env.JWT_ACCESS_SECRET = 'test-secret';
  });

  describe('Valid token', () => {
    it('should verify valid token from cookie and call next', async () => {
      const mockUser = { id: 1, email: 'test@test.com', jwtVersion: 1 };
      req.cookies.accessToken = 'validtoken';
      jwt.verify.mockReturnValue({ id: 1, jwtVersion: 1 });
      User.findByPk.mockResolvedValue(mockUser);

      await authenticateToken(req, res, next);

      expect(jwt.verify).toHaveBeenCalledWith('validtoken', process.env.JWT_ACCESS_SECRET);
      expect(User.findByPk).toHaveBeenCalledWith(1);
      expect(req.user).toEqual(mockUser);
      expect(next).toHaveBeenCalled();
    });

    it('should handle token with valid user', async () => {
      const mockUser = { id: 2, email: 'user@test.com', firstName: 'Test', jwtVersion: 1 };
      req.cookies.accessToken = 'validtoken2';
      jwt.verify.mockReturnValue({ id: 2, jwtVersion: 1 });
      User.findByPk.mockResolvedValue(mockUser);

      await authenticateToken(req, res, next);

      expect(jwt.verify).toHaveBeenCalledWith('validtoken2', process.env.JWT_ACCESS_SECRET);
      expect(User.findByPk).toHaveBeenCalledWith(2);
      expect(req.user).toEqual(mockUser);
      expect(next).toHaveBeenCalled();
    });
  });

  describe('Invalid token', () => {
    it('should return 401 for missing token', async () => {
      req.cookies = {};

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Access token required',
        code: 'NO_TOKEN'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 401 for invalid token', async () => {
      req.cookies.accessToken = 'invalidtoken';
      jwt.verify.mockImplementation(() => {
        throw new Error('Invalid token');
      });

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Invalid token',
        code: 'INVALID_TOKEN'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 401 for expired token', async () => {
      req.cookies.accessToken = 'expiredtoken';
      const error = new Error('jwt expired');
      error.name = 'TokenExpiredError';
      jwt.verify.mockImplementation(() => {
        throw error;
      });

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Token expired',
        code: 'TOKEN_EXPIRED'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 401 for invalid token format (missing user id)', async () => {
      req.cookies.accessToken = 'tokenwithnoid';
      jwt.verify.mockReturnValue({ email: 'test@test.com' }); // Missing id

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Invalid token format',
        code: 'INVALID_TOKEN_FORMAT'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 401 when user not found', async () => {
      req.cookies.accessToken = 'validtoken';
      jwt.verify.mockReturnValue({ id: 999 });
      User.findByPk.mockResolvedValue(null);

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'User not found',
        code: 'USER_NOT_FOUND'
      });
      expect(next).not.toHaveBeenCalled();
    });
  });

  describe('Edge cases', () => {
    it('should handle empty string token', async () => {
      req.cookies.accessToken = '';

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Access token required',
        code: 'NO_TOKEN'
      });
    });

    it('should handle JWT malformed error', async () => {
      req.cookies.accessToken = 'malformed.token';
      const error = new Error('jwt malformed');
      error.name = 'JsonWebTokenError';
      jwt.verify.mockImplementation(() => {
        throw error;
      });

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Invalid token',
        code: 'INVALID_TOKEN'
      });
    });

    it('should handle database error when finding user', async () => {
      req.cookies.accessToken = 'validtoken';
      jwt.verify.mockReturnValue({ id: 1 });
      User.findByPk.mockRejectedValue(new Error('Database error'));

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Invalid token',
        code: 'INVALID_TOKEN'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should handle undefined cookies', async () => {
      req.cookies = undefined;

      await authenticateToken(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Access token required',
        code: 'NO_TOKEN'
      });
    });
  });
});

describe('requireVerifiedEmail Middleware', () => {
  let req, res, next;

  beforeEach(() => {
    req = {
      user: null
    };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn()
    };
    next = jest.fn();
    jest.clearAllMocks();
  });

  describe('Verified users', () => {
    it('should call next for verified user', () => {
      req.user = {
        id: 1,
        email: 'verified@test.com',
        isVerified: true
      };

      requireVerifiedEmail(req, res, next);

      expect(next).toHaveBeenCalled();
      expect(res.status).not.toHaveBeenCalled();
      expect(res.json).not.toHaveBeenCalled();
    });

    it('should call next for verified OAuth user', () => {
      req.user = {
        id: 2,
        email: 'google@test.com',
        authProvider: 'google',
        isVerified: true
      };

      requireVerifiedEmail(req, res, next);

      expect(next).toHaveBeenCalled();
      expect(res.status).not.toHaveBeenCalled();
    });
  });

  describe('Unverified users', () => {
    it('should return 403 for unverified user', () => {
      req.user = {
        id: 1,
        email: 'unverified@test.com',
        isVerified: false
      };

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Email verification required. Please verify your email address to perform this action.',
        code: 'EMAIL_NOT_VERIFIED'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 403 when isVerified is null', () => {
      req.user = {
        id: 1,
        email: 'test@test.com',
        isVerified: null
      };

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Email verification required. Please verify your email address to perform this action.',
        code: 'EMAIL_NOT_VERIFIED'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 403 when isVerified is undefined', () => {
      req.user = {
        id: 1,
        email: 'test@test.com'
        // isVerified is undefined
      };

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(403);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Email verification required. Please verify your email address to perform this action.',
        code: 'EMAIL_NOT_VERIFIED'
      });
      expect(next).not.toHaveBeenCalled();
    });
  });

  describe('No user', () => {
    it('should return 401 when user is not set', () => {
      req.user = null;

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Authentication required',
        code: 'NO_AUTH'
      });
      expect(next).not.toHaveBeenCalled();
    });

    it('should return 401 when user is undefined', () => {
      req.user = undefined;

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Authentication required',
        code: 'NO_AUTH'
      });
      expect(next).not.toHaveBeenCalled();
    });
  });

  describe('Edge cases', () => {
    it('should handle user object with extra fields', () => {
      req.user = {
        id: 1,
        email: 'test@test.com',
        isVerified: true,
        firstName: 'Test',
        lastName: 'User',
        phone: '1234567890'
      };

      requireVerifiedEmail(req, res, next);

      expect(next).toHaveBeenCalled();
    });

    it('should prioritize missing user over unverified user', () => {
      // If called without authenticateToken first
      req.user = null;

      requireVerifiedEmail(req, res, next);

      expect(res.status).toHaveBeenCalledWith(401);
      expect(res.json).toHaveBeenCalledWith({
        error: 'Authentication required',
        code: 'NO_AUTH'
      });
    });
  });
});