#!/usr/bin/env node
/**
 * Rule #3 + #6 — TDD-gate + writing-plans enforce for production code.
 *
 * PreToolUse on Edit / Write / MultiEdit. Pattern-matches file path against
 * production-code heuristic (isProductionCodePath). When matched:
 *   1. (#6) For feature/bugfix/refactor/cleanup classified tasks: require
 *      Skill(superpowers:writing-plans) OR existing plan-file reference in
 *      current turn.
 *   2. (#3) Require preceding test edit + a `Bash` run of vitest/pest with
 *      a "fail" / "FAIL" / "Failed" indicator in its stdout (RED phase).
 *
 * Override: "срочно" / "быстрый коммит" / "ремонт инфраструктуры".
 *
 * Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md
 */

import {
  readStdin,
  parseEventJson,
  readTranscript,
  lastUserPromptText,
  lastTurnEntries,
  findOverride,
  logOverride,
  exitDecision,
  isProductionCodePath,
  readRouterState,
} from './enforce-hook-helpers.mjs';

const RULE_KEY_TDD = 'tdd-gate';
const RULE_KEY_PLAN = 'writing-plans-required';

/** Map a production path to expected test path patterns (heuristic). */
function expectedTestPathMatchers(prodPath) {
  const n = String(prodPath || '').replace(/\\/g, '/');
  const matchers = [];
  // tools/foo.mjs → tools/foo.test.mjs / tools/foo.spec.mjs
  let m = n.match(/(.*\/)?([^/]+)\.mjs$/);
  if (m) {
    matchers.push(`${m[1] || ''}${m[2]}.test.mjs`);
    matchers.push(`${m[1] || ''}${m[2]}.spec.mjs`);
  }
  // app/app/Path/X.php → app/tests/**/XTest.php OR app/tests/**/X*.php
  m = n.match(/\/app\/app\/(.+)\/([^/]+)\.php$/);
  if (m) {
    matchers.push(`/app/tests/Unit/${m[2]}Test.php`);
    matchers.push(`/app/tests/Feature/${m[2]}Test.php`);
    // Loose containment
    matchers.push(`/app/tests/.+${m[2]}Test.php`);
  }
  // resources/js/views/X.vue → X.spec.ts / X.test.ts loose
  m = n.match(/\/resources\/js\/(.+\/)?([^/]+)\.(vue|ts|tsx|js)$/);
  if (m) {
    matchers.push(`/resources/js/${m[1] || ''}${m[2]}.spec.ts`);
    matchers.push(`/resources/js/${m[1] || ''}${m[2]}.test.ts`);
    matchers.push(`/resources/js/${m[1] || ''}__tests__/${m[2]}.spec.ts`);
  }
  return matchers;
}

function hasMatchingTestEdit(turn, prodPath) {
  const matchers = expectedTestPathMatchers(prodPath);
  const basename = String(prodPath || '').replace(/\\/g, '/').split('/').pop().split('.')[0];
  for (const e of turn) {
    const c = e && e.message && e.message.content;
    if (!Array.isArray(c)) continue;
    for (const b of c) {
      if (!b || b.type !== 'tool_use') continue;
      if (!['Edit', 'Write', 'MultiEdit'].includes(b.name)) continue;
      const p = (b.input && (b.input.file_path || b.input.notebook_path) || '').replace(/\\/g, '/');
      if (!p) continue;
      // Check test-file pattern (loose contains-basename + test/spec)
      if (/\.(test|spec)\.[a-z0-9]+$/i.test(p) && p.includes(basename)) return true;
      // Check explicit matchers
      for (const m of matchers) {
        const mPattern = m.replace(/[.+]/g, '\\$&').replace(/\\\.\\\+/g, '.+');
        if (new RegExp(mPattern + '$').test(p)) return true;
      }
    }
  }
  return false;
}

function hasFailingTestRun(turn) {
  // Look for Bash tool_use followed by tool_result containing a failure indicator
  // OR PASS line with N failed > 0.
  const bashIds = new Set();
  for (const e of turn) {
    const c = e && e.message && e.message.content;
    if (!Array.isArray(c)) continue;
    for (const b of c) {
      if (b && b.type === 'tool_use' && b.name === 'Bash') {
        const cmd = (b.input && b.input.command) || '';
        if (/\b(vitest|pest|phpunit)\b/.test(cmd)) bashIds.add(b.id);
      }
    }
  }
  if (bashIds.size === 0) return false;
  for (const e of turn) {
    const c = e && e.message && e.message.content;
    if (!Array.isArray(c)) continue;
    for (const b of c) {
      if (b && b.type === 'tool_result' && bashIds.has(b.tool_use_id)) {
        const txt = typeof b.content === 'string' ? b.content
                  : Array.isArray(b.content) ? b.content.map((p) => p && p.text).filter(Boolean).join('\n') : '';
        if (/\b(fail|FAIL|Failed|×)\b/.test(txt)) return true;
        // Numeric: "Tests N failed | M passed" with N>0
        const m = txt.match(/Tests\s+(\d+)\s+failed/);
        if (m && Number(m[1]) > 0) return true;
        // JSON reporter (composer test / php artisan test → pest): {"result":"failed",...}
        // or {"failed":N}/{"errors":N} with N>0. command-not-found / error REDs lack the
        // English "Failed" keyword above, so recognise the structured marker too.
        if (/"result"\s*:\s*"failed"/.test(txt)) return true;
        if (/"(?:failed|errors)"\s*:\s*[1-9]/.test(txt)) return true;
      }
    }
  }
  return false;
}

function hasPlanIndicator(turn) {
  for (const e of turn) {
    const c = e && e.message && e.message.content;
    if (!Array.isArray(c)) continue;
    for (const b of c) {
      if (b && b.type === 'tool_use') {
        if (b.name === 'Skill' && b.input && /writing-plans/i.test(String(b.input.skill || ''))) return true;
        const p = (b.input && (b.input.file_path || b.input.notebook_path) || '');
        if (/docs\/superpowers\/plans\//i.test(p)) return true;
        // Also accept Read of a plan file (existing plan)
        if (b.name === 'Read' && /docs\/superpowers\/plans\//i.test(p)) return true;
      }
      if (b && b.type === 'text' && /docs\/superpowers\/plans\//.test(b.text || '')) return true;
    }
  }
  return false;
}

export function decide({
  toolName, filePath, transcriptEntries, classification, override, overridePlan,
}) {
  if (!['Edit', 'Write', 'MultiEdit'].includes(toolName)) return { block: false };
  if (!isProductionCodePath(filePath)) return { block: false };

  const turn = lastTurnEntries(transcriptEntries);

  // Rule #6 — plan requirement for feature/bugfix/refactor/cleanup.
  const taskType = classification && classification.task_type;
  if (!overridePlan && taskType && /^(feature|bugfix|refactor|cleanup)$/i.test(taskType)) {
    if (!hasPlanIndicator(turn)) {
      return {
        block: true,
        message: [
          `[enforce-tdd-gate] task_type="${taskType}" requires a plan before production-code edit.`,
          `Either invoke superpowers:writing-plans via Skill tool,`,
          `or reference an existing plan file (docs/superpowers/plans/...) in this turn first.`,
        ].join('\n'),
      };
    }
  }

  // Rule #3 — TDD gate.
  if (override) return { block: false };
  const hasTest = hasMatchingTestEdit(turn, filePath);
  if (!hasTest) {
    return {
      block: true,
      message: [
        `[enforce-tdd-gate] Production code edit on "${filePath}" without preceding test edit.`,
        `Write the failing test FIRST in the corresponding *.test.mjs / *.spec.ts / *Test.php.`,
        `Then run vitest/pest to confirm RED, then return to this prod-code Edit.`,
      ].join('\n'),
    };
  }
  if (!hasFailingTestRun(turn)) {
    return {
      block: true,
      message: [
        `[enforce-tdd-gate] Test was edited but no vitest/pest run with RED output observed in this turn.`,
        `Run the test suite (vitest run <test-file> / composer test) to confirm RED before prod-code edit.`,
      ].join('\n'),
    };
  }
  return { block: false };
}

async function main() {
  try {
    const raw = await readStdin();
    const event = parseEventJson(raw);
    const toolName = event.tool_name || '';
    const filePath = (event.tool_input && (event.tool_input.file_path || event.tool_input.notebook_path)) || '';

    const transcript = readTranscript(event.transcript_path);
    const userPrompt = lastUserPromptText(transcript);
    const override = findOverride(userPrompt, RULE_KEY_TDD);
    const overridePlan = findOverride(userPrompt, RULE_KEY_PLAN);
    if (override) logOverride(RULE_KEY_TDD, override, event.session_id);
    if (overridePlan) logOverride(RULE_KEY_PLAN, overridePlan, event.session_id);

    const state = readRouterState(event.session_id);
    const classification = state && state.classification ? {
      task_type: state.classification.task_type,
    } : null;

    const result = decide({ toolName, filePath, transcriptEntries: transcript, classification, override, overridePlan });
    exitDecision(result);
  } catch {
    exitDecision({ block: false });
  }
}

const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-tdd-gate.mjs');
if (isCli) main();
