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

// Pin a deterministic CSRF secret. Mocks must be declared before importing the SUT.
vi.mock('./config.js', () => ({
  config: { CSRF_SECRET: 'csrf-test-secret' },
  productId: 'devops-internal',
}));

const { generateCsrfToken, validateCsrfToken, getSessionId } = await import('./csrf.js');

describe('generateCsrfToken / validateCsrfToken', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('produces a base64-encoded token that round-trips through validate', () => {
    const token = generateCsrfToken('session-1');
    expect(token).toMatch(/^[A-Za-z0-9+/=]+$/);
    expect(validateCsrfToken(token, 'session-1')).toBe(true);
  });

  it('rejects when the session id does not match', () => {
    const token = generateCsrfToken('session-1');
    expect(validateCsrfToken(token, 'session-2')).toBe(false);
  });

  it('rejects when the token has been tampered with (signature mismatch)', () => {
    const token = generateCsrfToken('session-1');
    const decoded = Buffer.from(token, 'base64').toString('utf-8');
    const [sid, ts] = decoded.split(':');
    // Replace the trailing hash with garbage of the same length.
    const tampered = Buffer.from(`${sid}:${ts}:${'0'.repeat(64)}`).toString('base64');
    expect(validateCsrfToken(tampered, 'session-1')).toBe(false);
  });

  it('rejects when the token is older than the 1h window', () => {
    const token = generateCsrfToken('session-1');
    // Advance just past the 3_600_000ms cutoff.
    vi.setSystemTime(new Date(Date.now() + 3_600_001));
    expect(validateCsrfToken(token, 'session-1')).toBe(false);
  });

  it('accepts when the token is just inside the 1h window', () => {
    const token = generateCsrfToken('session-1');
    vi.setSystemTime(new Date(Date.now() + 3_599_000));
    expect(validateCsrfToken(token, 'session-1')).toBe(true);
  });

  it('rejects garbage input without throwing', () => {
    expect(validateCsrfToken('not-base64!!!', 'session-1')).toBe(false);
    expect(validateCsrfToken('', 'session-1')).toBe(false);
  });

  it('produces different tokens for different sessions at the same instant', () => {
    const t1 = generateCsrfToken('session-a');
    const t2 = generateCsrfToken('session-b');
    expect(t1).not.toBe(t2);
    expect(validateCsrfToken(t1, 'session-b')).toBe(false);
    expect(validateCsrfToken(t2, 'session-a')).toBe(false);
  });
});

describe('getSessionId', () => {
  it('returns authUserId when present on the request', () => {
    expect(getSessionId({ authUserId: 'user-42' })).toBe('user-42');
  });

  it('returns null when authUserId is absent', () => {
    expect(getSessionId({})).toBeNull();
    expect(getSessionId({ headers: {} })).toBeNull();
  });
});
