import path = require("path");
import fs = require("fs-extra");
import { VrtDiffResult, ChangeRegion } from "./vrtCapture.util";

/**
 * VRTReportGenerator — Generates a static HTML summary report for VRT results.
 * Features:
 *   • Three-pane view: Baseline | Test | Diff Heatmap
 *   • Change Region Analysis table with hover-to-highlight on heatmap
 *   • "What to Update" guidance per failure
 *   • Approve / Reject with CLI command generation
 *   • Batch export of approval commands
 */
export class VRTReportGenerator {
  private static allResults: Map<string, VrtDiffResult[]> = new Map();

  /** Register results from a single scenario */
  static registerScenarioResults(
    scenarioName: string,
    results: VrtDiffResult[],
  ): void {
    if (results.length > 0) {
      VRTReportGenerator.allResults.set(scenarioName, results);
    }
  }

  /** Clear all collected results (call in AfterAll after generating report) */
  static clear(): void {
    VRTReportGenerator.allResults.clear();
  }

  /** Generate the HTML report and JSON summary */
  static generate(): string | null {
    if (VRTReportGenerator.allResults.size === 0) {
      return null;
    }

    const reportDir = path.join(process.cwd(), "test-results", "vrt");
    fs.ensureDirSync(reportDir);
    const assetsDir = path.join(reportDir, "assets");
    fs.ensureDirSync(path.join(assetsDir, "baseline"));
    fs.ensureDirSync(path.join(assetsDir, "test"));
    fs.ensureDirSync(path.join(assetsDir, "diff"));

    // Build JSON summary
    const summary: any = {
      timestamp: new Date().toISOString(),
      sprint: process.env.VRT_SPRINT || "default",
      mode: process.env.VRT_MODE || "off",
      scenarios: {} as Record<string, any>,
      totals: { total: 0, passed: 0, failed: 0, newSnapshots: 0 },
    };

    // Copy assets and build results
    Array.from(VRTReportGenerator.allResults.entries()).forEach(
      ([scenario, results]) => {
        summary.scenarios[scenario] = results.map((r) => {
          const entry: any = {
            name: r.snapshotName,
            passed: r.passed,
            mismatch: r.mismatchPercentage,
            mismatchPixels: r.mismatchPixels,
            totalPixels: r.totalPixels,
            changeRegions: r.changeRegions,
            imageWidth: r.imageWidth,
            imageHeight: r.imageHeight,
            baselinePath: VRTReportGenerator.copyAsset(
              r.baselinePath,
              assetsDir,
              "baseline",
            ),
            testPath: VRTReportGenerator.copyAsset(
              r.testPath,
              assetsDir,
              "test",
            ),
            diffPath: r.diffPath
              ? VRTReportGenerator.copyAsset(r.diffPath, assetsDir, "diff")
              : null,
            heatmapPath: r.heatmapPath
              ? VRTReportGenerator.copyAsset(r.heatmapPath, assetsDir, "diff")
              : null,
          };
          return entry;
        });
        summary.totals.total += results.length;
        summary.totals.passed += results.filter(
          (r) => r.passed && r.mismatchPercentage >= 0,
        ).length;
        summary.totals.failed += results.filter((r) => !r.passed).length;
        summary.totals.newSnapshots += results.filter(
          (r) => r.mismatchPercentage < 0,
        ).length;
      },
    );

    const jsonPath = path.join(reportDir, "vrt-results.json");
    fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2));

    // Build HTML report
    const htmlPath = path.join(reportDir, "vrt-report.html");
    fs.writeFileSync(htmlPath, VRTReportGenerator.buildHtml(summary));

    return htmlPath;
  }

  private static copyAsset(
    src: string,
    assetsDir: string,
    subDir: string,
  ): string {
    if (!src || !fs.existsSync(src)) return src;
    const dest = path.join(assetsDir, subDir, path.basename(src));
    fs.copySync(src, dest);
    return `assets/${subDir}/${path.basename(src)}`;
  }

  private static escapeHtml(str: string): string {
    if (!str) return "";
    return str
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");
  }

  private static slugify(str: string): string {
    return str.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase();
  }

  private static buildHtml(summary: any): string {
    const { totals } = summary;

    // Collect all results flat for tab rendering
    const allResults: { scenario: string; r: any }[] = [];
    for (const [scenario, results] of Object.entries(
      summary.scenarios as Record<string, any[]>,
    )) {
      for (const r of results) {
        allResults.push({ scenario, r });
      }
    }

    const failed = allResults.filter((x) => !x.r.passed && x.r.mismatch >= 0);
    const passed = allResults.filter((x) => x.r.passed && x.r.mismatch >= 0);
    const newSnaps = allResults.filter((x) => x.r.mismatch < 0);

    const failedCards = failed
      .map((x) => VRTReportGenerator.renderDiffCard(x.scenario, x.r))
      .join("\n");
    const passedCards = passed
      .map(
        (x) => `
    <div class="card passed-card">
      <div class="card-header">
        <h3>\u2705 ${VRTReportGenerator.escapeHtml(x.r.name)}</h3>
        <div class="card-meta">
          <span class="badge scenario">${VRTReportGenerator.escapeHtml(x.scenario)}</span>
          <span class="badge diff-pct">${x.r.mismatch.toFixed(3)}%</span>
        </div>
      </div>
    </div>`,
      )
      .join("\n");
    const newCards = newSnaps
      .map(
        (x) => `
    <div class="card new-card">
      <div class="card-header">
        <h3>\uD83C\uDD95 ${VRTReportGenerator.escapeHtml(x.r.name)}</h3>
        <div class="card-meta">
          <span class="badge scenario">${VRTReportGenerator.escapeHtml(x.scenario)}</span>
          <span class="badge">No baseline</span>
        </div>
      </div>
      <div class="single-pane">
        <img src="${VRTReportGenerator.escapeHtml(x.r.testPath)}" alt="New snapshot" loading="lazy" onclick="openLightbox(this)"/>
      </div>
    </div>`,
      )
      .join("\n");

    return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>VRT Report \u2014 ${summary.sprint}</title>
  <style>${VRTReportGenerator.getStyles()}</style>
</head>
<body>
  <header>
    <div class="header-content">
      <h1>Visual Regression Report</h1>
      <div class="meta">
        <span class="badge">${summary.sprint}</span>
        <span class="badge env">${summary.mode}</span>
        <span class="timestamp">${new Date(summary.timestamp).toLocaleString()}</span>
      </div>
    </div>
  </header>

  <section class="summary">
    <div class="stat total"><span class="num">${totals.total}</span><span class="label">Total</span></div>
    <div class="stat passed"><span class="num">${totals.passed}</span><span class="label">Passed</span></div>
    <div class="stat failed"><span class="num">${totals.failed}</span><span class="label">Failed</span></div>
    <div class="stat new"><span class="num">${totals.newSnapshots}</span><span class="label">New</span></div>
  </section>

  <nav class="tabs">
    <button class="tab active" data-tab="failures">Failures (${failed.length})</button>
    <button class="tab" data-tab="passed">Passed (${passed.length})</button>
    <button class="tab" data-tab="new">New Snapshots (${newSnaps.length})</button>
  </nav>

  <main>
    <div id="failures" class="tab-panel active">
      ${failed.length === 0 ? '<p class="empty">No failures \u2014 all tests passed!</p>' : ""}
      ${failedCards}
    </div>
    <div id="passed" class="tab-panel">
      ${passedCards}
    </div>
    <div id="new" class="tab-panel">
      ${newCards}
    </div>
  </main>

  <script>${VRTReportGenerator.getScript()}</script>
</body>
</html>`;
  }

  private static renderDiffCard(scenario: string, r: any): string {
    const key = `${scenario}-${r.name}`;
    const slug = VRTReportGenerator.slugify(key);
    const heatmapSrc = r.heatmapPath || r.diffPath || "";
    const regions: ChangeRegion[] = r.changeRegions || [];

    const regionRows = regions
      .map(
        (
          rg: ChangeRegion,
        ) => `<tr class="region-row" data-rx="${rg.bounds.x}" data-ry="${rg.bounds.y}" data-rw="${rg.bounds.width}" data-rh="${rg.bounds.height}" data-severity="${rg.severity}">
        <td><strong>${VRTReportGenerator.escapeHtml(rg.label)}</strong></td>
        <td>${rg.location}</td>
        <td>${rg.bounds.x}, ${rg.bounds.y}</td>
        <td>${rg.bounds.width} \u00d7 ${rg.bounds.height}</td>
        <td>${rg.changedPixels.toLocaleString()} px</td>
        <td class="severity-${rg.severity}">${rg.changePercent}%</td>
        <td>${rg.shareOfTotalDiff}%</td>
        <td class="severity-${rg.severity}">${rg.severity}</td>
      </tr>`,
      )
      .join("\n");

    const sigCount = regions.filter(
      (rg: ChangeRegion) => rg.severity === "significant",
    ).length;
    const modCount = regions.filter(
      (rg: ChangeRegion) => rg.severity === "moderate",
    ).length;
    const minCount = regions.filter(
      (rg: ChangeRegion) => rg.severity === "minor",
    ).length;

    const guidance = VRTReportGenerator.buildGuidance(r, regions);

    return `
    <div class="card failure" id="card-${slug}">
      <div class="card-header">
        <h3>${VRTReportGenerator.escapeHtml(r.name)}</h3>
        <div class="card-meta">
          <span class="badge scenario">${VRTReportGenerator.escapeHtml(scenario)}</span>
          <span class="badge diff-pct">${r.mismatch.toFixed(3)}% diff</span>
          <span class="badge mismatch">${r.mismatchPixels.toLocaleString()} px</span>
        </div>
      </div>

      <div class="three-pane">
        <div class="pane">
          <h4>Baseline</h4>
          <img src="${VRTReportGenerator.escapeHtml(r.baselinePath)}" alt="Baseline" loading="lazy" onclick="openLightbox(this)"/>
        </div>
        <div class="pane">
          <h4>Test</h4>
          <img src="${VRTReportGenerator.escapeHtml(r.testPath)}" alt="Test" loading="lazy" onclick="openLightbox(this)"/>
        </div>
        <div class="pane">
          <h4>Diff Heatmap</h4>
          <div class="heatmap-container" data-imgw="${r.imageWidth || 1440}" data-imgh="${r.imageHeight || 900}">
            <img src="${VRTReportGenerator.escapeHtml(heatmapSrc)}" alt="Diff" loading="lazy" onclick="openLightbox(this)"/>
            <canvas class="heatmap-overlay"></canvas>
          </div>
        </div>
      </div>

      ${
        regions.length > 0
          ? `
      <div class="change-analysis">
        <div class="change-analysis-header">
          <h4>Change Region Analysis</h4>
          <div class="region-summary-badges">
            ${sigCount > 0 ? `<span class="badge severity-significant">${sigCount} significant</span>` : ""}
            ${modCount > 0 ? `<span class="badge severity-moderate">${modCount} moderate</span>` : ""}
            ${minCount > 0 ? `<span class="badge severity-minor">${minCount} minor</span>` : ""}
          </div>
        </div>
        <table class="region-table">
          <thead>
            <tr><th>Region</th><th>Location</th><th>Position (x, y)</th><th>Size</th><th>Changed Pixels</th><th>Density</th><th>Share of Diff</th><th>Severity</th></tr>
          </thead>
          <tbody>${regionRows}</tbody>
        </table>
        <div class="update-guidance">
          <h4>What to Update</h4>
          <ul>${guidance.map((g: string) => `<li>${g}</li>`).join("")}</ul>
        </div>
      </div>`
          : ""
      }

      <div class="approval" data-scenario="${VRTReportGenerator.escapeHtml(scenario)}" data-snapshot="${VRTReportGenerator.escapeHtml(r.name)}">
        <span class="approval-label">Review:</span>
        <button class="btn approve" onclick="submitApproval(this, 'approved')">\u2713 Approve</button>
        <button class="btn reject" onclick="submitApproval(this, 'rejected')">\u2717 Reject</button>
        <input type="text" class="comment-input" placeholder="Comment (optional)" id="comment-${slug}"/>
        <span class="approval-status" id="status-${slug}"></span>
      </div>
      <div class="cli-command" id="cli-${slug}" style="display:none">
        <h5>Run this to update baseline:</h5>
        <pre class="cli-pre"></pre>
        <button class="btn copy-cmd" onclick="copyCliCommand(this)">Copy command</button>
      </div>
    </div>`;
  }

  private static buildGuidance(r: any, regions: ChangeRegion[]): string[] {
    const guidance: string[] = [];
    if (regions.length === 0) return ["No visual changes detected."];

    const significant = regions.filter(
      (rg: ChangeRegion) => rg.severity === "significant",
    );
    const moderate = regions.filter(
      (rg: ChangeRegion) => rg.severity === "moderate",
    );
    const minor = regions.filter((rg: ChangeRegion) => rg.severity === "minor");
    const totalChangedPx = regions.reduce(
      (sum: number, rg: ChangeRegion) => sum + rg.changedPixels,
      0,
    );

    guidance.push(
      `<strong>${regions.length} change region(s)</strong> detected covering <strong>${totalChangedPx.toLocaleString()} px</strong> (${r.mismatch.toFixed(3)}% of page).`,
    );

    for (const rg of significant) {
      guidance.push(
        `<span class="severity-significant">\u25b2 ${VRTReportGenerator.escapeHtml(rg.label)}</span> at <strong>${rg.location}</strong> (${rg.bounds.x},${rg.bounds.y} \u2014 ${rg.bounds.width}\u00d7${rg.bounds.height}px): <strong>${rg.changePercent}%</strong> changed, accounts for <strong>${rg.shareOfTotalDiff}%</strong> of total diff. <em>Likely a layout shift, new/removed element, or content change \u2014 review and update baseline if intentional.</em>`,
      );
    }

    for (const rg of moderate) {
      guidance.push(
        `<span class="severity-moderate">\u25cf ${VRTReportGenerator.escapeHtml(rg.label)}</span> at <strong>${rg.location}</strong> (${rg.bounds.x},${rg.bounds.y} \u2014 ${rg.bounds.width}\u00d7${rg.bounds.height}px): <strong>${rg.changePercent}%</strong> changed. <em>Possible font rendering, colour, or spacing change \u2014 compare visually before updating.</em>`,
      );
    }

    if (minor.length > 0) {
      guidance.push(
        `<span class="severity-minor">\u25cb ${minor.length} minor region(s)</span> with low-density changes \u2014 likely sub-pixel rendering or anti-aliasing differences. <em>Safe to update baseline if no visual impact.</em>`,
      );
    }

    guidance.push(
      "To update: re-run with <code>VRT_MODE=baseline</code> to regenerate baselines, or click <strong>Approve</strong> to record acceptance.",
    );

    return guidance;
  }

  private static getStyles(): string {
    return `
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, 'Segoe UI', Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; line-height: 1.5; }
    header { background: #1d1d1f; color: #fff; padding: 24px 32px; }
    .header-content { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
    h1 { font-size: 22px; font-weight: 600; }
    .meta { display: flex; gap: 8px; align-items: center; }
    .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; background: #333; color: #fff; }
    .badge.env { background: #0a84ff; }
    .badge.scenario { background: #5e5ce6; }
    .badge.diff-pct { background: #ff453a; }
    .badge.mismatch { background: #ff9f0a; color: #000; }
    .timestamp { font-size: 13px; opacity: 0.7; }

    .summary { max-width: 1400px; margin: 24px auto; display: flex; gap: 16px; padding: 0 32px; }
    .stat { background: #fff; border-radius: 12px; padding: 20px; flex: 1; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
    .stat .num { display: block; font-size: 32px; font-weight: 700; }
    .stat .label { font-size: 13px; color: #86868b; text-transform: uppercase; letter-spacing: 0.5px; }
    .stat.passed .num { color: #30d158; }
    .stat.failed .num { color: #ff453a; }
    .stat.new .num { color: #0a84ff; }

    .tabs { max-width: 1400px; margin: 0 auto; padding: 0 32px; display: flex; gap: 4px; border-bottom: 1px solid #d2d2d7; }
    .tab { background: none; border: none; padding: 12px 20px; font-size: 14px; font-weight: 500; cursor: pointer; color: #86868b; border-bottom: 2px solid transparent; transition: all 0.2s; }
    .tab:hover { color: #1d1d1f; }
    .tab.active { color: #0a84ff; border-bottom-color: #0a84ff; }

    main { max-width: 1400px; margin: 24px auto; padding: 0 32px; }
    .tab-panel { display: none; }
    .tab-panel.active { display: block; }
    .empty { text-align: center; padding: 48px; color: #86868b; font-size: 16px; }

    .card { background: #fff; border-radius: 12px; margin-bottom: 20px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
    .card.failure { border-left: 4px solid #ff453a; }
    .card.passed-card { border-left: 4px solid #30d158; }
    .card.new-card { border-left: 4px solid #0a84ff; }
    .card-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; border-bottom: 1px solid #f0f0f2; }
    .card-header h3 { font-size: 16px; font-weight: 600; }
    .card-meta { display: flex; gap: 6px; flex-wrap: wrap; }

    .three-pane { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; background: #e5e5ea; }
    .pane { background: #fff; padding: 12px; }
    .pane h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: #86868b; margin-bottom: 8px; }
    .pane img { width: 100%; border: 1px solid #e5e5ea; border-radius: 4px; cursor: pointer; transition: transform 0.2s; }
    .pane img:hover { transform: scale(1.02); }
    .single-pane { padding: 12px; }
    .single-pane img { max-width: 50%; border: 1px solid #e5e5ea; border-radius: 4px; cursor: pointer; }

    /* Change Analysis */
    .change-analysis { padding: 16px 20px; background: #f0f4ff; border-top: 1px solid #c7d2fe; }
    .change-analysis-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; }
    .change-analysis-header h4 { font-size: 14px; font-weight: 700; color: #1e3a5f; }
    .region-summary-badges { display: flex; gap: 6px; }
    .region-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 16px; }
    .region-table th, .region-table td { padding: 6px 10px; border: 1px solid #d2d2d7; text-align: left; }
    .region-table th { background: #e0e7ff; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.3px; }
    .severity-significant { color: #dc2626; font-weight: 600; }
    .severity-moderate { color: #d97706; font-weight: 600; }
    .severity-minor { color: #65a30d; font-weight: 500; }
    .badge.severity-significant { background: #fecaca; color: #dc2626; }
    .badge.severity-moderate { background: #fef3c7; color: #d97706; }
    .badge.severity-minor { background: #ecfccb; color: #65a30d; }
    .update-guidance { background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px; padding: 12px 16px; }
    .update-guidance h4 { font-size: 14px; font-weight: 700; color: #92400e; margin-bottom: 8px; }
    .update-guidance ul { padding-left: 20px; font-size: 13px; line-height: 1.7; }
    .update-guidance li { margin-bottom: 4px; }
    .update-guidance code { background: #fef3c7; padding: 1px 5px; border-radius: 3px; font-size: 12px; }

    /* Heatmap overlay */
    .heatmap-container { position: relative; display: inline-block; width: 100%; }
    .heatmap-container img { display: block; width: 100%; }
    .heatmap-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
    .region-row { cursor: pointer; transition: background 0.15s; }
    .region-row:hover { background: #e0e7ff; }

    /* Approval */
    .approval { padding: 12px 20px; background: #fafafa; border-top: 1px solid #f0f0f2; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
    .approval-label { font-size: 13px; font-weight: 600; color: #86868b; }
    .btn { padding: 6px 16px; border-radius: 6px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; transition: background 0.2s; }
    .btn.approve { background: #30d158; color: #fff; }
    .btn.approve:hover { background: #28a745; }
    .btn.reject { background: #ff453a; color: #fff; }
    .btn.reject:hover { background: #dc3545; }
    .comment-input { flex: 1; min-width: 200px; padding: 6px 12px; border: 1px solid #d2d2d7; border-radius: 6px; font-size: 13px; }
    .approval-status { font-size: 13px; font-weight: 500; }

    /* CLI command block */
    .cli-command { padding: 10px 20px 14px; background: #1d1d1f; border-top: 1px solid #333; }
    .cli-command h5 { color: #86868b; font-size: 12px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.3px; }
    .cli-pre { background: #2d2d2f; color: #30d158; padding: 10px 14px; border-radius: 6px; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; margin-bottom: 8px; }
    .btn.copy-cmd { background: #5e5ce6; color: #fff; font-size: 12px; padding: 4px 12px; }
    .btn.copy-cmd:hover { background: #4b49b6; }
    .btn.batch-export { background: #f59e0b; color: #000; font-size: 13px; padding: 6px 16px; border-radius: 6px; border: none; cursor: pointer; margin-left: auto; }
    .btn.batch-export:hover { background: #d97706; }
    .approval .btn.approved-state { background: #065f46; color: #fff; opacity: 0.7; cursor: default; }
    .approval .btn.rejected-state { background: #7f1d1d; color: #fff; opacity: 0.7; cursor: default; }

    /* Lightbox */
    .lightbox { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.9); z-index: 1000; justify-content: center; align-items: center; cursor: zoom-out; }
    .lightbox.active { display: flex; }
    .lightbox img { max-width: 95vw; max-height: 95vh; border-radius: 8px; }

    @media (max-width: 900px) {
      .three-pane { grid-template-columns: 1fr; }
      .summary { flex-wrap: wrap; }
    }`;
  }

  private static getScript(): string {
    return `
    // Tab switching
    document.querySelectorAll('.tab').forEach(tab => {
      tab.addEventListener('click', () => {
        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
        document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
        tab.classList.add('active');
        document.getElementById(tab.dataset.tab).classList.add('active');
      });
    });

    // Lightbox
    const lightbox = document.createElement('div');
    lightbox.className = 'lightbox';
    lightbox.innerHTML = '<img/>';
    document.body.appendChild(lightbox);
    lightbox.addEventListener('click', () => lightbox.classList.remove('active'));

    function openLightbox(img) {
      lightbox.querySelector('img').src = img.src;
      lightbox.classList.add('active');
    }

    // Region highlight on heatmap
    function initRegionHighlights() {
      document.querySelectorAll('.card.failure').forEach(card => {
        const container = card.querySelector('.heatmap-container');
        const canvas = card.querySelector('.heatmap-overlay');
        const img = container?.querySelector('img');
        const rows = card.querySelectorAll('.region-row');
        if (!container || !canvas || !img || rows.length === 0) return;

        const imgW = parseInt(container.dataset.imgw) || 1440;
        const imgH = parseInt(container.dataset.imgh) || 900;

        function sizeCanvas() {
          canvas.width = img.clientWidth;
          canvas.height = img.clientHeight;
        }
        img.addEventListener('load', sizeCanvas);
        if (img.complete) sizeCanvas();
        new ResizeObserver(sizeCanvas).observe(img);

        rows.forEach(row => {
          row.addEventListener('mouseenter', () => drawHighlight(row, canvas, imgW, imgH));
          row.addEventListener('mouseleave', () => clearHighlight(canvas));
        });
      });
    }

    function drawHighlight(row, canvas, imgW, imgH) {
      const ctx = canvas.getContext('2d');
      if (!ctx) return;
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      const scaleX = canvas.width / imgW;
      const scaleY = canvas.height / imgH;

      const rx = parseInt(row.dataset.rx) * scaleX;
      const ry = parseInt(row.dataset.ry) * scaleY;
      const rw = parseInt(row.dataset.rw) * scaleX;
      const rh = parseInt(row.dataset.rh) * scaleY;
      const severity = row.dataset.severity;

      const colors = { significant: 'rgba(220,38,38,0.35)', moderate: 'rgba(217,119,6,0.3)', minor: 'rgba(59,130,246,0.25)' };
      const borders = { significant: '#dc2626', moderate: '#d97706', minor: '#3b82f6' };

      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.clearRect(rx, ry, rw, rh);

      ctx.fillStyle = colors[severity] || colors.minor;
      ctx.fillRect(rx, ry, rw, rh);

      ctx.strokeStyle = borders[severity] || borders.minor;
      ctx.lineWidth = 2;
      ctx.setLineDash([6, 3]);
      ctx.strokeRect(rx, ry, rw, rh);

      const label = row.querySelector('td strong')?.textContent || '';
      ctx.setLineDash([]);
      ctx.font = 'bold 12px -apple-system, sans-serif';
      ctx.fillStyle = '#fff';
      ctx.strokeStyle = 'rgba(0,0,0,0.7)';
      ctx.lineWidth = 3;
      ctx.strokeText(label, rx + 4, ry - 4 > 14 ? ry - 4 : ry + 14);
      ctx.fillText(label, rx + 4, ry - 4 > 14 ? ry - 4 : ry + 14);
    }

    function clearHighlight(canvas) {
      const ctx = canvas.getContext('2d');
      if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    // Approval
    function submitApproval(btn, status) {
      const approvalDiv = btn.closest('.approval');
      const scenario = approvalDiv.dataset.scenario;
      const snapshot = approvalDiv.dataset.snapshot;
      const slug = (scenario + '-' + snapshot).replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
      const comment = document.getElementById('comment-' + slug)?.value || '';
      const by = prompt('Your name:');
      if (!by) return;

      const record = { scenario, snapshot, status, comment, by, at: new Date().toISOString() };

      const approvals = JSON.parse(localStorage.getItem('vrt-approvals') || '{}');
      approvals[slug] = record;
      localStorage.setItem('vrt-approvals', JSON.stringify(approvals));

      const statusEl = document.getElementById('status-' + slug);
      if (statusEl) {
        statusEl.textContent = status === 'approved' ? '\\u2713 Approved by ' + by : '\\u2717 Rejected by ' + by;
        statusEl.style.color = status === 'approved' ? '#30d158' : '#ff453a';
      }

      approvalDiv.querySelectorAll('.btn.approve, .btn.reject').forEach(b => {
        b.classList.add(status === 'approved' ? 'approved-state' : 'rejected-state');
        b.disabled = true;
      });

      const cliDiv = document.getElementById('cli-' + slug);
      if (cliDiv) {
        var cmd = 'VRT_MODE=baseline npx cucumber-js --name \\'' + scenario + '\\' # Re-run scenario to update baseline for: ' + snapshot;
        cliDiv.querySelector('.cli-pre').textContent = cmd;
        cliDiv.style.display = 'block';
      }

      updateBatchManifest();
    }

    function updateBatchManifest() {
      const approvals = JSON.parse(localStorage.getItem('vrt-approvals') || '{}');
      const entries = Object.values(approvals);
      if (entries.length === 0) return;

      let script = '#!/bin/bash\\\\n# VRT Approval Batch\\\\nset -e\\\\n\\\\n';
      entries.forEach(function(r) {
        script += '# ' + r.status.toUpperCase() + ': ' + r.snapshot + ' (' + r.scenario + ')\\\\n';
      });
      script += '\\\\n# Re-run all scenarios in baseline mode:\\\\n';
      var scenarios = [...new Set(entries.map(function(r) { return r.scenario; }))];
      scenarios.forEach(function(s) {
        script += 'VRT_MODE=baseline npx cucumber-js --name \\'' + s + '\\'\\\\n';
      });

      localStorage.setItem('vrt-batch-script', script);

      let batchBtn = document.getElementById('batch-export-btn');
      if (!batchBtn) {
        batchBtn = document.createElement('button');
        batchBtn.id = 'batch-export-btn';
        batchBtn.className = 'btn batch-export';
        batchBtn.textContent = 'Export All Approvals (' + entries.length + ')';
        batchBtn.onclick = exportBatchScript;
        document.querySelector('header .header-content')?.appendChild(batchBtn);
      } else {
        batchBtn.textContent = 'Export All Approvals (' + entries.length + ')';
      }
    }

    function exportBatchScript() {
      const script = localStorage.getItem('vrt-batch-script') || '';
      const blob = new Blob([script], { type: 'text/x-shellscript' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'vrt-approve-batch.sh';
      a.click();
      URL.revokeObjectURL(a.href);
    }

    function copyCliCommand(btn) {
      const pre = btn.previousElementSibling;
      if (!pre) return;
      navigator.clipboard.writeText(pre.textContent).then(() => {
        btn.textContent = 'Copied!';
        setTimeout(() => btn.textContent = 'Copy command', 1500);
      });
    }

    initRegionHighlights();
    updateBatchManifest();`;
  }
}
