/**
 * Unit tests for src/services/AgentService.ts
 */

import { AgentService } from '../../../src/services/AgentService';
import { AgentRepository } from '../../../src/repositories/AgentRepository';
import { CredentialRepository } from '../../../src/repositories/CredentialRepository';
import { AuditService } from '../../../src/services/AuditService';
import {
  AgentNotFoundError,
  AgentAlreadyExistsError,
  AgentAlreadyDecommissionedError,
  FreeTierLimitError,
} from '../../../src/utils/errors';
import { IAgent, ICreateAgentRequest } from '../../../src/types/index';

// Mock dependencies
jest.mock('../../../src/repositories/AgentRepository');
jest.mock('../../../src/repositories/CredentialRepository');
jest.mock('../../../src/services/AuditService');

const MockAgentRepository = AgentRepository as jest.MockedClass<typeof AgentRepository>;
const MockCredentialRepository = CredentialRepository as jest.MockedClass<typeof CredentialRepository>;
const MockAuditService = AuditService as jest.MockedClass<typeof AuditService>;

const MOCK_AGENT: IAgent = {
  agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
  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'),
};

const IP = '127.0.0.1';
const UA = 'test-agent/1.0';

describe('AgentService', () => {
  let agentService: AgentService;
  let agentRepo: jest.Mocked<AgentRepository>;
  let credentialRepo: jest.Mocked<CredentialRepository>;
  let auditService: jest.Mocked<AuditService>;

  beforeEach(() => {
    jest.clearAllMocks();
    agentRepo = new MockAgentRepository({} as never) as jest.Mocked<AgentRepository>;
    credentialRepo = new MockCredentialRepository({} as never) as jest.Mocked<CredentialRepository>;
    auditService = new MockAuditService({} as never) as jest.Mocked<AuditService>;
    agentService = new AgentService(agentRepo, credentialRepo, auditService);
  });

  // ────────────────────────────────────────────────────────────────
  // registerAgent
  // ────────────────────────────────────────────────────────────────
  describe('registerAgent()', () => {
    const createData: ICreateAgentRequest = {
      email: 'agent@sentryagent.ai',
      agentType: 'screener',
      version: '1.0.0',
      capabilities: ['resume:read'],
      owner: 'team-a',
      deploymentEnv: 'production',
    };

    it('should create and return a new agent', async () => {
      agentRepo.countActive.mockResolvedValue(0);
      agentRepo.findByEmail.mockResolvedValue(null);
      agentRepo.create.mockResolvedValue(MOCK_AGENT);
      auditService.logEvent.mockResolvedValue({} as never);

      const result = await agentService.registerAgent(createData, IP, UA);
      expect(result).toEqual(MOCK_AGENT);
      expect(agentRepo.create).toHaveBeenCalledWith(createData);
    });

    it('should throw FreeTierLimitError when 100 agents already registered', async () => {
      agentRepo.countActive.mockResolvedValue(100);

      await expect(agentService.registerAgent(createData, IP, UA)).rejects.toThrow(
        FreeTierLimitError,
      );
    });

    it('should throw AgentAlreadyExistsError if email is already registered', async () => {
      agentRepo.countActive.mockResolvedValue(0);
      agentRepo.findByEmail.mockResolvedValue(MOCK_AGENT);

      await expect(agentService.registerAgent(createData, IP, UA)).rejects.toThrow(
        AgentAlreadyExistsError,
      );
    });
  });

  // ────────────────────────────────────────────────────────────────
  // getAgentById
  // ────────────────────────────────────────────────────────────────
  describe('getAgentById()', () => {
    it('should return the agent when found', async () => {
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      const result = await agentService.getAgentById(MOCK_AGENT.agentId);
      expect(result).toEqual(MOCK_AGENT);
    });

    it('should throw AgentNotFoundError when not found', async () => {
      agentRepo.findById.mockResolvedValue(null);
      await expect(agentService.getAgentById('nonexistent-id')).rejects.toThrow(
        AgentNotFoundError,
      );
    });
  });

  // ────────────────────────────────────────────────────────────────
  // listAgents
  // ────────────────────────────────────────────────────────────────
  describe('listAgents()', () => {
    it('should return a paginated list of agents', async () => {
      agentRepo.findAll.mockResolvedValue({ agents: [MOCK_AGENT], total: 1 });
      const result = await agentService.listAgents({ page: 1, limit: 20 });
      expect(result.data).toHaveLength(1);
      expect(result.total).toBe(1);
      expect(result.page).toBe(1);
      expect(result.limit).toBe(20);
    });
  });

  // ────────────────────────────────────────────────────────────────
  // updateAgent
  // ────────────────────────────────────────────────────────────────
  describe('updateAgent()', () => {
    it('should update and return the agent', async () => {
      const updated = { ...MOCK_AGENT, version: '2.0.0' };
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      agentRepo.update.mockResolvedValue(updated);
      auditService.logEvent.mockResolvedValue({} as never);

      const result = await agentService.updateAgent(
        MOCK_AGENT.agentId,
        { version: '2.0.0' },
        IP,
        UA,
      );
      expect(result.version).toBe('2.0.0');
    });

    it('should throw AgentNotFoundError when agent does not exist', async () => {
      agentRepo.findById.mockResolvedValue(null);
      await expect(
        agentService.updateAgent('nonexistent', { version: '2.0.0' }, IP, UA),
      ).rejects.toThrow(AgentNotFoundError);
    });

    it('should throw AgentAlreadyDecommissionedError for decommissioned agent', async () => {
      agentRepo.findById.mockResolvedValue({ ...MOCK_AGENT, status: 'decommissioned' });
      await expect(
        agentService.updateAgent(MOCK_AGENT.agentId, { version: '2.0.0' }, IP, UA),
      ).rejects.toThrow(AgentAlreadyDecommissionedError);
    });

    it('should throw AgentNotFoundError when update() returns null (race condition)', async () => {
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      agentRepo.update.mockResolvedValue(null);
      await expect(
        agentService.updateAgent(MOCK_AGENT.agentId, { version: '2.0.0' }, IP, UA),
      ).rejects.toThrow(AgentNotFoundError);
    });

    it('should log agent.suspended audit action when status changes to suspended', async () => {
      const updated = { ...MOCK_AGENT, status: 'suspended' as const };
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      agentRepo.update.mockResolvedValue(updated);
      auditService.logEvent.mockResolvedValue({} as never);

      await agentService.updateAgent(MOCK_AGENT.agentId, { status: 'suspended' }, IP, UA);

      expect(auditService.logEvent).toHaveBeenCalledWith(
        MOCK_AGENT.agentId,
        'agent.suspended',
        'success',
        IP,
        UA,
        expect.any(Object),
      );
    });

    it('should log agent.reactivated audit action when status changes from suspended to active', async () => {
      const suspended = { ...MOCK_AGENT, status: 'suspended' as const };
      const reactivated = { ...MOCK_AGENT, status: 'active' as const };
      agentRepo.findById.mockResolvedValue(suspended);
      agentRepo.update.mockResolvedValue(reactivated);
      auditService.logEvent.mockResolvedValue({} as never);

      await agentService.updateAgent(suspended.agentId, { status: 'active' }, IP, UA);

      expect(auditService.logEvent).toHaveBeenCalledWith(
        suspended.agentId,
        'agent.reactivated',
        'success',
        IP,
        UA,
        expect.any(Object),
      );
    });

    it('should log agent.decommissioned audit action when status changes to decommissioned via update', async () => {
      const updated = { ...MOCK_AGENT, status: 'decommissioned' as const };
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      agentRepo.update.mockResolvedValue(updated);
      auditService.logEvent.mockResolvedValue({} as never);

      await agentService.updateAgent(MOCK_AGENT.agentId, { status: 'decommissioned' }, IP, UA);

      expect(auditService.logEvent).toHaveBeenCalledWith(
        MOCK_AGENT.agentId,
        'agent.decommissioned',
        'success',
        IP,
        UA,
        expect.any(Object),
      );
    });
  });

  // ────────────────────────────────────────────────────────────────
  // decommissionAgent
  // ────────────────────────────────────────────────────────────────
  describe('decommissionAgent()', () => {
    it('should decommission the agent and revoke credentials', async () => {
      agentRepo.findById.mockResolvedValue(MOCK_AGENT);
      credentialRepo.revokeAllForAgent.mockResolvedValue(2);
      agentRepo.decommission.mockResolvedValue({ ...MOCK_AGENT, status: 'decommissioned' });
      auditService.logEvent.mockResolvedValue({} as never);

      await agentService.decommissionAgent(MOCK_AGENT.agentId, IP, UA);

      expect(credentialRepo.revokeAllForAgent).toHaveBeenCalledWith(MOCK_AGENT.agentId);
      expect(agentRepo.decommission).toHaveBeenCalledWith(MOCK_AGENT.agentId);
    });

    it('should throw AgentNotFoundError if agent does not exist', async () => {
      agentRepo.findById.mockResolvedValue(null);
      await expect(
        agentService.decommissionAgent('nonexistent', IP, UA),
      ).rejects.toThrow(AgentNotFoundError);
    });

    it('should throw AgentAlreadyDecommissionedError if already decommissioned', async () => {
      agentRepo.findById.mockResolvedValue({ ...MOCK_AGENT, status: 'decommissioned' });
      await expect(
        agentService.decommissionAgent(MOCK_AGENT.agentId, IP, UA),
      ).rejects.toThrow(AgentAlreadyDecommissionedError);
    });
  });
});
