/**
 * Unit tests for src/controllers/AgentController.ts
 * Services are mocked; handlers are invoked with mock req/res/next.
 */

import { Request, Response, NextFunction } from 'express';
import { AgentController } from '../../../src/controllers/AgentController';
import { AgentService } from '../../../src/services/AgentService';
import { IAgent, ITokenPayload } from '../../../src/types/index';
import { ValidationError, AuthorizationError, AgentNotFoundError } from '../../../src/utils/errors';

jest.mock('../../../src/services/AgentService');

const MockAgentService = AgentService as jest.MockedClass<typeof AgentService>;

// ─── helpers ─────────────────────────────────────────────────────────────────

const MOCK_USER: ITokenPayload = {
  sub: 'agent-id-001',
  client_id: 'agent-id-001',
  scope: 'agents:read agents:write',
  jti: 'jti-001',
  iat: 1000,
  exp: 9999999999,
};

const MOCK_AGENT: IAgent = {
  agentId: 'agent-id-001',
  organizationId: 'org_system',
  email: 'agent@sentryagent.ai',
  agentType: 'screener',
  version: '1.0.0',
  capabilities: ['resume:read'],
  owner: 'team-a',
  deploymentEnv: 'production',
  status: 'active',
  isPublic: false,
  createdAt: new Date('2026-03-28T09:00:00Z'),
  updatedAt: new Date('2026-03-28T09:00:00Z'),
};

function buildMocks(): {
  req: Partial<Request>;
  res: Partial<Response>;
  next: NextFunction;
} {
  const res: Partial<Response> = {
    status: jest.fn().mockReturnThis(),
    json: jest.fn().mockReturnThis(),
    send: jest.fn().mockReturnThis(),
  };
  return {
    req: {
      user: MOCK_USER,
      body: {},
      params: {},
      query: {},
      headers: {},
      ip: '127.0.0.1',
    },
    res,
    next: jest.fn() as NextFunction,
  };
}

// ─── suite ───────────────────────────────────────────────────────────────────

describe('AgentController', () => {
  let agentService: jest.Mocked<AgentService>;
  let controller: AgentController;

  beforeEach(() => {
    jest.clearAllMocks();
    agentService = new MockAgentService({} as never, {} as never, {} as never) as jest.Mocked<AgentService>;
    controller = new AgentController(agentService);
  });

  // ── registerAgent ────────────────────────────────────────────────────────────

  describe('registerAgent()', () => {
    it('should return 201 with the created agent on success', async () => {
      const { req, res, next } = buildMocks();
      req.body = {
        email: 'agent@sentryagent.ai',
        agentType: 'screener',
        version: '1.0.0',
        capabilities: ['resume:read'],
        owner: 'team-a',
        deploymentEnv: 'production',
      };
      agentService.registerAgent.mockResolvedValue(MOCK_AGENT);

      await controller.registerAgent(req as Request, res as Response, next);

      expect(agentService.registerAgent).toHaveBeenCalledTimes(1);
      expect(res.status).toHaveBeenCalledWith(201);
      expect(res.json).toHaveBeenCalledWith(MOCK_AGENT);
      expect(next).not.toHaveBeenCalled();
    });

    it('should call next(ValidationError) when body is invalid', async () => {
      const { req, res, next } = buildMocks();
      req.body = { agentType: 'screener' }; // missing required fields

      await controller.registerAgent(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(expect.any(ValidationError));
      expect(agentService.registerAgent).not.toHaveBeenCalled();
    });

    it('should call next(AuthorizationError) when req.user is missing', async () => {
      const { req, res, next } = buildMocks();
      req.user = undefined;

      await controller.registerAgent(req as Request, res as Response, next);

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

    it('should forward service errors to next', async () => {
      const { req, res, next } = buildMocks();
      req.body = {
        email: 'agent@sentryagent.ai',
        agentType: 'screener',
        version: '1.0.0',
        capabilities: ['resume:read'],
        owner: 'team-a',
        deploymentEnv: 'production',
      };
      const serviceError = new Error('DB error');
      agentService.registerAgent.mockRejectedValue(serviceError);

      await controller.registerAgent(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(serviceError);
    });
  });

  // ── listAgents ───────────────────────────────────────────────────────────────

  describe('listAgents()', () => {
    it('should return 200 with paginated agents', async () => {
      const { req, res, next } = buildMocks();
      req.query = { page: '1', limit: '20' };
      const paginatedResponse = { data: [MOCK_AGENT], total: 1, page: 1, limit: 20 };
      agentService.listAgents.mockResolvedValue(paginatedResponse);

      await controller.listAgents(req as Request, res as Response, next);

      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json).toHaveBeenCalledWith(paginatedResponse);
    });

    it('should call next(AuthorizationError) when req.user is missing', async () => {
      const { req, res, next } = buildMocks();
      req.user = undefined;

      await controller.listAgents(req as Request, res as Response, next);

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

    it('should call next(ValidationError) when query params are invalid', async () => {
      const { req, res, next } = buildMocks();
      req.query = { page: 'not-a-number' };

      await controller.listAgents(req as Request, res as Response, next);

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

    it('should forward service errors to next', async () => {
      const { req, res, next } = buildMocks();
      req.query = {};
      const serviceError = new Error('Service error');
      agentService.listAgents.mockRejectedValue(serviceError);

      await controller.listAgents(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(serviceError);
    });
  });

  // ── getAgentById ─────────────────────────────────────────────────────────────

  describe('getAgentById()', () => {
    it('should return 200 with the agent', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      agentService.getAgentById.mockResolvedValue(MOCK_AGENT);

      await controller.getAgentById(req as Request, res as Response, next);

      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json).toHaveBeenCalledWith(MOCK_AGENT);
    });

    it('should call next(AuthorizationError) when req.user is missing', async () => {
      const { req, res, next } = buildMocks();
      req.user = undefined;
      req.params = { agentId: 'any' };

      await controller.getAgentById(req as Request, res as Response, next);

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

    it('should forward AgentNotFoundError to next', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: 'nonexistent' };
      const notFound = new AgentNotFoundError('nonexistent');
      agentService.getAgentById.mockRejectedValue(notFound);

      await controller.getAgentById(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(notFound);
    });
  });

  // ── updateAgent ──────────────────────────────────────────────────────────────

  describe('updateAgent()', () => {
    it('should return 200 with the updated agent', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      req.body = { version: '2.0.0' };
      const updated = { ...MOCK_AGENT, version: '2.0.0' };
      agentService.updateAgent.mockResolvedValue(updated);

      await controller.updateAgent(req as Request, res as Response, next);

      expect(res.status).toHaveBeenCalledWith(200);
      expect(res.json).toHaveBeenCalledWith(updated);
    });

    it('should call next(AuthorizationError) when req.user is missing', async () => {
      const { req, res, next } = buildMocks();
      req.user = undefined;
      req.params = { agentId: 'any' };
      req.body = { version: '2.0.0' };

      await controller.updateAgent(req as Request, res as Response, next);

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

    it('should call next(ValidationError) when body is invalid', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      req.body = {}; // empty body — updateAgentSchema requires at least 1 field

      await controller.updateAgent(req as Request, res as Response, next);

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

    it('should forward service errors to next', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      req.body = { version: '2.0.0' };
      const serviceError = new AgentNotFoundError(MOCK_AGENT.agentId);
      agentService.updateAgent.mockRejectedValue(serviceError);

      await controller.updateAgent(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(serviceError);
    });
  });

  // ── decommissionAgent ────────────────────────────────────────────────────────

  describe('decommissionAgent()', () => {
    it('should return 204 on success', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      agentService.decommissionAgent.mockResolvedValue();

      await controller.decommissionAgent(req as Request, res as Response, next);

      expect(res.status).toHaveBeenCalledWith(204);
      expect(res.send).toHaveBeenCalled();
      expect(next).not.toHaveBeenCalled();
    });

    it('should call next(AuthorizationError) when req.user is missing', async () => {
      const { req, res, next } = buildMocks();
      req.user = undefined;
      req.params = { agentId: 'any' };

      await controller.decommissionAgent(req as Request, res as Response, next);

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

    it('should forward service errors to next', async () => {
      const { req, res, next } = buildMocks();
      req.params = { agentId: MOCK_AGENT.agentId };
      const serviceError = new AgentNotFoundError(MOCK_AGENT.agentId);
      agentService.decommissionAgent.mockRejectedValue(serviceError);

      await controller.decommissionAgent(req as Request, res as Response, next);

      expect(next).toHaveBeenCalledWith(serviceError);
    });
  });
});
