/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * Generates Salesforce Playwright utility files for QA profile projects.
 *
 * Returns a Record<relativePath, content> map. Keys are paths relative to
 * the project root (e.g., "src/utils/salesforce-auth.ts"). The command
 * writes these files using FileWriter, which respects the --force flag.
 */
export function generateSalesforcePlaywrightUtils() {
    return {
        'src/utils/salesforce-auth.ts': salesforceAuthTs(),
        'src/utils/salesforce-api.ts': salesforceApiTs(),
        'test/utils/sf-cli-client.ts': sfCliClientTs(),
        'test/utils/retry-until.ts': retryUntilTs(),
    };
}
function salesforceAuthTs() {
    return `/*
 * Salesforce Playwright Auth Utilities
 * Generated by @jterrats/setup-agents — sf setup-agents local --profile qa
 *
 * Authenticates Playwright tests using the active SF CLI session.
 * No credentials or passwords needed — reuses the token from \`sf org display\`.
 *
 * Usage:
 *   import { navigateWithCLISession, useSalesforceCLISession } from './salesforce-auth';
 *
 *   test.beforeEach(async ({ page, context }) => {
 *     await navigateWithCLISession(page, context);
 *   });
 */
import { execFileSync } from 'node:child_process';
import type { Page, BrowserContext } from '@playwright/test';

export interface SalesforceOrgInfo {
  instanceUrl: string;
  username: string;
  accessToken: string;
  orgId: string;
}

/**
 * Reads the active SF CLI org session without spawning a shell.
 * Respects SF_TARGET_ORG env var; falls back to the default org.
 */
export function getSalesforceOrgInfo(): SalesforceOrgInfo | null {
  try {
    const targetOrg = process.env.SF_TARGET_ORG;
    const args = targetOrg
      ? ['org', 'display', '--json', '--target-org', targetOrg]
      : ['org', 'display', '--json'];

    const raw = execFileSync('sf', args, {
      encoding: 'utf-8',
      stdio: ['pipe', 'pipe', 'pipe'],
      env: { ...process.env, NO_COLOR: '1', SF_USE_PROGRESS_BAR: 'false' },
      maxBuffer: 10 * 1024 * 1024,
    });

    const data = parseCliJson(raw);
    if (data?.status !== 0 || !data?.result?.accessToken) return null;

    return {
      instanceUrl: data.result.instanceUrl as string,
      username: data.result.username as string,
      accessToken: data.result.accessToken as string,
      orgId: data.result.id as string,
    };
  } catch (err: unknown) {
    const msg = err instanceof Error ? err.message : String(err);
    console.error('❌ getSalesforceOrgInfo failed:', msg);
    console.error('   Run: sf org login web');
    return null;
  }
}

/**
 * Injects the SF CLI access token as a session cookie and navigates to
 * the given path (defaults to Lightning home).
 *
 * Call this in test.beforeEach for flows that require a full-page navigation.
 */
export async function navigateWithCLISession(
  page: Page,
  context?: BrowserContext,
  options?: { targetPath?: string }
): Promise<void> {
  const orgInfo = getSalesforceOrgInfo();
  if (!orgInfo) throw new Error('No SF CLI session. Run: sf org login web');

  const { hostname } = new URL(orgInfo.instanceUrl);
  const ctx = context ?? page.context();

  await ctx.addCookies([
    {
      name: 'sid',
      value: orgInfo.accessToken,
      domain: hostname,
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: 'None',
    },
  ]);

  const targetPath = options?.targetPath ?? '/lightning/page/home';
  await page.goto(\`\${orgInfo.instanceUrl}\${targetPath}\`, {
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  });

  console.log(\`✅ Logged in as \${orgInfo.username} → \${page.url()}\`);
}

/**
 * Sets up the SF CLI session on a browser context without navigating.
 * Use this in a Playwright fixture or globalSetup to pre-authenticate.
 *
 * @example
 *   test.use({ storageState: 'playwright/.auth/sf-session.json' });
 *   // or call useSalesforceCLISession(context) in beforeEach
 */
export async function useSalesforceCLISession(context: BrowserContext): Promise<void> {
  const orgInfo = getSalesforceOrgInfo();
  if (!orgInfo) {
    console.warn('⚠️  No SF CLI session — tests will need manual login');
    return;
  }

  const { hostname } = new URL(orgInfo.instanceUrl);
  await context.addCookies([
    {
      name: 'sid',
      value: orgInfo.accessToken,
      domain: hostname,
      path: '/',
      httpOnly: true,
      secure: true,
      sameSite: 'None',
    },
  ]);

  console.log(\`✅ SF CLI session set for \${orgInfo.instanceUrl}\`);
}

// ─── Internal helpers ──────────────────────────────────────────────────────

function parseCliJson(raw: string): Record<string, unknown> | null {
  const clean = raw
    .replace(/\x1b[[0-9;]*m/g, '') // strip ANSI
    .replace(/^\uFEFF/, '') // strip BOM
    .trim();

  const start = clean.indexOf('{');
  const end = clean.lastIndexOf('}');
  if (start === -1 || end <= start) return null;

  try {
    return JSON.parse(clean.slice(start, end + 1)) as Record<string, unknown>;
  } catch {
    return null;
  }
}
`;
}
function salesforceApiTs() {
    return `/*
 * Salesforce REST API Utilities for Playwright
 * Generated by @jterrats/setup-agents — sf setup-agents local --profile qa
 *
 * Provides CRUD and SOQL helpers that reuse the active browser session.
 * The access token is read from the SF CLI via getSalesforceOrgInfo(),
 * so no credentials need to be stored in the test project.
 *
 * Usage:
 *   import { executeSoqlQuery, createRecord } from './salesforce-api';
 *
 *   const result = await executeSoqlQuery(page, 'SELECT Id, Name FROM Account LIMIT 5');
 *   const id = await createRecord(page, 'Case', { Subject: 'Test', Status: 'New' });
 */
import type { Page } from '@playwright/test';
import { getSalesforceOrgInfo } from './salesforce-auth.js';

const API_VERSION = 'v62.0';

/**
 * Runs a SOQL query against the org's REST API.
 * Returns the parsed QueryResult (records array + totalSize + done).
 */
export async function executeSoqlQuery(page: Page, query: string): Promise<SalesforceQueryResult> {
  const { instanceUrl, accessToken } = requireOrgInfo();

  const response = await page.request.get(
    \`\${instanceUrl}/services/data/\${API_VERSION}/query?q=\${encodeURIComponent(query)}\`,
    { headers: authHeaders(accessToken) }
  );

  await assertOk(response, 'SOQL query');
  return response.json() as Promise<SalesforceQueryResult>;
}

/**
 * Creates a record via the REST Composite API.
 * Returns the new record Id.
 */
export async function createRecord(
  page: Page,
  objectType: string,
  fields: Record<string, unknown>
): Promise<string> {
  const { instanceUrl, accessToken } = requireOrgInfo();

  const response = await page.request.post(
    \`\${instanceUrl}/services/data/\${API_VERSION}/sobjects/\${objectType}/\`,
    { headers: authHeaders(accessToken), data: fields }
  );

  await assertOk(response, \`create \${objectType}\`);
  const result = await response.json() as { id: string };
  return result.id;
}

/**
 * Updates an existing record (PATCH).
 */
export async function updateRecord(
  page: Page,
  objectType: string,
  recordId: string,
  fields: Record<string, unknown>
): Promise<void> {
  const { instanceUrl, accessToken } = requireOrgInfo();

  const response = await page.request.patch(
    \`\${instanceUrl}/services/data/\${API_VERSION}/sobjects/\${objectType}/\${recordId}\`,
    { headers: authHeaders(accessToken), data: fields }
  );

  await assertOk(response, \`update \${objectType}:\${recordId}\`);
}

/**
 * Deletes a record by Id.
 */
export async function deleteRecord(
  page: Page,
  objectType: string,
  recordId: string
): Promise<void> {
  const { instanceUrl, accessToken } = requireOrgInfo();

  const response = await page.request.delete(
    \`\${instanceUrl}/services/data/\${API_VERSION}/sobjects/\${objectType}/\${recordId}\`,
    { headers: authHeaders(accessToken) }
  );

  await assertOk(response, \`delete \${objectType}:\${recordId}\`);
}

// ─── Types ─────────────────────────────────────────────────────────────────

export interface SalesforceQueryResult {
  totalSize: number;
  done: boolean;
  records: Array<Record<string, unknown>>;
}

// ─── Internal helpers ──────────────────────────────────────────────────────

function requireOrgInfo() {
  const info = getSalesforceOrgInfo();
  if (!info) throw new Error('No SF CLI session. Run: sf org login web');
  return info;
}

function authHeaders(token: string): Record<string, string> {
  return {
    Authorization: \`Bearer \${token}\`,
    'Content-Type': 'application/json',
  };
}

async function assertOk(response: { ok(): boolean; status(): number; text(): Promise<string> }, label: string): Promise<void> {
  if (!response.ok()) {
    const body = await response.text();
    throw new Error(\`\${label} failed [\${response.status()}]: \${body}\`);
  }
}
`;
}
function sfCliClientTs() {
    return `/*
 * SfCliClient — typed Salesforce CLI wrapper for Playwright / E2E tests.
 * Generated by @jterrats/setup-agents — sf setup-agents local --profile qa
 *
 * Usage:
 *   const client = new SfCliClient();                    // uses default org
 *   const client = new SfCliClient('qa-sandbox');        // named alias
 *   const cases  = await client.query<{ Id: string }>('SELECT Id FROM Case');
 *
 * Security: all sf commands use execFileSync with arg arrays — no shell interpolation.
 */
import { execFileSync } from 'node:child_process';
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';

export class SfCliClient {
  constructor(private targetOrg?: string) {}

  /** Run a SOQL query and return typed rows. */
  async query<T>(soql: string, opts?: { useToolingApi?: boolean; persistToFile?: string }): Promise<T[]> {
    const args = ['data', 'query', '--query', soql, '--json'];
    if (opts?.useToolingApi) args.push('--use-tooling-api');
    if (this.targetOrg) args.push('--target-org', this.targetOrg);
    const raw = execFileSync('sf', args, { encoding: 'utf8', timeout: 30_000 });
    const parsed = JSON.parse(raw) as { result?: { records?: T[] } };
    const records = parsed.result?.records ?? [];
    if (opts?.persistToFile) {
      mkdirSync(dirname(opts.persistToFile), { recursive: true });
      writeFileSync(opts.persistToFile, raw, 'utf8');
    }
    return records;
  }

  /** Run anonymous Apex from a file. */
  async runApex(file: string): Promise<{ success: boolean; logs: string }> {
    const args = ['apex', 'run', '--file', file, '--json'];
    if (this.targetOrg) args.push('--target-org', this.targetOrg);
    try {
      const raw = execFileSync('sf', args, { encoding: 'utf8', timeout: 60_000 });
      const parsed = JSON.parse(raw) as { result?: { success?: boolean; logs?: string } };
      return { success: parsed.result?.success ?? false, logs: parsed.result?.logs ?? '' };
    } catch {
      return { success: false, logs: '' };
    }
  }

  /** Delete a single record by SObject type and Id. */
  async deleteRecord(sobject: string, id: string): Promise<void> {
    const args = ['data', 'delete', 'record', '--sobject', sobject, '--record-id', id, '--json'];
    if (this.targetOrg) args.push('--target-org', this.targetOrg);
    execFileSync('sf', args, { encoding: 'utf8', timeout: 15_000 });
  }
}
`;
}
function retryUntilTs() {
    return `/*
 * retryUntil — bounded retry helper for async DML (Queueable / Platform Events).
 * Generated by @jterrats/setup-agents — sf setup-agents local --profile qa
 *
 * Usage:
 *   const cases = await retryUntil(
 *     () => client.query('SELECT Id FROM Case WHERE Subject = \\'Test\\''),
 *     (rows) => rows.length > 0,
 *     { timeoutMs: 10_000, intervalMs: 500 }
 *   );
 */

export async function retryUntil<T>(
  fn: () => Promise<T>,
  predicate: (value: T) => boolean,
  opts: { timeoutMs: number; intervalMs: number }
): Promise<T> {
  const deadline = Date.now() + opts.timeoutMs;
  let lastValue: T | undefined;
  while (Date.now() < deadline) {
    lastValue = await fn();
    if (predicate(lastValue)) return lastValue;
    await sleep(opts.intervalMs);
  }
  if (lastValue !== undefined && predicate(lastValue)) return lastValue;
  throw new Error(\`retryUntil: predicate not satisfied within \${opts.timeoutMs}ms\`);
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
`;
}
//# sourceMappingURL=salesforce-playwright-generator.js.map