import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { Response, NextFunction } from 'express';
import { AuthenticatedRequest, ForbiddenError, UnauthorizedError } from '../../types/index.js';

// Mock dependencies
const mockHasPermission = jest.fn();
const mockGetEffectivePermissions = jest.fn();
const mockCheckCachedPermission = jest.fn();
const mockGetCachedPermissions = jest.fn();
const mockSetCachedPermissions = jest.fn();
const mockLoggerWarn = jest.fn();
const mockLoggerDebug = jest.fn();

jest.mock('../../../modules/roles/permissions.service.js', () => ({
  permissionsService: {
    hasPermission: (...args: any[]) => mockHasPermission(...args),
    getEffectivePermissions: (...args: any[]) => mockGetEffectivePermissions(...args),
  },
}));

jest.mock('../../services/permission-cache.service.js', () => ({
  checkCachedPermission: (...args: any[]) => mockCheckCachedPermission(...args),
  getCachedPermissions: (...args: any[]) => mockGetCachedPermissions(...args),
  setCachedPermissions: (...args: any[]) => mockSetCachedPermissions(...args),
}));

jest.mock('../../utils/logger.js', () => ({
  logger: {
    warn: (...args: any[]) => mockLoggerWarn(...args),
    debug: (...args: any[]) => mockLoggerDebug(...args),
    info: jest.fn(),
    error: jest.fn(),
  },
}));

// Import after mocking
import {
  requirePerm,
  requireAnyPerm,
  requireAllPerms,
  requireAccess,
  requirePermOrOwner,
} from '../rbac.middleware.js';

describe('RBAC Middleware', () => {
  let mockReq: Partial<AuthenticatedRequest>;
  let mockRes: Partial<Response>;
  let mockNext: NextFunction;
  const tenantId = 'tenant-uuid-1';
  const userId = 'user-uuid-1';

  beforeEach(() => {
    jest.clearAllMocks();
    mockReq = {
      user: {
        userId,
        tenantId,
        email: 'test@test.com',
        roles: ['manager'],
      } as any,
      params: {},
      body: {},
    };
    mockRes = {
      status: jest.fn().mockReturnThis() as any,
      json: jest.fn() as any,
    };
    mockNext = jest.fn();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  describe('requirePerm', () => {
    it('should call next() when user has permission via cache', async () => {
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requirePerm('invoices:create');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledWith(tenantId, userId, 'invoices', 'create');
      expect(mockNext).toHaveBeenCalledWith();
      expect(mockHasPermission).not.toHaveBeenCalled();
    });

    it('should call next() when user has permission via database (cache miss)', async () => {
      mockCheckCachedPermission.mockResolvedValue(null);
      mockHasPermission.mockResolvedValue(true);
      mockGetEffectivePermissions.mockResolvedValue([
        { resource: 'invoices', action: 'create' },
      ]);

      const middleware = requirePerm('invoices:create');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledWith(tenantId, userId, 'invoices', 'create');
      expect(mockHasPermission).toHaveBeenCalledWith(tenantId, userId, 'invoices', 'create');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should throw ForbiddenError when user lacks permission', async () => {
      mockCheckCachedPermission.mockResolvedValue(false);

      const middleware = requirePerm('invoices:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ForbiddenError);
      expect(mockLoggerWarn).toHaveBeenCalledWith(
        'Access denied (requirePerm)',
        expect.objectContaining({
          userId,
          tenantId,
          permission: 'invoices:delete',
        })
      );
    });

    it('should bypass permission check for super_admin', async () => {
      mockReq.user = {
        userId,
        tenantId,
        email: 'super@test.com',
        roles: ['super_admin'],
      } as any;

      const middleware = requirePerm('invoices:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockHasPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should throw UnauthorizedError when user is not authenticated', async () => {
      mockReq.user = undefined;

      const middleware = requirePerm('invoices:read');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(UnauthorizedError);
    });

    it('should support dot notation for permission codes', async () => {
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requirePerm('invoices.create');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledWith(tenantId, userId, 'invoices', 'create');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should warm up cache after database check when permission denied', async () => {
      mockCheckCachedPermission.mockResolvedValue(null);
      mockHasPermission.mockResolvedValue(false);
      mockGetEffectivePermissions.mockResolvedValue([]);

      const middleware = requirePerm('invoices:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockSetCachedPermissions).toHaveBeenCalledWith(tenantId, userId, []);
    });
  });

  describe('requireAnyPerm', () => {
    it('should call next() when user has at least one permission', async () => {
      mockCheckCachedPermission
        .mockResolvedValueOnce(false)
        .mockResolvedValueOnce(true);

      const middleware = requireAnyPerm('invoices:delete', 'invoices:read');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should throw ForbiddenError when user lacks all permissions', async () => {
      mockCheckCachedPermission.mockResolvedValue(false);

      const middleware = requireAnyPerm('invoices:delete', 'users:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ForbiddenError);
      expect(mockLoggerWarn).toHaveBeenCalledWith(
        'Access denied (requireAnyPerm)',
        expect.objectContaining({
          permissions: ['invoices:delete', 'users:delete'],
        })
      );
    });

    it('should bypass for super_admin', async () => {
      mockReq.user = {
        userId,
        tenantId,
        email: 'super@test.com',
        roles: ['super_admin'],
      } as any;

      const middleware = requireAnyPerm('invoices:delete', 'users:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });
  });

  describe('requireAllPerms', () => {
    it('should call next() when user has all permissions', async () => {
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requireAllPerms('invoices:read', 'payments:read');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledTimes(2);
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should throw ForbiddenError when user lacks any permission', async () => {
      mockCheckCachedPermission
        .mockResolvedValueOnce(true)
        .mockResolvedValueOnce(false);

      const middleware = requireAllPerms('invoices:read', 'invoices:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ForbiddenError);
      expect(mockLoggerWarn).toHaveBeenCalledWith(
        'Access denied (requireAllPerms)',
        expect.objectContaining({
          required: ['invoices:read', 'invoices:delete'],
          missing: ['invoices:delete'],
        })
      );
    });

    it('should bypass for super_admin', async () => {
      mockReq.user = {
        userId,
        tenantId,
        email: 'super@test.com',
        roles: ['super_admin'],
      } as any;

      const middleware = requireAllPerms('invoices:delete', 'users:delete', 'system:manage');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });
  });

  describe('requireAccess (hybrid mode)', () => {
    it('should allow access via permission', async () => {
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requireAccess({ roles: ['admin'], permission: 'invoices:create' });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledWith(tenantId, userId, 'invoices', 'create');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should fallback to role check when permission denied', async () => {
      mockCheckCachedPermission.mockResolvedValue(null);
      mockHasPermission.mockResolvedValue(false);
      mockReq.user!.roles = ['admin'];

      const middleware = requireAccess({ roles: ['admin'], permission: 'invoices:create' });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Access granted via role (hybrid mode)',
        expect.objectContaining({
          matchedRoles: ['admin'],
        })
      );
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should deny access when neither permission nor role matches', async () => {
      mockCheckCachedPermission.mockResolvedValue(false);

      const middleware = requireAccess({ roles: ['admin'], permission: 'invoices:delete' });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ForbiddenError);
    });

    it('should work with only roles specified', async () => {
      mockReq.user!.roles = ['admin'];

      const middleware = requireAccess({ roles: ['admin', 'manager'] });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should work with only permission specified', async () => {
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requireAccess({ permission: 'invoices:read' });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should bypass for super_admin', async () => {
      mockReq.user = {
        userId,
        tenantId,
        email: 'super@test.com',
        roles: ['super_admin'],
      } as any;

      const middleware = requireAccess({ roles: ['admin'], permission: 'system:manage' });
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });
  });

  describe('requirePermOrOwner', () => {
    it('should allow access when user is the resource owner', async () => {
      mockReq.params = { userId };

      const middleware = requirePermOrOwner('users:update');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Access granted (owner)',
        expect.objectContaining({ userId })
      );
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should check permission when user is not the owner', async () => {
      mockReq.params = { userId: 'other-user-uuid' };
      mockCheckCachedPermission.mockResolvedValue(true);

      const middleware = requirePermOrOwner('users:update');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).toHaveBeenCalledWith(tenantId, userId, 'users', 'update');
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should deny access when not owner and lacks permission', async () => {
      mockReq.params = { userId: 'other-user-uuid' };
      mockCheckCachedPermission.mockResolvedValue(false);

      const middleware = requirePermOrOwner('users:update');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalled();
      const error = (mockNext as jest.Mock).mock.calls[0][0];
      expect(error).toBeInstanceOf(ForbiddenError);
    });

    it('should use custom owner field from params', async () => {
      mockReq.params = { createdBy: userId };

      const middleware = requirePermOrOwner('invoices:update', 'createdBy');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Access granted (owner)',
        expect.any(Object)
      );
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should check owner field in body if not in params', async () => {
      mockReq.params = {};
      mockReq.body = { userId };

      const middleware = requirePermOrOwner('users:update');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Access granted (owner)',
        expect.any(Object)
      );
      expect(mockNext).toHaveBeenCalledWith();
    });

    it('should bypass for super_admin', async () => {
      mockReq.user = {
        userId,
        tenantId,
        email: 'super@test.com',
        roles: ['super_admin'],
      } as any;
      mockReq.params = { userId: 'other-user-uuid' };

      const middleware = requirePermOrOwner('users:delete');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockCheckCachedPermission).not.toHaveBeenCalled();
      expect(mockNext).toHaveBeenCalledWith();
    });
  });

  describe('error handling', () => {
    it('should pass errors to next() middleware', async () => {
      const testError = new Error('Database connection failed');
      mockCheckCachedPermission.mockRejectedValue(testError);

      const middleware = requirePerm('invoices:read');
      await middleware(mockReq as AuthenticatedRequest, mockRes as Response, mockNext);

      expect(mockNext).toHaveBeenCalledWith(testError);
    });
  });
});
