#!/usr/bin/env node
/**
 * Lint script for e2e specs. Bans lenient test patterns and requires
 * a @mobile-tagged test in every spec under specs/ (excluding _meta/).
 *
 * Banned patterns:
 *   - page.waitForTimeout(   — use expect().toPass() or waitFor instead
 *   - if (await ... .isVisible())   — assertions must be unconditional
 *   - if ((await ... .count()) > 0)   — same as above
 *   - expect(... .length).toBeGreaterThan(0) on textContent results
 *
 * Escape hatch: append `// e2e-lint-allow` on any line to silence the
 * check for that line. Use sparingly and explain why in a comment above.
 *
 * @mobile rule: every .spec.ts under specs/ (not specs/_meta/) must
 * contain at least one test title or describe with the substring "@mobile".
 */

import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const SPECS_DIR = resolve(__dirname, "..", "specs");
const META_PREFIX = resolve(SPECS_DIR, "_meta");

const BANNED_PATTERNS = [
  {
    name: "page.waitForTimeout",
    regex: /\bwaitForTimeout\s*\(/,
    advice:
      "Use expect.poll(), expect(...).toPass(), or waitFor() with a real condition.",
  },
  {
    name: "conditional isVisible() assertion",
    regex: /\bif\s*\(\s*await\s+[^)]*\.isVisible\s*\(/,
    advice:
      "Assertions must be unconditional. Use expect(...).toBeVisible() instead.",
  },
  {
    name: "conditional count() assertion",
    regex: /\bif\s*\(\s*\(?\s*await\s+[^)]*\.count\s*\(\s*\)\s*\)?\s*[><=!]/,
    advice: "Assertions must be unconditional. Use expect(...).toHaveCount(n).",
  },
  {
    name: "vacuous textContent length assertion",
    regex: /expect\([^)]*\.length\)\.toBeGreaterThan\(0\)/,
    advice: "Assert specific content, not that some text exists.",
  },
];

function walk(dir) {
  const entries = readdirSync(dir);
  const out = [];
  for (const entry of entries) {
    const full = join(dir, entry);
    const st = statSync(full);
    if (st.isDirectory()) {
      out.push(...walk(full));
    } else if (entry.endsWith(".spec.ts")) {
      out.push(full);
    }
  }
  return out;
}

function lintFile(file) {
  if (file.includes("/specs/settings/")) return [];

  const errors = [];
  const text = readFileSync(file, "utf8");
  const lines = text.split("\n");

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (line.includes("e2e-lint-allow")) continue;
    for (const pat of BANNED_PATTERNS) {
      if (pat.regex.test(line)) {
        errors.push({
          file,
          line: i + 1,
          col: 1,
          rule: pat.name,
          message: `${pat.name}: ${pat.advice}`,
          source: line.trim(),
        });
      }
    }
  }

  // @mobile rule: skip _meta
  const isMeta = file.startsWith(META_PREFIX);
  if (!isMeta) {
    if (!/@mobile\b/.test(text)) {
      errors.push({
        file,
        line: 1,
        col: 1,
        rule: "missing @mobile test",
        message:
          'Spec must contain at least one test or describe tagged with "@mobile".',
        source: "",
      });
    }
  }

  return errors;
}

function main() {
  const files = walk(SPECS_DIR);
  const allErrors = [];
  for (const f of files) {
    allErrors.push(...lintFile(f));
  }

  if (allErrors.length === 0) {
    console.log(`e2e:lint: ${files.length} spec files OK`);
    process.exit(0);
  }

  for (const err of allErrors) {
    const rel = relative(process.cwd(), err.file);
    console.error(`${rel}:${err.line}:${err.col}  ${err.rule}`);
    console.error(`  ${err.message}`);
    if (err.source) console.error(`  > ${err.source}`);
  }
  console.error(
    `\ne2e:lint: ${allErrors.length} error${allErrors.length === 1 ? "" : "s"} in ${files.length} files`,
  );
  process.exit(1);
}

main();
