import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { GOOGLE_OAUTH_SCOPES } from '../src/services/google-sheets';
import {
  buildGoogleAuthorizationUrl,
  googleConnectionMetadata,
  normaliseGoogleValues,
  oauthFlowMatches,
  oauthStateHash,
  parseBoundedGoogleRange,
} from '../src/services/google-sheets';

describe('Google OAuth proof and redaction', () => {
  it('binds state and PKCE without putting the verifier in the authorization URL', () => {
    const state = 'state-secret';
    const verifier = 'verifier-secret';
    const url = new URL(buildGoogleAuthorizationUrl({
      clientId: 'client-id',
      redirectUri: 'https://pig.example/oauth/google/callback',
      state,
      challenge: 'challenge',
    }));
    assert.equal(url.searchParams.get('state'), state);
    assert.equal(url.searchParams.get('code_challenge'), 'challenge');
    assert.equal(url.searchParams.get('code_challenge_method'), 'S256');
    assert.equal(url.searchParams.get('scope'), GOOGLE_OAUTH_SCOPES.join(' '));
    assert.equal(url.searchParams.get('access_type'), 'offline');
    assert.equal(url.toString().includes(verifier), false);
  });

  it('accepts only the matching, unexpired, just-consumed state', () => {
    const now = new Date('2026-08-13T12:00:00.000Z');
    const flow = {
      stateHash: oauthStateHash('expected'),
      browserBindingHash: oauthStateHash('browser'),
      expiresAt: new Date('2026-08-13T12:01:00.000Z'),
      consumedAt: now,
    };
    assert.equal(oauthFlowMatches(flow, 'expected', 'browser', now), true);
    assert.equal(oauthFlowMatches(flow, 'attacker', 'browser', now), false);
    assert.equal(oauthFlowMatches(flow, 'expected', 'other-browser', now), false);
    assert.equal(oauthFlowMatches({ ...flow, consumedAt: null }, 'expected', 'browser', now), false);
    assert.equal(oauthFlowMatches({ ...flow, expiresAt: now }, 'expected', 'browser', now), false);
  });

  it('never serializes encrypted tokens in connection metadata', () => {
    const metadata = googleConnectionMetadata(true, {
      userId: '00000000-0000-4000-8000-000000000001',
      refreshTokenEncrypted: 'v1.refresh.secret',
      accessTokenEncrypted: 'v1.access.secret',
      accessTokenExpiresAt: new Date(),
      scopes: [...GOOGLE_OAUTH_SCOPES],
      connectedAt: new Date('2026-08-13T12:00:00.000Z'),
      updatedAt: new Date(),
    });
    const serialized = JSON.stringify(metadata);
    assert.equal(serialized.includes('v1.refresh.secret'), false);
    assert.equal(serialized.includes('v1.access.secret'), false);
    assert.deepEqual(Object.keys(metadata), ['configured', 'connected', 'connectedAt', 'scopes']);
  });
});

describe('Google Sheets range and value boundaries', () => {
  it('requires an explicit rectangular range within A14 limits and the selected grid', () => {
    assert.deepEqual(parseBoundedGoogleRange('a1:CV2001', { rowCount: 3_000, columnCount: 100 }), {
      a1: 'A1:CV2001',
      rows: 2_001,
      columns: 100,
    });
    assert.throws(() => parseBoundedGoogleRange('A:Z'));
    assert.throws(() => parseBoundedGoogleRange('A1:C2002'));
    assert.throws(() => parseBoundedGoogleRange('A1:C10', { rowCount: 9, columnCount: 3 }));
  });

  it('normalizes formatted values into A14 rows while keeping text inert', () => {
    const table = normaliseGoogleValues([
      ['external_id', 'name', 'active', 'score'],
      [7, '=IMPORTDATA("https://example.test")', true, 2.5],
    ], 4);
    assert.deepEqual(table.headers, ['external_id', 'name', 'active', 'score']);
    assert.deepEqual(table.rows, [['7', '=IMPORTDATA("https://example.test")', 'true', '2.5']]);
    assert.match(table.warnings.join(' '), /formula source was not imported/i);
  });
});
