// tests/build/no-remote-fonts.test.ts — Plan 01-12 Wave 0 RED unit test.
//
// MV3 CSP enforcement gate: production dist/ MUST contain ZERO
// fonts.googleapis.com / https://fonts URLs. The canonical tokens.css
// (Wave 1 Task 2) replaces the design-incoming Google Fonts @import
// (line 12 of design-incoming/.../tokens.css) with local @font-face
// rules pointing at ./fonts/*.woff2.
//
// Polarity at Wave 0 land:
//   - RED-vacuous today (the design-incoming tokens.css is not yet copied
//     into src/shared/; once Wave 1 Task 2 copies + applies the surgical
//     edits, this test confirms the edit removed the remote @import).
//
// Implementation mirrors tests/background/no-test-hooks-in-prod-bundle.test.ts:
//   - Build via npm run build (gated by SKIP_BUILD=1 escape hatch)
//   - Recursive walk over dist/
//   - For each file, count occurrences of forbidden substrings
//   - Assert zero matches
//
// References:
//   - RESEARCH §1 + §13 (MV3 CSP self-host enforcement)
//   - Plan 01-12 threat model T-01-12-01 + T-01-12-02 (remote font load
//     + CDN MITM mitigations)

import { execFile } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
import { promisify } from 'node:util';

import { describe, expect, it } from 'vitest';

const execFileAsync = promisify(execFile);

/** Forbidden substrings — any occurrence in dist/ violates MV3 CSP self-host invariant. */
const FORBIDDEN_REMOTE_STRINGS: ReadonlyArray<string> = [
  'fonts.googleapis.com',
  'https://fonts',
  'googleapis',
];

const PROD_BUILD_TIMEOUT_MS = 90_000;
const DIST_DIR = resolvePath(process.cwd(), 'dist');

interface ForbiddenMatch {
  readonly filePath: string;
  readonly count: number;
}

function listAllFilesRecursive(root: string): ReadonlyArray<string> {
  const accumulator: string[] = [];
  const stack: string[] = [root];
  while (stack.length > 0) {
    const dir = stack.pop()!;
    const entries = readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = resolvePath(dir, entry.name);
      if (entry.isSymbolicLink()) continue;
      if (entry.isDirectory()) {
        stack.push(fullPath);
      } else if (entry.isFile()) {
        accumulator.push(fullPath);
      }
    }
  }
  return accumulator.sort();
}

function countOccurrencesInFile(filePath: string, needle: string): number {
  const binaryExtensions = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp',
    '.woff', '.woff2', '.ttf', '.otf',
  ]);
  const dotIdx = filePath.lastIndexOf('.');
  const ext = dotIdx >= 0 ? filePath.substring(dotIdx).toLowerCase() : '';
  if (binaryExtensions.has(ext)) return 0;
  const stat = statSync(filePath);
  if (stat.size === 0) return 0;
  const text = readFileSync(filePath, 'utf8');
  let count = 0;
  let from = 0;
  for (;;) {
    const idx = text.indexOf(needle, from);
    if (idx < 0) break;
    count += 1;
    from = idx + needle.length;
  }
  return count;
}

function findMatchesInDist(needle: string): ReadonlyArray<ForbiddenMatch> {
  const files = listAllFilesRecursive(DIST_DIR);
  const matches: ForbiddenMatch[] = [];
  for (const filePath of files) {
    const count = countOccurrencesInFile(filePath, needle);
    if (count > 0) {
      matches.push({ filePath, count });
    }
  }
  return matches;
}

async function runProductionBuild(): Promise<void> {
  await execFileAsync('npm', ['run', 'build'], {
    timeout: PROD_BUILD_TIMEOUT_MS,
    maxBuffer: 16 * 1024 * 1024,
    env: { ...process.env, NODE_NO_WARNINGS: '1' },
  });
}

describe('production bundle has no remote font URLs (MV3 CSP — T-01-12-01)', () => {
  it('npm run build completes and dist/ exists', { timeout: PROD_BUILD_TIMEOUT_MS + 5_000 }, async () => {
    if (process.env.SKIP_BUILD !== '1') {
      await runProductionBuild();
    }
    expect(
      existsSync(DIST_DIR),
      `dist/ missing at ${DIST_DIR}. Either npm run build failed or SKIP_BUILD=1 was set ` +
        `without a pre-existing build.`,
    ).toBe(true);
  });

  for (const needle of FORBIDDEN_REMOTE_STRINGS) {
    it(`dist/ does not contain '${needle}' (MV3 CSP self-host invariant)`, () => {
      if (!existsSync(DIST_DIR)) {
        throw new Error(
          `dist/ missing — run \`npm run build\` first (SKIP_BUILD=1 set but no prior build).`,
        );
      }
      const matches = findMatchesInDist(needle);
      expect(
        matches.length,
        matches.length === 0
          ? 'unreachable'
          : `dist/ contains '${needle}' in ${matches.length} file(s) — violates MV3 CSP ` +
            `style-src 'self' + font-src 'self'. Replace remote @import in tokens.css ` +
            `with local @font-face rules pointing at ./fonts/*.woff2 (Wave 1 Task 2). ` +
            `Offending files:\n` +
            matches
              .map((m) => `  - ${m.filePath} (${m.count} occurrence${m.count === 1 ? '' : 's'})`)
              .join('\n'),
      ).toBe(0);
    });
  }
});
