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

import { Pool } from 'pg';
import {
  OIDCTrustPolicyService,
  TrustPolicyNotFoundError,
  TrustPolicyViolationError,
} from '../../../src/services/OIDCTrustPolicyService';
import { ValidationError } from '../../../src/utils/errors';

jest.mock('pg');

const MockPool = Pool as jest.MockedClass<typeof Pool>;

function makePool(): jest.Mocked<Pool> {
  const pool = new MockPool() as jest.Mocked<Pool>;
  pool.query = jest.fn();
  return pool;
}

function makePolicyRow(overrides: Record<string, unknown> = {}): Record<string, unknown> {
  return {
    id: 'policy-001',
    provider: 'github',
    repository: 'acme/my-repo',
    branch: null,
    agent_id: 'agent-001',
    created_at: new Date('2026-01-01'),
    updated_at: new Date('2026-01-01'),
    ...overrides,
  };
}

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

  beforeEach(() => {
    jest.clearAllMocks();
    pool = makePool();
    service = new OIDCTrustPolicyService(pool);
  });

  describe('createTrustPolicy()', () => {
    it('should create a trust policy successfully', async () => {
      pool.query = jest.fn()
        .mockResolvedValueOnce({ rows: [{ agent_id: 'agent-001' }], rowCount: 1 })
        .mockResolvedValueOnce({ rows: [makePolicyRow()], rowCount: 1 });

      const result = await service.createTrustPolicy({
        provider: 'github',
        repository: 'acme/my-repo',
        branch: null,
        agentId: 'agent-001',
      });

      expect(result.provider).toBe('github');
      expect(result.repository).toBe('acme/my-repo');
      expect(result.branch).toBeNull();
    });

    it('should throw ValidationError for non-github provider', async () => {
      await expect(
        service.createTrustPolicy({
          provider: 'gitlab' as never,
          repository: 'acme/my-repo',
          branch: null,
          agentId: 'agent-001',
        }),
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError for malformed repository', async () => {
      await expect(
        service.createTrustPolicy({
          provider: 'github',
          repository: 'no-slash-here',
          branch: null,
          agentId: 'agent-001',
        }),
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when agentId is empty', async () => {
      await expect(
        service.createTrustPolicy({
          provider: 'github',
          repository: 'acme/my-repo',
          branch: null,
          agentId: '',
        }),
      ).rejects.toThrow(ValidationError);
    });

    it('should throw ValidationError when agent not found', async () => {
      pool.query = jest.fn().mockResolvedValueOnce({ rows: [], rowCount: 0 });

      await expect(
        service.createTrustPolicy({
          provider: 'github',
          repository: 'acme/my-repo',
          branch: null,
          agentId: 'nonexistent',
        }),
      ).rejects.toThrow(ValidationError);
    });
  });

  describe('listTrustPoliciesForAgent()', () => {
    it('should return mapped policies', async () => {
      pool.query = jest.fn().mockResolvedValue({
        rows: [makePolicyRow(), makePolicyRow({ id: 'policy-002' })],
        rowCount: 2,
      });

      const policies = await service.listTrustPoliciesForAgent('agent-001');

      expect(policies).toHaveLength(2);
      expect(policies[0].id).toBe('policy-001');
    });

    it('should return an empty array when no policies exist', async () => {
      pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });

      const policies = await service.listTrustPoliciesForAgent('agent-001');

      expect(policies).toHaveLength(0);
    });
  });

  describe('deleteTrustPolicy()', () => {
    it('should delete a policy successfully', async () => {
      pool.query = jest.fn().mockResolvedValue({ rowCount: 1 });

      await expect(service.deleteTrustPolicy('policy-001')).resolves.toBeUndefined();
    });

    it('should throw TrustPolicyNotFoundError when policy does not exist', async () => {
      pool.query = jest.fn().mockResolvedValue({ rowCount: 0 });

      await expect(service.deleteTrustPolicy('nonexistent')).rejects.toThrow(TrustPolicyNotFoundError);
    });
  });

  describe('enforceTrustPolicy()', () => {
    it('should pass when a wildcard branch policy exists (branch: null)', async () => {
      pool.query = jest.fn().mockResolvedValue({
        rows: [makePolicyRow({ branch: null })],
        rowCount: 1,
      });

      await expect(
        service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
      ).resolves.toBeUndefined();
    });

    it('should pass when branch matches exactly', async () => {
      pool.query = jest.fn().mockResolvedValue({
        rows: [makePolicyRow({ branch: 'main' })],
        rowCount: 1,
      });

      await expect(
        service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
      ).resolves.toBeUndefined();
    });

    it('should normalize refs/heads/ prefix and match', async () => {
      pool.query = jest.fn().mockResolvedValue({
        rows: [makePolicyRow({ branch: 'main' })],
        rowCount: 1,
      });

      await expect(
        service.enforceTrustPolicy('github', 'acme/my-repo', 'refs/heads/main', 'agent-001'),
      ).resolves.toBeUndefined();
    });

    it('should throw TrustPolicyViolationError when no policies exist', async () => {
      pool.query = jest.fn().mockResolvedValue({ rows: [], rowCount: 0 });

      await expect(
        service.enforceTrustPolicy('github', 'acme/my-repo', 'main', 'agent-001'),
      ).rejects.toThrow(TrustPolicyViolationError);
    });

    it('should throw TrustPolicyViolationError when branch does not match constrained policy', async () => {
      pool.query = jest.fn().mockResolvedValue({
        rows: [makePolicyRow({ branch: 'main' })],
        rowCount: 1,
      });

      await expect(
        service.enforceTrustPolicy('github', 'acme/my-repo', 'feature/evil', 'agent-001'),
      ).rejects.toThrow(TrustPolicyViolationError);
    });
  });
});
