import { createHmac, randomBytes } from 'node:crypto';
import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { HUBSPOT_REQUIRED_SCOPES } from '../../../packages/core/src/hubspot';
import { HubSpotCrmClient, HUBSPOT_READ_PROPERTIES } from '../src/integrations/hubspot/client';
import {
  buildHubSpotAuthorizationUrl,
  HubSpotOAuthClient,
  HubSpotTokenManager,
  HubSpotTokenVault,
  type LockedHubSpotCredential,
} from '../src/integrations/hubspot/oauth';
import {
  normalizeHubSpotSignatureUri,
  verifyHubSpotV3Signature,
} from '../src/integrations/hubspot/signature';
import { HubSpotSyncService } from '../src/integrations/hubspot/sync';
import { createHubSpotWebhookRoutes } from '../src/routes/hubspot-webhook';

const tokenPayload = {
  access_token: 'access-token',
  refresh_token: 'refresh-token',
  expires_in: 1_800,
  hub_id: 12345,
  scopes: [...HUBSPOT_REQUIRED_SCOPES],
};

describe('HubSpot OAuth decisions', () => {
  it('requests only the three read scopes and binds state plus redirect URI', () => {
    const value = buildHubSpotAuthorizationUrl({
      clientId: 'client-id',
      redirectUri: 'https://pig.example/api/integrations/hubspot/oauth/callback',
    }, 'state-value');
    const url = new URL(value);
    assert.equal(url.origin + url.pathname, 'https://app.hubspot.com/oauth/authorize');
    assert.equal(url.searchParams.get('state'), 'state-value');
    assert.equal(url.searchParams.get('redirect_uri'), 'https://pig.example/api/integrations/hubspot/oauth/callback');
    assert.deepEqual(url.searchParams.get('scope')?.split(' '), [...HUBSPOT_REQUIRED_SCOPES]);
    assert.equal(HUBSPOT_REQUIRED_SCOPES.some((scope) => scope.endsWith('.write')), false);
  });

  it('uses the official form-encoded v3 token exchange', async () => {
    let request: Request | undefined;
    const oauth = new HubSpotOAuthClient({
      clientId: 'client-id',
      clientSecret: 'client-secret',
      redirectUri: 'https://pig.example/callback',
    }, async (input, init) => {
      request = new Request(input, init);
      return Response.json(tokenPayload);
    });
    const tokens = await oauth.exchangeAuthorizationCode('authorization-code');
    assert.equal(request?.url, 'https://api.hubapi.com/oauth/v3/token');
    assert.equal(request?.method, 'POST');
    assert.equal(request?.headers.get('content-type'), 'application/x-www-form-urlencoded');
    const form = new URLSearchParams(await request?.text());
    assert.equal(form.get('grant_type'), 'authorization_code');
    assert.equal(form.get('code'), 'authorization-code');
    assert.equal(tokens.portalId, '12345');
  });

  it('purpose-binds token envelopes to connection and token kind', () => {
    const vault = new HubSpotTokenVault(randomBytes(32).toString('base64'));
    const envelope = vault.encrypt('connection-a', 'access', 'secret-token');
    assert.equal(vault.decrypt('connection-a', 'access', envelope), 'secret-token');
    assert.throws(() => vault.decrypt('connection-a', 'refresh', envelope));
    assert.throws(() => vault.decrypt('connection-b', 'access', envelope));
  });

  it('refreshes an expired token while the connection lock is held', async () => {
    const vault = new HubSpotTokenVault(randomBytes(32).toString('base64'));
    const events: string[] = [];
    const credential: LockedHubSpotCredential = {
      id: 'connection-a',
      status: 'active',
      encryptedAccessToken: vault.encrypt('connection-a', 'access', 'expired'),
      encryptedRefreshToken: vault.encrypt('connection-a', 'refresh', 'stored-refresh'),
      accessTokenExpiresAt: new Date('2026-01-01T00:00:00Z'),
      updateTokens: async (input) => {
        events.push('update');
        assert.equal(vault.decrypt('connection-a', 'access', input.encryptedAccessToken), 'new-access');
      },
    };
    const manager = new HubSpotTokenManager({
      withConnectionLock: async (_id, operation) => {
        events.push('lock');
        const result = await operation(credential);
        events.push('unlock');
        return result;
      },
    }, {
      refreshAccessToken: async (token) => {
        events.push('refresh');
        assert.equal(token, 'stored-refresh');
        return { ...tokenPayload, accessToken: 'new-access', refreshToken: 'new-refresh', expiresInSeconds: 1_800, portalId: '12345' };
      },
    }, vault, () => new Date('2026-01-01T01:00:00Z'));
    assert.equal(await manager.getAccessToken('connection-a'), 'new-access');
    assert.deepEqual(events, ['lock', 'refresh', 'update', 'unlock']);
  });
});

describe('HubSpot v3 request verification', () => {
  it('uses the exact raw body and only HubSpot-approved query decoding', () => {
    const clientSecret = 'client-secret';
    const method = 'POST';
    const publicUri = 'https://pig.example/api/webhooks/hubspot?next=%2Fcrm%3Fid%3D1';
    const normalized = 'https://pig.example/api/webhooks/hubspot?next=/crm?id%3D1';
    const rawBody = '[{"eventId":1}]';
    const timestamp = '1786453200000';
    const signature = createHmac('sha256', clientSecret)
      .update(`${method}${normalized}${rawBody}${timestamp}`)
      .digest('base64');
    assert.equal(normalizeHubSpotSignatureUri(publicUri), normalized);
    assert.deepEqual(verifyHubSpotV3Signature({
      clientSecret,
      method,
      publicUri,
      rawBody,
      signature,
      timestamp,
      now: new Date(Number(timestamp)),
    }), { valid: true });
    assert.equal(verifyHubSpotV3Signature({
      clientSecret,
      method,
      publicUri,
      rawBody: `${rawBody} `,
      signature,
      timestamp,
      now: new Date(Number(timestamp)),
    }).valid, false);
  });

  it('rejects timestamps outside the five-minute window', () => {
    assert.deepEqual(verifyHubSpotV3Signature({
      clientSecret: 'secret',
      method: 'POST',
      publicUri: 'https://pig.example/api/webhooks/hubspot',
      rawBody: '[]',
      signature: 'not-used',
      timestamp: '1000',
      now: new Date(301_001),
    }), { valid: false, reason: 'stale_timestamp' });
  });
});

describe('read-only CRM and resumable sync', () => {
  it('lists explicit official properties and follows the opaque after cursor', async () => {
    let request: Request | undefined;
    const client = new HubSpotCrmClient(async (input, init) => {
      request = new Request(input, init);
      return Response.json({ results: [], paging: { next: { after: 'next-page' } } });
    });
    const page = await client.listObjects('token', 'companies', { after: 'current-page' });
    const url = new URL(request?.url ?? 'https://invalid');
    assert.equal(request?.method, 'GET');
    assert.equal(url.pathname, '/crm/objects/2026-03/companies');
    assert.equal(url.searchParams.get('after'), 'current-page');
    assert.equal(url.searchParams.get('properties'), HUBSPOT_READ_PROPERTIES.companies.join(','));
    assert.equal(request?.headers.get('authorization'), 'Bearer token');
    assert.equal(page.nextAfter, 'next-page');
  });

  it('commits records and the next cursor as one page decision', async () => {
    const commits: unknown[] = [];
    const service = new HubSpotSyncService({
      getCursor: async () => ({ after: '17', phase: 'initial' }),
      commitPage: async (input) => { commits.push(input); },
    }, {
      getAccessToken: async () => 'access-token',
    }, {
      listObjects: async (_token, type, options) => {
        assert.equal(type, 'contacts');
        assert.equal(options?.after, '17');
        return {
          results: [{
            id: '42',
            properties: { email: 'person@example.com' },
            createdAt: '2026-01-01T00:00:00.000Z',
            updatedAt: '2026-01-02T00:00:00.000Z',
            archived: false,
          }],
          nextAfter: '18',
        };
      },
    }, () => new Date('2026-01-03T00:00:00.000Z'));
    const result = await service.syncNextPage('connection-a', 'contacts');
    assert.equal(result.complete, false);
    assert.equal(result.nextAfter, '18');
    assert.equal(commits.length, 1);
    assert.deepEqual(
      Object.assign({}, commits[0], { records: undefined, completedAt: undefined }),
      {
        connectionId: 'connection-a',
        objectType: 'contacts',
        phase: 'initial',
        expectedAfter: '17',
        nextAfter: '18',
        records: undefined,
        completedAt: undefined,
      },
    );
  });
});

describe('HubSpot webhook boundary', () => {
  it('verifies, bounds and durably hands off a batch before returning 204', async () => {
    const body = JSON.stringify([{
      eventId: 1,
      subscriptionId: 2,
      portalId: 3,
      appId: 4,
      occurredAt: 1_786_453_200_000,
      objectId: 5,
      subscriptionType: 'contact.creation',
      attemptNumber: 0,
    }]);
    const timestamp = '1786453200000';
    const publicUri = 'https://pig.example/api/webhooks/hubspot';
    const signature = createHmac('sha256', 'client-secret')
      .update(`POST${publicUri}${body}${timestamp}`)
      .digest('base64');
    let received = 0;
    const routes = createHubSpotWebhookRoutes({
      clientSecret: 'client-secret',
      publicUri,
      appId: '4',
      now: () => new Date(Number(timestamp)),
      store: { enqueueVerifiedBatch: async ({ events }) => { received = events.length; } },
    });
    const response = await routes.request(publicUri, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'x-hubspot-signature-v3': signature,
        'x-hubspot-request-timestamp': timestamp,
      },
      body,
    });
    assert.equal(response.status, 204);
    assert.equal(received, 1);
  });
});
