/**
 * Unit tests for src/repositories/AgentRepository.ts
 * Uses a mocked pg.Pool — no real database connection.
 */

import { Pool } from 'pg';
import { AgentRepository } from '../../../src/repositories/AgentRepository';
import { IAgent, ICreateAgentRequest, IUpdateAgentRequest, IAgentListFilters } from '../../../src/types/index';

jest.mock('pg', () => ({
  Pool: jest.fn().mockImplementation(() => ({
    query: jest.fn(),
    connect: jest.fn(),
  })),
}));

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

const AGENT_ROW = {
  agent_id: 'a1b2c3d4-0000-0000-0000-000000000001',
  organization_id: 'org_system',
  email: 'agent@sentryagent.ai',
  agent_type: 'screener',
  version: '1.0.0',
  capabilities: ['resume:read'],
  owner: 'team-a',
  deployment_env: 'production',
  status: 'active',
  is_public: false,
  created_at: new Date('2026-03-28T09:00:00Z'),
  updated_at: new Date('2026-03-28T09:00:00Z'),
};

const EXPECTED_AGENT: IAgent = {
  agentId: AGENT_ROW.agent_id,
  organizationId: AGENT_ROW.organization_id,
  email: AGENT_ROW.email,
  agentType: 'screener',
  version: AGENT_ROW.version,
  capabilities: AGENT_ROW.capabilities,
  owner: AGENT_ROW.owner,
  deploymentEnv: 'production',
  status: 'active',
  isPublic: false,
  createdAt: AGENT_ROW.created_at,
  updatedAt: AGENT_ROW.updated_at,
};

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

describe('AgentRepository', () => {
  let pool: jest.Mocked<Pool>;
  let repo: AgentRepository;

  beforeEach(() => {
    jest.clearAllMocks();
    pool = new Pool() as jest.Mocked<Pool>;
    repo = new AgentRepository(pool);
  });

  // ── create ──────────────────────────────────────────────────────────────────

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

    it('should insert a row and return a mapped IAgent', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [AGENT_ROW], rowCount: 1 });

      const result = await repo.create(createData);

      expect(pool.query).toHaveBeenCalledTimes(1);
      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('INSERT INTO agents');
      expect(params).toContain(createData.email);
      expect(params).toContain(createData.agentType);

      expect(result).toMatchObject({
        email: EXPECTED_AGENT.email,
        agentType: EXPECTED_AGENT.agentType,
        status: 'active',
      });
    });
  });

  // ── findById ─────────────────────────────────────────────────────────────────

  describe('findById()', () => {
    it('should return a mapped IAgent when the row exists', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [AGENT_ROW], rowCount: 1 });

      const result = await repo.findById(AGENT_ROW.agent_id);

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('SELECT'),
        [AGENT_ROW.agent_id],
      );
      expect(result).toMatchObject(EXPECTED_AGENT);
    });

    it('should return null when no rows are returned', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const result = await repo.findById('nonexistent');

      expect(result).toBeNull();
    });
  });

  // ── findByEmail ──────────────────────────────────────────────────────────────

  describe('findByEmail()', () => {
    it('should return a mapped IAgent when the email exists', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [AGENT_ROW], rowCount: 1 });

      const result = await repo.findByEmail(AGENT_ROW.email);

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('email'),
        [AGENT_ROW.email],
      );
      expect(result).toMatchObject(EXPECTED_AGENT);
    });

    it('should return null when no rows are returned', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const result = await repo.findByEmail('notfound@example.com');

      expect(result).toBeNull();
    });
  });

  // ── findAll ──────────────────────────────────────────────────────────────────

  describe('findAll()', () => {
    it('should return paginated agents with total count (no filters)', async () => {
      (pool.query as jest.Mock)
        .mockResolvedValueOnce({ rows: [{ count: '1' }], rowCount: 1 }) // count query
        .mockResolvedValueOnce({ rows: [AGENT_ROW], rowCount: 1 });    // data query

      const filters: IAgentListFilters = { page: 1, limit: 20 };
      const result = await repo.findAll(filters);

      expect(pool.query).toHaveBeenCalledTimes(2);
      expect(result.total).toBe(1);
      expect(result.agents).toHaveLength(1);
      expect(result.agents[0]).toMatchObject(EXPECTED_AGENT);
    });

    it('should apply owner, agentType, and status filters', async () => {
      (pool.query as jest.Mock)
        .mockResolvedValueOnce({ rows: [{ count: '0' }], rowCount: 1 })
        .mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const filters: IAgentListFilters = {
        page: 1,
        limit: 10,
        owner: 'team-a',
        agentType: 'screener',
        status: 'active',
      };
      const result = await repo.findAll(filters);

      const [countSql] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(countSql).toContain('owner');
      expect(countSql).toContain('agent_type');
      expect(countSql).toContain('status');
      expect(result.total).toBe(0);
      expect(result.agents).toHaveLength(0);
    });

    it('should return an empty list when no agents exist', async () => {
      (pool.query as jest.Mock)
        .mockResolvedValueOnce({ rows: [{ count: '0' }], rowCount: 1 })
        .mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const result = await repo.findAll({ page: 1, limit: 20 });

      expect(result.total).toBe(0);
      expect(result.agents).toEqual([]);
    });
  });

  // ── update ───────────────────────────────────────────────────────────────────

  describe('update()', () => {
    it('should update fields and return mapped IAgent', async () => {
      const updatedRow = { ...AGENT_ROW, version: '2.0.0' };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [updatedRow], rowCount: 1 });

      const data: IUpdateAgentRequest = { version: '2.0.0' };
      const result = await repo.update(AGENT_ROW.agent_id, data);

      expect(pool.query).toHaveBeenCalledTimes(1);
      const [sql] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('UPDATE agents');
      expect(result).not.toBeNull();
      expect(result?.version).toBe('2.0.0');
    });

    it('should return null when the agent is not found after update', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const result = await repo.update('nonexistent', { version: '2.0.0' });

      expect(result).toBeNull();
    });

    it('should return null when no fields are provided', async () => {
      const result = await repo.update(AGENT_ROW.agent_id, {});

      expect(pool.query).not.toHaveBeenCalled();
      expect(result).toBeNull();
    });

    it('should update multiple fields at once', async () => {
      const updatedRow = { ...AGENT_ROW, version: '3.0.0', status: 'suspended', owner: 'team-b' };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [updatedRow], rowCount: 1 });

      const data: IUpdateAgentRequest = { version: '3.0.0', status: 'suspended', owner: 'team-b' };
      const result = await repo.update(AGENT_ROW.agent_id, data);

      expect(result?.status).toBe('suspended');
      expect(result?.owner).toBe('team-b');
    });
  });

  // ── decommission ──────────────────────────────────────────────────────────────

  describe('decommission()', () => {
    it('should set status to decommissioned and return the agent', async () => {
      const decomRow = { ...AGENT_ROW, status: 'decommissioned' };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [decomRow], rowCount: 1 });

      const result = await repo.decommission(AGENT_ROW.agent_id);

      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('decommissioned');
      expect(params).toContain(AGENT_ROW.agent_id);
      expect(result?.status).toBe('decommissioned');
    });

    it('should return null when agent is not found', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 });

      const result = await repo.decommission('nonexistent');

      expect(result).toBeNull();
    });
  });

  // ── countActive ───────────────────────────────────────────────────────────────

  describe('countActive()', () => {
    it('should return the count of non-decommissioned agents', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ count: '42' }], rowCount: 1 });

      const count = await repo.countActive();

      const [sql] = (pool.query as jest.Mock).mock.calls[0] as [string];
      expect(sql).toContain('decommissioned');
      expect(count).toBe(42);
    });

    it('should return 0 when there are no active agents', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ count: '0' }], rowCount: 1 });

      const count = await repo.countActive();

      expect(count).toBe(0);
    });
  });
});
