/**
 * Unit tests for src/middleware/auth.ts
 */

import crypto from 'crypto';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { signToken } from '../../../src/utils/jwt';
import { ITokenPayload } from '../../../src/types/index';
import { AuthenticationError } from '../../../src/utils/errors';

// Generate test RSA keys
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

// Mock environment and Redis before importing auth middleware
jest.mock('../../../src/cache/redis', () => ({
  getRedisClient: jest.fn().mockResolvedValue({
    get: jest.fn().mockResolvedValue(null), // Not revoked by default
  }),
}));

// We need to set env vars before importing the middleware
process.env['JWT_PUBLIC_KEY'] = publicKey;

// Import after setting env
import { authMiddleware } from '../../../src/middleware/auth';
import { getRedisClient } from '../../../src/cache/redis';

const mockGetRedisClient = getRedisClient as jest.Mock;

function makeTestToken(overrides: Partial<ITokenPayload> = {}): string {
  const payload: Omit<ITokenPayload, 'iat' | 'exp'> = {
    sub: uuidv4(),
    client_id: uuidv4(),
    scope: 'agents:read',
    jti: uuidv4(),
    ...overrides,
  };
  return signToken(payload, privateKey);
}

function makeReq(authHeader?: string): Partial<Request> {
  return {
    headers: authHeader ? { authorization: authHeader } : {},
    ip: '127.0.0.1',
  };
}

describe('authMiddleware', () => {
  let next: jest.MockedFunction<NextFunction>;

  beforeEach(() => {
    next = jest.fn();
    mockGetRedisClient.mockResolvedValue({
      get: jest.fn().mockResolvedValue(null),
    });
  });

  it('should call next() and set req.user for a valid token', async () => {
    const token = makeTestToken();
    const req = makeReq(`Bearer ${token}`) as Request;
    const res = {} as Response;

    await authMiddleware(req, res, next);

    expect(next).toHaveBeenCalledWith();
    expect(req.user).toBeDefined();
    expect(req.user?.sub).toBeTruthy();
  });

  it('should call next(AuthenticationError) when Authorization header is missing', async () => {
    const req = makeReq() as Request;
    const res = {} as Response;

    await authMiddleware(req, res, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
  });

  it('should call next(AuthenticationError) when header does not start with Bearer', async () => {
    const req = makeReq('Basic dXNlcjpwYXNz') as Request;
    const res = {} as Response;

    await authMiddleware(req, res, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
  });

  it('should call next(AuthenticationError) for an invalid JWT', async () => {
    const req = makeReq('Bearer invalid.jwt.token') as Request;
    const res = {} as Response;

    await authMiddleware(req, res, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
  });

  it('should call next(AuthenticationError) for a revoked token', async () => {
    mockGetRedisClient.mockResolvedValue({
      get: jest.fn().mockResolvedValue('1'), // Token is revoked
    });

    const token = makeTestToken();
    const req = makeReq(`Bearer ${token}`) as Request;
    const res = {} as Response;

    await authMiddleware(req, res, next);

    expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
  });
});
