// tests/uat/lib/assertions.ts — Plan 01-13 Wave 2.
//
// Host-side assertion primitives. Re-exports of node:assert/strict with
// structured failure messages + diagnostic-dump wrappers that capture
// SW + offscreen console buffers on failure.
//
// IMPORTANT — NO chrome.* helpers here. All chrome.* work happens
// inside the extension-internal harness page (see
// tests/uat/extension-page-harness.ts and its `window.__mokoshHarness`
// surface). This module is host-side ONLY — it runs in the Node
// process that drives Puppeteer. Calling chrome.* from here would
// fail (no chrome global in Node) — by-construction, not by convention.
//
// References:
//   - node:assert/strict (deep strict equality):
//     https://nodejs.org/api/assert.html#strict-assertion-mode

import * as assert from 'node:assert/strict';

/**
 * One assertion-internal check record — populated by the harness page's
 * `assertA*` methods. Each AssertionRecord carries 1..N CheckRecords
 * which collectively determine whether the AssertionRecord PASSES.
 */
export interface CheckRecord {
  readonly name: string;
  readonly expected: unknown;
  readonly actual: unknown;
  readonly passed: boolean;
}

/**
 * Structured result returned by every page-side `assertA*` method.
 * Mirrors the shape used by the proven prototype (c647f61) so the
 * host-side `runAssertion` + `printAssertionResult` can consume any
 * assertion uniformly.
 */
export interface AssertionRecord {
  readonly passed: boolean;
  readonly name: string;
  readonly checks: ReadonlyArray<CheckRecord>;
  readonly diagnostics: ReadonlyArray<string>;
  readonly error?: string;
}

/**
 * Accumulating console buffers from `launchHarnessBrowser`. Passed
 * into `runAssertion` so a failing assertion can dump the SW + offscreen
 * logs to stderr alongside the structured CheckRecords. The buffers
 * are MUTABLE arrays owned by `launch.ts`; readers MUST NOT mutate.
 */
export interface ConsoleBuffers {
  readonly swConsole: ReadonlyArray<string>;
  readonly offConsole: ReadonlyArray<string>;
}

/**
 * How many trailing lines of each console buffer to dump on a failure.
 * Bounded so a long-running test with thousands of lines does not
 * overwhelm stderr; the cap is generous enough to capture the relevant
 * preamble + the actual failure trigger.
 */
const CONSOLE_DUMP_TAIL_LINES = 100;

/**
 * Wrap a single assertion attempt with try/catch + diagnostic dump on
 * failure. The `fn` is the page-side call (typically a `driveA*`
 * wrapper from `harness-page-driver.ts`); a thrown error becomes an
 * AssertionRecord with `passed: false` + the error message in `.error`.
 *
 * On failure, dumps the last `CONSOLE_DUMP_TAIL_LINES` of each console
 * buffer to stderr — sized to fit the typical assertion timeline
 * (several seconds of SW + offscreen logs) without spamming.
 *
 * @param name    - Assertion name (used only for the failure preamble).
 * @param fn      - Async function returning the page-side AssertionRecord.
 * @param buffers - Console buffers from `launchHarnessBrowser`.
 * @returns The page-side AssertionRecord (with passed=false on throw).
 */
export async function runAssertion(
  name: string,
  fn: () => Promise<AssertionRecord>,
  buffers: ConsoleBuffers,
): Promise<AssertionRecord> {
  try {
    const result = await fn();
    if (!result.passed) {
      dumpConsoleTail(name, buffers);
    }
    return result;
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : String(err);
    dumpConsoleTail(name, buffers);
    return {
      passed: false,
      name,
      checks: [],
      diagnostics: [`runAssertion caught: ${errMsg}`],
      error: errMsg,
    };
  }
}

/**
 * Dump the tail of each console buffer to stderr — used by
 * `runAssertion` on any failure path. Each line is already pre-tagged
 * (`[sw:log] ...` / `[off:log] ...`) by the listeners in `launch.ts`.
 *
 * @param assertionName - Name to prefix the dump header.
 * @param buffers       - Console buffers to dump.
 */
function dumpConsoleTail(
  assertionName: string,
  buffers: ConsoleBuffers,
): void {
  process.stderr.write(
    `\n--- console dump for assertion '${assertionName}' (tail ${CONSOLE_DUMP_TAIL_LINES} lines per buffer) ---\n`,
  );
  const swTail = buffers.swConsole.slice(-CONSOLE_DUMP_TAIL_LINES);
  const offTail = buffers.offConsole.slice(-CONSOLE_DUMP_TAIL_LINES);
  for (const line of swTail) {
    process.stderr.write(line + '\n');
  }
  for (const line of offTail) {
    process.stderr.write(line + '\n');
  }
  process.stderr.write(
    `--- end console dump for '${assertionName}' ---\n\n`,
  );
}

/**
 * Pretty-print an AssertionRecord to stdout. Used by both the
 * orchestrator (`harness.test.ts` in Wave 3A) and the standalone A6
 * entry (`a6.test.ts`). Single source of formatting truth.
 *
 * @param result - The structured result from a page-side assertion.
 */
export function printAssertionResult(result: AssertionRecord): void {
  process.stdout.write('\n');
  process.stdout.write('='.repeat(72) + '\n');
  process.stdout.write(`${result.name}: ${result.passed ? 'PASS' : 'FAIL'}\n`);
  if (result.error !== undefined) {
    process.stdout.write(`Top-level error: ${result.error}\n`);
  }
  process.stdout.write('\nChecks:\n');
  for (const check of result.checks) {
    const mark = check.passed ? '[PASS]' : '[FAIL]';
    process.stdout.write(`  ${mark} ${check.name}\n`);
    process.stdout.write(`         expected: ${JSON.stringify(check.expected)}\n`);
    process.stdout.write(`         actual:   ${JSON.stringify(check.actual)}\n`);
  }
  process.stdout.write('\nDiagnostics:\n');
  for (const diag of result.diagnostics) {
    process.stdout.write(`  - ${diag}\n`);
  }
  process.stdout.write('='.repeat(72) + '\n');
}

/**
 * Wrapper around `assert.deepStrictEqual` with a structured message
 * preamble. Throws AssertionError on mismatch (caller catches in
 * `runAssertion`).
 *
 * @param actual   - Observed value.
 * @param expected - Reference value.
 * @param message  - Human-readable context (e.g. assertion name).
 */
export function assertEqual(actual: unknown, expected: unknown, message: string): void {
  assert.deepStrictEqual(actual, expected, message);
}

/**
 * Assert `actual >= expected`. Throws AssertionError on failure with
 * a structured message including both values.
 *
 * @param actual   - Observed numeric value.
 * @param expected - Lower bound (inclusive).
 * @param message  - Human-readable context.
 */
export function assertGte(actual: number, expected: number, message: string): void {
  if (actual < expected) {
    throw new assert.AssertionError({
      message: `${message} — expected ${actual} >= ${expected}`,
      actual,
      expected,
      operator: '>=',
    });
  }
}

/**
 * Assert `actual` matches the regex. Throws AssertionError on failure.
 *
 * @param actual  - Observed string.
 * @param regex   - Pattern to test.
 * @param message - Human-readable context.
 */
export function assertMatch(actual: string, regex: RegExp, message: string): void {
  if (!regex.test(actual)) {
    throw new assert.AssertionError({
      message: `${message} — expected ${JSON.stringify(actual)} to match ${regex}`,
      actual,
      expected: regex,
      operator: 'match',
    });
  }
}

/**
 * Assert `cond` is exactly `true`. Throws AssertionError otherwise.
 *
 * @param cond    - Boolean to assert true.
 * @param message - Human-readable context.
 */
export function assertTrue(cond: boolean, message: string): void {
  assert.strictEqual(cond, true, message);
}

/**
 * Default polling interval for `waitFor` — matches the prototype's
 * 100ms cadence (good tradeoff between CPU and detection latency).
 */
const WAIT_FOR_POLL_INTERVAL_MS = 100;

/**
 * Poll an async probe until it satisfies the predicate or the timeout
 * elapses. Mirrors the prototype's host-side polling primitive
 * (verbatim semantics, host-side scope).
 *
 * IMPORTANT: this is the HOST-SIDE waitFor. The HARNESS-PAGE-SIDE
 * waitFor (inside `tests/uat/extension-page-harness.ts`) is a separate
 * implementation with identical semantics — the page-side runs in the
 * browser isolate; the host-side runs in Node. They cannot share a
 * module because one is bundled into the HTML harness and the other
 * runs natively.
 *
 * @param probe       - Async function returning the current value.
 * @param predicate   - Returns true when the value matches the expectation.
 * @param timeoutMs   - Maximum wait time before throwing.
 * @param description - Used in the timeout error message.
 * @returns The value that satisfied the predicate.
 * @throws If the timeout elapses; the error includes the last observed value.
 */
export async function waitFor<T>(
  probe: () => Promise<T> | T,
  predicate: (value: T) => boolean,
  timeoutMs: number,
  description: string,
): Promise<T> {
  const start = Date.now();
  let lastValue: T = await probe();
  while (Date.now() - start < timeoutMs) {
    if (predicate(lastValue)) {
      return lastValue;
    }
    await new Promise((resolve) => setTimeout(resolve, WAIT_FOR_POLL_INTERVAL_MS));
    lastValue = await probe();
  }
  throw new Error(
    `waitFor timeout (${timeoutMs}ms) — ${description}; lastValue=${JSON.stringify(lastValue)}`,
  );
}
