import { describe, it, expect, vi, beforeEach } from 'vitest';

const { mockRedisGet, mockRedisSet, mockRedisDel, mockRedisKeys } = vi.hoisted(() => ({
  mockRedisGet: vi.fn(),
  mockRedisSet: vi.fn(),
  mockRedisDel: vi.fn(),
  mockRedisKeys: vi.fn(),
}));

vi.mock('../redis.js', () => ({
  default: {
    get: mockRedisGet,
    set: mockRedisSet,
    del: mockRedisDel,
    keys: mockRedisKeys,
  },
}));

const mockTryRefreshToken = vi.hoisted(() => vi.fn());
vi.mock('./token-refresh.js', () => ({ tryRefreshToken: mockTryRefreshToken }));

// Use a real 32-byte key so AES-256-GCM doesn't throw
process.env.CREDENTIAL_ENCRYPTION_KEY = '0'.repeat(64);

import { getCredential, storeCredential } from './credential-store.js';

function encryptCreds(creds: object): string {
  // We can't call encrypt() directly (not exported), so we'll round-trip through storeCredential
  // For tests, we'll use a helper approach: just test behavior using storeCredential to set up state.
  return JSON.stringify(creds); // placeholder — see note below
}

describe('getCredential', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockRedisSet.mockResolvedValue('OK');
  });

  it('returns null when no credential stored', async () => {
    mockRedisGet.mockResolvedValue(null);
    const result = await getCredential('cust1', 'linkedin');
    expect(result).toBeNull();
  });

  it('returns credential when not expired', async () => {
    // Store then retrieve using real encryption
    const creds = { accessToken: 'tok', expiresAt: Date.now() + 3_600_000 };
    await storeCredential('cust1', 'linkedin', creds);
    const stored = mockRedisSet.mock.calls[0][1] as string;
    mockRedisGet.mockResolvedValue(stored);

    const result = await getCredential('cust1', 'linkedin');
    expect(result).toMatchObject({ accessToken: 'tok' });
    expect(mockTryRefreshToken).not.toHaveBeenCalled();
  });

  it('attempts refresh when token is within 60s of expiry', async () => {
    const creds = { accessToken: 'old', refreshToken: 'ref', expiresAt: Date.now() + 30_000 };
    await storeCredential('cust1', 'linkedin', creds);
    const stored = mockRedisSet.mock.calls[0][1] as string;
    mockRedisGet.mockResolvedValue(stored);
    mockTryRefreshToken.mockResolvedValue({ accessToken: 'new', expiresAt: Date.now() + 3_600_000 });

    const result = await getCredential<{ accessToken: string }>('cust1', 'linkedin');
    expect(mockTryRefreshToken).toHaveBeenCalledWith('cust1', 'linkedin', expect.objectContaining({ accessToken: 'old' }));
    expect(result?.accessToken).toBe('new');
  });

  it('returns null when token expired and no refresh token', async () => {
    const creds = { accessToken: 'old', expiresAt: Date.now() - 1000 };
    await storeCredential('cust1', 'linkedin', creds);
    const stored = mockRedisSet.mock.calls[0][1] as string;
    mockRedisGet.mockResolvedValue(stored);

    const result = await getCredential('cust1', 'linkedin');
    expect(result).toBeNull();
    expect(mockTryRefreshToken).not.toHaveBeenCalled();
  });

  it('returns null when refresh fails', async () => {
    const creds = { accessToken: 'old', refreshToken: 'ref', expiresAt: Date.now() - 1000 };
    await storeCredential('cust1', 'linkedin', creds);
    const stored = mockRedisSet.mock.calls[0][1] as string;
    mockRedisGet.mockResolvedValue(stored);
    mockTryRefreshToken.mockResolvedValue(null);

    const result = await getCredential('cust1', 'linkedin');
    expect(result).toBeNull();
  });

  it('returns non-OAuth credentials without expiry check', async () => {
    const creds = { host: 'imap.gmail.com', port: 993, user: 'u', password: 'p' };
    await storeCredential('cust1', 'email', creds);
    const stored = mockRedisSet.mock.calls[0][1] as string;
    mockRedisGet.mockResolvedValue(stored);

    const result = await getCredential('cust1', 'email');
    expect(result).toMatchObject({ host: 'imap.gmail.com' });
    expect(mockTryRefreshToken).not.toHaveBeenCalled();
  });
});
