/**
 * SET 89C — case-body registration conformance + canonical unwrap.
 *
 * The single entry point (`conformCaseBody`) used at every case intake:
 * verified templates, initial generation, fallback fill, born-red surgery,
 * freeze-gate redraw, RHA case surgery, judgment amendment, case relay.
 *
 * Contract:
 *   1. deterministic outer-block unwrap (exact `it`/`test`/`describe`);
 *   2. token-aware registration check (no `it(`/`test(`/`describe(` anywhere
 *      in the body, including under conditionals or in called helpers);
 *   3. dynamic-eval lint (SET 35b);
 *   4. syntax check (sucrase);
 *   5. assertion presence check (`expect(`).
 *
 * The scanner is token-aware: it strips string literals (single, double,
 * template), line comments, and block comments before scanning. It also
 * distinguishes `obj.test(` (member call, not a registrar) from `test(`.
 * Regex is used only after the strip, on a stream where quotes/comments
 * cannot confuse it.
 */
import { caseSyntaxErrors, dynamicEvalLint } from './testExec';

export type ConformDisposition =
  | 'ok'
  | 'rejected-registration'
  | 'rejected-syntax'
  | 'rejected-dynamic-eval'
  | 'rejected-empty';

export type UnwrapKind =
  | 'none'
  | 'outer-it'
  | 'outer-test'
  | 'outer-describe-it';

export interface ConformResult {
  disposition: ConformDisposition;
  /** The body after unwrap (identical to input when no unwrap happened). */
  body: string;
  unwrapped: UnwrapKind;
  /** Human-readable reason (empty when ok). */
  reason: string;
}

// ---------------------------------------------------------------------------
// Token-aware source stripping — the *only* correct way to search for
// registration calls, because JS strings and comments can legitimately
// contain the word `it(` or `test(`.
// ---------------------------------------------------------------------------

/**
 * Return a copy of `source` with every string literal, template literal, and
 * comment replaced by whitespace of the SAME length. Line and column positions
 * are preserved so regex offsets remain meaningful for callers.
 */
export function stripStringsAndComments(source: string): string {
  const out: string[] = [];
  let i = 0;
  const n = source.length;
  while (i < n) {
    const c = source[i]!;
    // Line comment.
    if (c === '/' && source[i + 1] === '/') {
      const start = i;
      while (i < n && source[i] !== '\n') i++;
      out.push(' '.repeat(i - start));
      continue;
    }
    // Block comment.
    if (c === '/' && source[i + 1] === '*') {
      const start = i;
      i += 2;
      while (i < n && !(source[i] === '*' && source[i + 1] === '/')) i++;
      if (i < n) i += 2;
      out.push(source.slice(start, i).replace(/[^\n]/g, ' '));
      continue;
    }
    // String / template.
    if (c === '"' || c === "'" || c === '`') {
      const quote = c;
      const start = i;
      i++;
      while (i < n) {
        const ch = source[i]!;
        if (ch === '\\') { i += 2; continue; }
        if (ch === quote) { i++; break; }
        // Template ${...} — treat as opaque, still strip (deep interpolation
        // scanning is not required for the registration decision).
        if (quote === '`' && ch === '$' && source[i + 1] === '{') {
          i += 2;
          let depth = 1;
          while (i < n && depth > 0) {
            const d = source[i]!;
            if (d === '{') depth++;
            else if (d === '}') depth--;
            i++;
          }
          continue;
        }
        i++;
      }
      out.push(source.slice(start, i).replace(/[^\n]/g, ' '));
      continue;
    }
    out.push(c);
    i++;
  }
  return out.join('');
}

/**
 * Registration-call detector, token-aware. Looks for `it(`, `test(`, or
 * `describe(` in the stripped source, not preceded by an identifier character
 * or `.` (so `obj.test(` and `sanity_test(` are ignored).
 */
export function containsRegistrationCall(source: string): boolean {
  const stripped = stripStringsAndComments(source);
  // (^|[^A-Za-z0-9_$.]) prevents `obj.test(` and `sanity_test(` matches.
  return /(^|[^A-Za-z0-9_$.])(it|test|describe)\s*\(/.test(stripped);
}

/**
 * Match the canonical `it("name", fn)` / `test("name", fn)` shape:
 * exactly one top-level call whose second argument is an arrow or function.
 * Returns the inner callback body when the entire source is exactly one such
 * block, otherwise null.
 */
function matchExactOuterCall(
  source: string,
  registrar: 'it' | 'test' | 'describe',
): { innerBody: string; strippedInner: string } | null {
  const trimmed = source.trim().replace(/;\s*$/, '');
  const re = new RegExp(`^${registrar}\\s*\\(\\s*(['"\`])`);
  const head = re.exec(trimmed);
  if (!head) return null;
  const quote = head[1]!;
  let i = head[0].length;
  // Scan through the title string.
  while (i < trimmed.length) {
    const c = trimmed[i]!;
    if (c === '\\') { i += 2; continue; }
    if (c === quote) { i++; break; }
    i++;
  }
  // Expect `,` (with optional async and arrow / function).
  const rest = trimmed.slice(i);
  const arrow = /^\s*,\s*(async\s+)?(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/.exec(rest);
  const fn = /^\s*,\s*(async\s+)?function\s*(?:[A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{/.exec(rest);
  const match = arrow || fn;
  if (!match) return null;
  const bodyOpen = i + match[0].length - 1;
  // Find matching `}` at depth 0 with string awareness.
  let depth = 0;
  let inStr: string | null = null;
  for (let j = bodyOpen; j < trimmed.length; j++) {
    const c = trimmed[j]!;
    if (inStr) {
      if (c === '\\') { j++; continue; }
      if (c === inStr) inStr = null;
      continue;
    }
    if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
    if (c === '{') depth++;
    else if (c === '}') {
      depth--;
      if (depth === 0) {
        const after = trimmed.slice(j + 1).trim();
        // Allow ` )` or ` );` after the closing brace.
        if (after !== '' && !/^\)\s*;?\s*$/.test(after)) return null;
        const innerBody = trimmed.slice(bodyOpen + 1, j).trim();
        const strippedInner = stripStringsAndComments(innerBody);
        return { innerBody, strippedInner };
      }
    }
  }
  return null;
}

/**
 * The single entry point for every case-body intake. Performs the full
 * ordered pipeline described in SET 89 §5.3 and returns a structured
 * disposition (never throws).
 */
export function conformCaseBody(source: string): ConformResult {
  const raw = (source ?? '').trim();
  if (!raw) {
    return { disposition: 'rejected-empty', body: source, unwrapped: 'none', reason: 'empty case body' };
  }

  // 1) exact outer-block unwrap (`it`/`test` first, then `describe` with one
  //    canonical inner `it`/`test`).
  let unwrapped: UnwrapKind = 'none';
  let body = source;
  const outerIt = matchExactOuterCall(raw, 'it');
  const outerTest = outerIt ? null : matchExactOuterCall(raw, 'test');
  const outerDescribe = (outerIt || outerTest) ? null : matchExactOuterCall(raw, 'describe');
  if (outerIt) {
    body = outerIt.innerBody;
    unwrapped = 'outer-it';
  } else if (outerTest) {
    body = outerTest.innerBody;
    unwrapped = 'outer-test';
  } else if (outerDescribe) {
    // Optional two-level unwrap: allowed only when the describe body is
    // exactly one it/test and nothing else. Reject otherwise.
    const inner = outerDescribe.innerBody.trim();
    const innerAsIt = matchExactOuterCall(inner, 'it');
    const innerAsTest = innerAsIt ? null : matchExactOuterCall(inner, 'test');
    if (innerAsIt) {
      body = innerAsIt.innerBody;
      unwrapped = 'outer-describe-it';
    } else if (innerAsTest) {
      body = innerAsTest.innerBody;
      unwrapped = 'outer-describe-it';
    } else {
      // Multiple registrations under a describe, or hooks, or sibling stmts —
      // never partially discard, reject cleanly.
      return {
        disposition: 'rejected-registration',
        body,
        unwrapped: 'none',
        reason: 'describe(...) body is not exactly one canonical it/test — nested registration',
      };
    }
  }

  // 2) token-aware registration check on the unwrapped body.
  if (containsRegistrationCall(body)) {
    return {
      disposition: 'rejected-registration',
      body,
      unwrapped,
      reason: 'case body contains a nested it/test/describe registration call',
    };
  }

  // 3) dynamic-eval lint.
  const lint = dynamicEvalLint(body);
  if (lint) {
    return {
      disposition: 'rejected-dynamic-eval',
      body,
      unwrapped,
      reason: lint,
    };
  }

  // 4) syntax check (sucrase, matches harness assembly).
  const syn = caseSyntaxErrors([{ name: '_', body }])[0];
  if (syn) {
    return {
      disposition: 'rejected-syntax',
      body,
      unwrapped,
      reason: syn.error,
    };
  }

  // 5) assertion presence.
  if (!/\bexpect\s*\(/.test(stripStringsAndComments(body))) {
    return {
      disposition: 'rejected-empty',
      body,
      unwrapped,
      reason: 'case body has no expect(...) assertion',
    };
  }

  return { disposition: 'ok', body, unwrapped, reason: '' };
}

/**
 * Convenience: `true` when the body is unquestionably clean under the
 * conformance contract. Used by callers that only need a boolean.
 */
export function isConformantCaseBody(source: string): boolean {
  return conformCaseBody(source).disposition === 'ok';
}
