import { readFileSync, statSync } from 'node:fs';
import { E2E } from '../../playwright.config';

/**
 * Reading one-time codes back out of the API's own output.
 *
 * There is no local SMS gateway, and the code is not recoverable anywhere else:
 * `iam.user_verifications.verification_code` holds an argon2 hash, and the
 * notification rows it is delivered through store an empty body. The log line
 * the message composer emits is the only plaintext.
 *
 * Codes are matched by *position in the log*, not by recipient — the line names
 * no user. Callers take an offset before the action that triggers the send and
 * only read what was written after it, which is sound because the suite runs
 * single-worker and serially.
 */

const OTP_PATTERN = /is (\d{4,8})\./g;

/**
 * Byte offset to read from later. Zero when the log does not exist yet.
 *
 * Bytes, and read back as bytes — see `otpSince`.
 */
export function logOffset(): number {
  try {
    return statSync(E2E.apiLog).size;
  } catch {
    return 0;
  }
}

/** Waits for a code to appear after `offset`, and returns the last one seen. */
export async function waitForOtp(
  offset: number,
  timeoutMs = 20_000,
): Promise<string> {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const found = otpSince(offset);
    if (found) return found;
    await new Promise((resolve) => setTimeout(resolve, 250));
  }

  throw new Error(
    `No one-time code appeared in ${E2E.apiLog} within ${timeoutMs}ms. ` +
      `Is the API teeing its output there?`,
  );
}

function otpSince(offset: number): string | null {
  let text: string;
  try {
    // Sliced as a Buffer, then decoded — not `readFileSync(…, 'utf8').slice()`.
    // `logOffset()` is a byte count from `statSync`, while slicing a string
    // counts UTF-16 code units, and the API logs Amharic notification bodies:
    // every multi-byte character made the offset overshoot, so a code written
    // just after it was skipped and the wait timed out. The drift grows with
    // the log, which is why this failed intermittently and more often later in
    // a run.
    text = readFileSync(E2E.apiLog).subarray(offset).toString('utf8');
  } catch {
    return null;
  }

  const codes = [...text.matchAll(OTP_PATTERN)].map((m) => m[1]);
  return codes.length > 0 ? codes[codes.length - 1] : null;
}
