const mockTokensInstance = {
  secretSync: jest.fn().mockReturnValue('mock-secret'),
  create: jest.fn().mockReturnValue('mock-token-123'),
  verify: jest.fn().mockReturnValue(true)
};

jest.mock('csrf', () => {
  return jest.fn().mockImplementation(() => mockTokensInstance);
});

jest.mock('cookie-parser', () => {
  return jest.fn().mockReturnValue((req, res, next) => next());
});

const { csrfProtection, generateCSRFToken, getCSRFToken } = require('../../../middleware/csrf');

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

  beforeEach(() => {
    req = {
      method: 'POST',
      headers: {},
      body: {},
      query: {},
      cookies: {}
    };
    res = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
      send: jest.fn(),
      cookie: jest.fn(),
      set: jest.fn(),
      locals: {}
    };
    next = jest.fn();
    jest.clearAllMocks();
  });

  describe('csrfProtection', () => {
    describe('Safe methods', () => {
      it('should skip CSRF protection for GET requests', () => {
        req.method = 'GET';

        csrfProtection(req, res, next);

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

      it('should skip CSRF protection for HEAD requests', () => {
        req.method = 'HEAD';

        csrfProtection(req, res, next);

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

      it('should skip CSRF protection for OPTIONS requests', () => {
        req.method = 'OPTIONS';

        csrfProtection(req, res, next);

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

    describe('Token validation', () => {
      beforeEach(() => {
        req.cookies = { 'csrf-token': 'mock-token-123' };
      });

      it('should validate token from x-csrf-token header', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });

      it('should validate token from request body', () => {
        req.body.csrfToken = 'mock-token-123';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });

      it('should validate token from query parameters', () => {
        req.query.csrfToken = 'mock-token-123';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });

      it('should prefer header token over body token', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.body.csrfToken = 'different-token';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });

      it('should prefer header token over query token', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.query.csrfToken = 'different-token';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });

      it('should prefer body token over query token', () => {
        req.body.csrfToken = 'mock-token-123';
        req.query.csrfToken = 'different-token';

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });
    });

    describe('Missing tokens', () => {
      it('should return 403 when no token provided', () => {
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

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

      it('should return 403 when no cookie token provided', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = {};

        csrfProtection(req, res, next);

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

      it('should return 403 when cookies object is missing', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = undefined;

        csrfProtection(req, res, next);

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

      it('should return 403 when both tokens are missing', () => {
        req.cookies = {};

        csrfProtection(req, res, next);

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

    describe('Token mismatch', () => {
      it('should return 403 when tokens do not match', () => {
        req.headers['x-csrf-token'] = 'token-from-header';
        req.cookies = { 'csrf-token': 'token-from-cookie' };

        csrfProtection(req, res, next);

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

      it('should return 403 when header token is empty but cookie exists', () => {
        req.headers['x-csrf-token'] = '';
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

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

      it('should return 403 when cookie token is empty but header exists', () => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': '' };

        csrfProtection(req, res, next);

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

    describe('Token verification', () => {
      beforeEach(() => {
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': 'mock-token-123' };
      });

      it('should return 403 when token verification fails', () => {
        mockTokensInstance.verify.mockReturnValue(false);

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(res.status).toHaveBeenCalledWith(403);
        expect(res.json).toHaveBeenCalledWith({
          error: 'Invalid CSRF token',
          code: 'CSRF_TOKEN_INVALID'
        });
        expect(next).not.toHaveBeenCalled();
      });

      it('should call next when token verification succeeds', () => {
        mockTokensInstance.verify.mockReturnValue(true);

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
        expect(res.status).not.toHaveBeenCalled();
      });
    });

    describe('Edge cases', () => {
      it('should handle case-insensitive HTTP methods', () => {
        req.method = 'post';
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });

      it('should handle PUT requests', () => {
        req.method = 'PUT';
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });

      it('should handle DELETE requests', () => {
        req.method = 'DELETE';
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });

      it('should handle PATCH requests', () => {
        req.method = 'PATCH';
        req.headers['x-csrf-token'] = 'mock-token-123';
        req.cookies = { 'csrf-token': 'mock-token-123' };

        csrfProtection(req, res, next);

        expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
        expect(next).toHaveBeenCalled();
      });
    });
  });

  describe('generateCSRFToken', () => {
    it('should generate token and set cookie with proper options', () => {
      process.env.NODE_ENV = 'production';

      generateCSRFToken(req, res, next);

      expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret');
      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
      expect(next).toHaveBeenCalled();
    });

    it('should set secure flag to false in dev environment', () => {
      process.env.NODE_ENV = 'dev';

      generateCSRFToken(req, res, next);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: false,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should set secure flag to true in non-dev environment', () => {
      process.env.NODE_ENV = 'production';

      generateCSRFToken(req, res, next);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should set token in response header', () => {
      generateCSRFToken(req, res, next);

      expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'mock-token-123');
    });

    it('should make token available in res.locals', () => {
      generateCSRFToken(req, res, next);

      expect(res.locals.csrfToken).toBe('mock-token-123');
    });

    it('should call next after setting up token', () => {
      generateCSRFToken(req, res, next);

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

    it('should handle test environment', () => {
      process.env.NODE_ENV = 'test';

      generateCSRFToken(req, res, next);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should handle undefined NODE_ENV', () => {
      delete process.env.NODE_ENV;

      generateCSRFToken(req, res, next);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });
  });

  describe('getCSRFToken', () => {
    it('should generate token and return it in response', () => {
      process.env.NODE_ENV = 'production';

      getCSRFToken(req, res);

      expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret');
      expect(res.status).toHaveBeenCalledWith(204);
      expect(res.send).toHaveBeenCalled();
    });

    it('should set token in cookie with proper options', () => {
      process.env.NODE_ENV = 'production';

      getCSRFToken(req, res);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should set secure flag to false in dev environment', () => {
      process.env.NODE_ENV = 'dev';

      getCSRFToken(req, res);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: false,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should set secure flag to true in production environment', () => {
      process.env.NODE_ENV = 'production';

      getCSRFToken(req, res);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should handle test environment', () => {
      process.env.NODE_ENV = 'test';

      getCSRFToken(req, res);

      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 60 * 60 * 1000
      });
    });

    it('should generate new token each time', () => {
      mockTokensInstance.create
        .mockReturnValueOnce('token-1')
        .mockReturnValueOnce('token-2');

      getCSRFToken(req, res);
      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'token-1', expect.any(Object));
      expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'token-1');

      jest.clearAllMocks();
      getCSRFToken(req, res);
      expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'token-2', expect.any(Object));
      expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'token-2');
    });
  });

  describe('Integration scenarios', () => {
    it('should handle complete CSRF flow', () => {
      // First, generate a token
      generateCSRFToken(req, res, next);
      const generatedToken = res.locals.csrfToken;

      // Reset mocks
      jest.clearAllMocks();

      // Now test protection with the generated token
      req.method = 'POST';
      req.headers['x-csrf-token'] = generatedToken;
      req.cookies = { 'csrf-token': generatedToken };

      csrfProtection(req, res, next);

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

    it('should handle token generation endpoint flow', () => {
      getCSRFToken(req, res);

      const cookieCall = res.cookie.mock.calls[0];
      const headerCall = res.set.mock.calls[0];

      expect(cookieCall[0]).toBe('csrf-token');
      expect(cookieCall[1]).toBe('mock-token-123');
      expect(headerCall[0]).toBe('X-CSRF-Token');
      expect(headerCall[1]).toBe('mock-token-123');
      expect(res.status).toHaveBeenCalledWith(204);
      expect(res.send).toHaveBeenCalled();
    });
  });
});