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

import { Pool } from 'pg';
import { CredentialRepository } from '../../../src/repositories/CredentialRepository';
import { ICredential, ICredentialRow, ICredentialListFilters } from '../../../src/types/index';

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

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

const CREDENTIAL_ROW = {
  credential_id: 'cred-0000-0000-0000-000000000001',
  client_id: 'agent-0000-0000-0000-000000000001',
  secret_hash: '$2b$10$hashedSecret',
  status: 'active',
  created_at: new Date('2026-03-28T09:00:00Z'),
  expires_at: null,
  revoked_at: null,
};

const EXPECTED_CREDENTIAL: ICredential = {
  credentialId: CREDENTIAL_ROW.credential_id,
  clientId: CREDENTIAL_ROW.client_id,
  status: 'active',
  createdAt: CREDENTIAL_ROW.created_at,
  expiresAt: null,
  revokedAt: null,
};

const EXPECTED_CREDENTIAL_ROW: ICredentialRow = {
  ...EXPECTED_CREDENTIAL,
  secretHash: CREDENTIAL_ROW.secret_hash,
  vaultPath: null,
};

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

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

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

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

  describe('create()', () => {
    it('should insert a credential row and return ICredential without secret hash', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [CREDENTIAL_ROW], rowCount: 1 });

      const result = await repo.create(
        CREDENTIAL_ROW.client_id,
        CREDENTIAL_ROW.secret_hash,
        null,
      );

      expect(pool.query).toHaveBeenCalledTimes(1);
      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('INSERT INTO credentials');
      expect(params).toContain(CREDENTIAL_ROW.client_id);
      expect(params).toContain(CREDENTIAL_ROW.secret_hash);

      // Secret hash must NOT be on the returned ICredential
      expect(result).toMatchObject(EXPECTED_CREDENTIAL);
      expect((result as ICredentialRow).secretHash).toBeUndefined();
    });

    it('should pass expiresAt when provided', async () => {
      const expiresAt = new Date('2027-01-01T00:00:00Z');
      const rowWithExpiry = { ...CREDENTIAL_ROW, expires_at: expiresAt };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [rowWithExpiry], rowCount: 1 });

      const result = await repo.create(CREDENTIAL_ROW.client_id, CREDENTIAL_ROW.secret_hash, expiresAt);

      const [, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(params).toContain(expiresAt);
      expect(result.expiresAt).toEqual(expiresAt);
    });
  });

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

  describe('findById()', () => {
    it('should return ICredentialRow (with secretHash) when found', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [CREDENTIAL_ROW], rowCount: 1 });

      const result = await repo.findById(CREDENTIAL_ROW.credential_id);

      expect(pool.query).toHaveBeenCalledWith(
        expect.stringContaining('credential_id'),
        [CREDENTIAL_ROW.credential_id],
      );
      expect(result).toMatchObject(EXPECTED_CREDENTIAL_ROW);
      expect(result?.secretHash).toBe(CREDENTIAL_ROW.secret_hash);
    });

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

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

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

  // ── findByAgentId ─────────────────────────────────────────────────────────────

  describe('findByAgentId()', () => {
    it('should return paginated credentials for an agent', async () => {
      (pool.query as jest.Mock)
        .mockResolvedValueOnce({ rows: [{ count: '1' }], rowCount: 1 })
        .mockResolvedValueOnce({ rows: [CREDENTIAL_ROW], rowCount: 1 });

      const filters: ICredentialListFilters = { page: 1, limit: 20 };
      const result = await repo.findByAgentId(CREDENTIAL_ROW.client_id, filters);

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

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

      const filters: ICredentialListFilters = { page: 1, limit: 20, status: 'revoked' };
      const result = await repo.findByAgentId(CREDENTIAL_ROW.client_id, filters);

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

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

      const result = await repo.findByAgentId('agent-no-creds', { page: 1, limit: 20 });

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

    it('should not include secretHash in returned credentials', async () => {
      (pool.query as jest.Mock)
        .mockResolvedValueOnce({ rows: [{ count: '1' }], rowCount: 1 })
        .mockResolvedValueOnce({ rows: [CREDENTIAL_ROW], rowCount: 1 });

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

      expect((result.credentials[0] as ICredentialRow).secretHash).toBeUndefined();
    });
  });

  // ── updateHash ────────────────────────────────────────────────────────────────

  describe('updateHash()', () => {
    it('should update the secret hash and return ICredential', async () => {
      const newHash = '$2b$10$newHash';
      const updatedRow = { ...CREDENTIAL_ROW, secret_hash: newHash };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [updatedRow], rowCount: 1 });

      const result = await repo.updateHash(CREDENTIAL_ROW.credential_id, newHash, null);

      expect(pool.query).toHaveBeenCalledTimes(1);
      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('secret_hash');
      expect(params).toContain(newHash);
      expect(params).toContain(CREDENTIAL_ROW.credential_id);
      expect(result).toMatchObject(EXPECTED_CREDENTIAL);
    });

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

      const result = await repo.updateHash('nonexistent', '$2b$10$hash', null);

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

    it('should pass new expiresAt when provided', async () => {
      const newExpiry = new Date('2028-01-01T00:00:00Z');
      const updatedRow = { ...CREDENTIAL_ROW, expires_at: newExpiry };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [updatedRow], rowCount: 1 });

      const result = await repo.updateHash(CREDENTIAL_ROW.credential_id, '$2b$10$hash', newExpiry);

      const [, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(params).toContain(newExpiry);
      expect(result?.expiresAt).toEqual(newExpiry);
    });
  });

  // ── revoke ────────────────────────────────────────────────────────────────────

  describe('revoke()', () => {
    it('should set status to revoked and return ICredential', async () => {
      const revokedAt = new Date('2026-03-28T10:00:00Z');
      const revokedRow = { ...CREDENTIAL_ROW, status: 'revoked', revoked_at: revokedAt };
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [revokedRow], rowCount: 1 });

      const result = await repo.revoke(CREDENTIAL_ROW.credential_id);

      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('revoked');
      expect(params).toContain(CREDENTIAL_ROW.credential_id);
      expect(result?.status).toBe('revoked');
      expect(result?.revokedAt).toEqual(revokedAt);
    });

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

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

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

  // ── revokeAllForAgent ─────────────────────────────────────────────────────────

  describe('revokeAllForAgent()', () => {
    it('should return the count of revoked credentials', async () => {
      (pool.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 3 });

      const count = await repo.revokeAllForAgent(CREDENTIAL_ROW.client_id);

      const [sql, params] = (pool.query as jest.Mock).mock.calls[0] as [string, unknown[]];
      expect(sql).toContain('revoked');
      expect(params).toContain(CREDENTIAL_ROW.client_id);
      expect(count).toBe(3);
    });

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

      const count = await repo.revokeAllForAgent('agent-no-creds');

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