import fs from 'node:fs';
import path from 'node:path';

const ROOT = path.resolve(import.meta.dirname, '..');
const css = fs.readFileSync(path.join(ROOT, 'css', 'ga-icons.css'), 'utf8');
const html = fs.readFileSync(path.join(ROOT, 'pages', 'icons.html'), 'utf8');

let fails = 0;
const ok = (label, cond, detail = '') => {
  if (!cond) fails++;
  console.log(`${cond ? 'PASS' : 'FAIL'}  ${label}${detail ? ` — ${detail}` : ''}`);
};

const UTIL = new Set(['xs', 'sm', 'lg', 'xl', '2x', '3x', 'spin', 'icon-primary', 'icon-success', 'icon-danger', 'icon-warning', 'icon-info', 'icon-muted']);

// ---------------------------------------------------------------- 1. selectors
const selectors = [...css.matchAll(/^\.ga\.ga-([a-z0-9-]+)\s*\{/gm)].map((m) => m[1]);
const iconNames = selectors.filter((n) => !UTIL.has(n));
ok('no duplicate CSS selectors', new Set(selectors).size === selectors.length,
  `${selectors.length} total, ${new Set(selectors).size} unique`);

// -------------------------------------------------------------- 2. gallery sync
const arrSrc = html.match(/const gaIcons = \[([\s\S]*?)\n    \];/)[1];
const gallery = [...arrSrc.matchAll(/'([a-z0-9-]+)'/g)].map((m) => m[1]);
const cssSet = new Set(iconNames);
const galSet = new Set(gallery);
const missingFromGallery = iconNames.filter((n) => !galSet.has(n));
const brokenInGallery = gallery.filter((n) => !cssSet.has(n));
ok('no duplicate gallery entries', new Set(gallery).size === gallery.length,
  `${gallery.length} entries`);
ok('every CSS icon is in the gallery', missingFromGallery.length === 0, missingFromGallery.join(', '));
ok('every gallery name resolves to CSS', brokenInGallery.length === 0, brokenInGallery.join(', '));
ok('counts match', iconNames.length === new Set(gallery).size, `css=${iconNames.length} gallery=${new Set(gallery).size}`);

// ------------------------------------------------------- 3. rules / mask pairs
const rules = [...css.matchAll(/^\.ga\.ga-([a-z0-9-]+) \{ -webkit-mask-image: url\("([^"]+)"\); mask-image: url\("([^"]+)"\); \}$/gm)];
const multiline = [...css.matchAll(/^\.ga\.ga-([a-z0-9-]+) \{\r?\n {2}-webkit-mask-image: url\("([^"]+)"\);\r?\n {2}mask-image: url\("([^"]+)"\);\r?\n\}$/gm)];
const all = [...rules, ...multiline];
ok('every icon rule parsed as a mask pair', all.length === iconNames.length,
  `parsed ${all.length} of ${iconNames.length}`);

const mismatched = all.filter(([, , a, b]) => a !== b).map((m) => m[1]);
ok('-webkit-mask-image and mask-image are byte-identical', mismatched.length === 0, mismatched.join(', '));

// -------------------------------------------------------- 4. brace balance
const opens = (css.match(/\{/g) || []).length;
const closes = (css.match(/\}/g) || []).length;
ok('CSS braces balanced', opens === closes, `${opens} open / ${closes} close`);

// ------------------------------------------------- 5. encoding + SVG integrity
const PREFIX = 'data:image/svg+xml,';
const SHAPES = /<(path|circle|rect|line|polyline|polygon|ellipse)\b/;

// Minimal XML well-formedness checker: verifies tag nesting and attribute syntax.
function xmlWellFormed(src) {
  const stack = [];
  const tagRe = /<(\/?)([A-Za-z][\w:-]*)((?:\s+[\w:-]+\s*=\s*'[^']*')*)\s*(\/?)>/g;
  let idx = 0;
  let m;
  while ((m = tagRe.exec(src))) {
    if (m.index !== idx) return `unparsable at offset ${idx}: ${JSON.stringify(src.slice(idx, idx + 60))}`;
    idx = tagRe.lastIndex;
    const [, closing, name, , selfClose] = m;
    if (closing) {
      if (stack.pop() !== name) return `mismatched </${name}>`;
    } else if (!selfClose) stack.push(name);
  }
  if (idx !== src.length) return `trailing junk: ${JSON.stringify(src.slice(idx, idx + 60))}`;
  if (stack.length) return `unclosed <${stack.join('>, <')}>`;
  return null;
}

const encodingBad = [];
const parseBad = [];
const shapeBad = [];
const outlineBad = [];
const brandBad = [];

// Convention checks apply to the icons added in batch 7 only — the pre-existing
// set already mixes outline and filled artwork (github, shield-fill*, heart-fill...).
const newBlock = css.slice(css.indexOf('Batch 7a'));
const newNames = new Set([...newBlock.matchAll(/^\.ga\.ga-([a-z0-9-]+) \{/gm)].map((m) => m[1]));
// Brands are the only filled batch; batch 8 starts right after it.
const brandBlock = css.slice(css.indexOf('Batch 7d'), css.indexOf('Batch 8a'));
const brandNames = new Set([...brandBlock.matchAll(/^\.ga\.ga-([a-z0-9-]+) \{/gm)].map((m) => m[1]));
ok('batches 7+8 added 301 icons', newNames.size === 301, `${newNames.size} found`);
ok('batch 7d holds 28 brands', brandNames.size === 28, `${brandNames.size} found`);

for (const [, name, uri] of all) {
  const body = uri.slice(PREFIX.length);
  if (!uri.startsWith(PREFIX)) encodingBad.push(`${name}: bad prefix`);
  if (/[<>#"]/.test(body)) encodingBad.push(`${name}: raw <>#" in data URI`);

  const svg = decodeURIComponent(body);
  const err = xmlWellFormed(svg);
  if (err) parseBad.push(`${name}: ${err}`);
  if (!SHAPES.test(svg)) shapeBad.push(name);

  if (brandNames.has(name)) {
    if (!svg.includes(`viewBox='0 0 24 24' fill='#000'`) || / stroke=/.test(svg.slice(0, svg.indexOf('>')))) brandBad.push(name);
  } else if (newNames.has(name) && !svg.includes(`viewBox='0 0 24 24' fill='none' stroke='#000' stroke-width='2'`)) {
    outlineBad.push(name);
  }
}

ok('no raw <, >, # or " leaked into any data URI', encodingBad.length === 0, encodingBad.slice(0, 5).join(' | '));
ok('every SVG is well-formed XML', parseBad.length === 0, parseBad.slice(0, 5).join(' | '));
ok('every SVG contains at least one shape', shapeBad.length === 0, shapeBad.slice(0, 8).join(', '));
ok('outline icons use the 24x24 stroke convention', outlineBad.length === 0, outlineBad.slice(0, 8).join(', '));
ok('brand icons use the 24x24 filled convention', brandBad.length === 0, brandBad.slice(0, 8).join(', '));

// -------------------------------------------------------------- 6. coordinates
// Flag geometry that escapes the 0..24 viewBox by more than a hairline.
const outOfBounds = [];
for (const [, name, uri] of all) {
  const svg = decodeURIComponent(uri.slice(PREFIX.length));
  const nums = [...svg.matchAll(/(?:x1|y1|x2|y2|cx|cy)='(-?[\d.]+)'/g)].map((m) => parseFloat(m[1]));
  const pts = [...svg.matchAll(/points='([^']+)'/g)].flatMap((m) => m[1].trim().split(/[\s,]+/).map(Number));
  const bad = [...nums, ...pts].filter((v) => Number.isFinite(v) && (v < -0.5 || v > 24.5));
  if (bad.length) outOfBounds.push(`${name}: ${[...new Set(bad)].join(',')}`);
}
ok('no line/circle/polyline coords escape the viewBox', outOfBounds.length === 0, outOfBounds.slice(0, 8).join(' | '));

console.log(`\n${fails === 0 ? 'ALL CHECKS PASSED' : `${fails} CHECK(S) FAILED`}  —  ${iconNames.length} icons in ga-icons.css`);
process.exit(fails ? 1 : 0);
