/**
 * Multi-viewport visual parity comparison: Angular vs React.
 *
 * Captures full-page screenshots at 3 viewports (desktop, tablet, mobile)
 * for each route, generates pixel-diff images, and outputs a structured
 * JSON report for the comparison report pipeline.
 *
 * Usage:
 *   npx tsx tests/parity/visual/screenshot-diff-multi.ts
 *
 * Prerequisites:
 *   - Angular running on http://localhost:4200
 *   - React running on http://localhost:8080
 *   - Playwright browsers installed (npx playwright install chromium)
 */

import { chromium, type Page } from "@playwright/test";
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve, join } from "node:path";
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";
import { mockAngularAPIs } from "../../e2e-angular/support/angular-api-mock.js";

// ---------------------------------------------------------------------------
// Exported types (consumed by report generators)
// ---------------------------------------------------------------------------

export interface ScreenshotResult {
  route: string;
  viewport: string;
  angularPath: string;
  reactPath: string;
  diffPath: string;
  mismatchPct: number;
  mismatchCount: number;
  totalPixels: number;
  heightDiff: number;
  angularDimensions: { width: number; height: number };
  reactDimensions: { width: number; height: number };
  error?: string;
}

export interface MultiViewportReport {
  timestamp: string;
  viewports: Record<string, { width: number; height: number }>;
  results: ScreenshotResult[];
}

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

const ANGULAR_BASE = "http://localhost:4200";
const REACT_BASE = "http://localhost:8080";
const REPORT_DIR = resolve(import.meta.dirname, "../../../comparison-report/visual");
const SCREENSHOTS_DIR = join(REPORT_DIR, "screenshots");
const DIFFS_DIR = join(REPORT_DIR, "diffs");

const VIEWPORTS = {
  desktop: { width: 1440, height: 900 },
  tablet: { width: 768, height: 1024 },
  mobile: { width: 375, height: 812 },
} as const;

/** Wait for network idle + extra settle time for animations. */
const SETTLE_MS = 1500;

interface RouteEntry {
  name: string;
  angular: string;
  react: string;
  /** Extra wait time in ms for pages that load data. */
  waitMs?: number;
  /** Scroll to capture full page? Default true. */
  fullPage?: boolean;
  /** CSS selectors of elements to mask (hide) before capture — use for dynamic content. */
  maskSelectors?: string[];
}

const ROUTES: RouteEntry[] = [
  {
    name: "onlineboard-start",
    angular: "/",
    react: "/ru/onlineboard",
  },
  {
    name: "onlineboard-departure",
    angular: "/onlineboard/departure/AAQ/16042026-0000-2400",
    react: "/ru/onlineboard/departure/AAQ/16042026-0000-2400",
    waitMs: 3000,
  },
  {
    name: "onlineboard-arrival",
    angular: "/onlineboard/arrival/AAQ/16042026-0000-2400",
    react: "/ru/onlineboard/arrival/AAQ/16042026-0000-2400",
    waitMs: 3000,
  },
  {
    name: "onlineboard-route",
    angular: "/onlineboard/route/MOW-AER/16042026-0000-2400",
    react: "/ru/onlineboard/route/MOW-AER/16042026-0000-2400",
    waitMs: 3000,
  },
  {
    name: "onlineboard-flight",
    angular: "/onlineboard/flight/SU0022/16042026",
    react: "/ru/onlineboard/flight/SU0022/16042026",
    waitMs: 3000,
  },
  {
    name: "onlineboard-details",
    angular: "/onlineboard/SU0022-16042026",
    react: "/ru/onlineboard/SU0022-16042026",
    waitMs: 3000,
  },
  {
    name: "schedule-start",
    angular: "/schedule",
    react: "/ru/schedule",
  },
  {
    name: "schedule-route",
    angular: "/schedule/route/MOW-KUF-20260416-20260423",
    react: "/ru/schedule/route/MOW-KUF-20260416-20260423",
    waitMs: 3000,
  },
  {
    name: "flights-map",
    angular: "/flights-map",
    react: "/ru/flights-map",
    waitMs: 2000,
  },
  {
    name: "error-404",
    angular: "/error/404",
    react: "/error/404",
  },
];

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

async function applyMasks(page: Page, selectors: string[]): Promise<void> {
  if (selectors.length === 0) return;
  await page.evaluate((sels) => {
    for (const sel of sels) {
      document.querySelectorAll(sel).forEach((el) => {
        (el as HTMLElement).style.visibility = "hidden";
      });
    }
  }, selectors);
}

async function captureScreenshot(
  page: Page,
  url: string,
  waitMs: number,
  fullPage: boolean,
  maskSelectors: string[],
): Promise<Buffer> {
  await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
  await page.waitForTimeout(waitMs);
  await applyMasks(page, maskSelectors);
  return page.screenshot({ fullPage, type: "png" }) as Promise<Buffer>;
}

function alignAndDiff(
  angularBuf: Buffer,
  reactBuf: Buffer,
): { diffPng: PNG; mismatchCount: number; totalPixels: number; width: number; height: number } {
  const angularPng = PNG.sync.read(angularBuf);
  const reactPng = PNG.sync.read(reactBuf);

  // Use the larger dimensions to avoid clipping
  const width = Math.max(angularPng.width, reactPng.width);
  const height = Math.max(angularPng.height, reactPng.height);

  // Create canvases of uniform size, fill with white
  const padded = (src: PNG): Buffer => {
    const out = new PNG({ width, height });
    // Fill white
    for (let i = 0; i < out.data.length; i += 4) {
      out.data[i] = 255;
      out.data[i + 1] = 255;
      out.data[i + 2] = 255;
      out.data[i + 3] = 255;
    }
    // Copy source pixels
    for (let y = 0; y < src.height; y++) {
      for (let x = 0; x < src.width; x++) {
        const srcIdx = (y * src.width + x) * 4;
        const dstIdx = (y * width + x) * 4;
        out.data[dstIdx] = src.data[srcIdx]!;
        out.data[dstIdx + 1] = src.data[srcIdx + 1]!;
        out.data[dstIdx + 2] = src.data[srcIdx + 2]!;
        out.data[dstIdx + 3] = src.data[srcIdx + 3]!;
      }
    }
    return PNG.sync.write(out);
  };

  const aPadded = PNG.sync.read(padded(angularPng));
  const rPadded = PNG.sync.read(padded(reactPng));
  const diffPng = new PNG({ width, height });

  const mismatchCount = pixelmatch(
    aPadded.data,
    rPadded.data,
    diffPng.data,
    width,
    height,
    { threshold: 0.1, alpha: 0.3 },
  );

  return { diffPng, mismatchCount, totalPixels: width * height, width, height };
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
  // Create output directories
  for (const app of ["angular", "react"] as const) {
    mkdirSync(join(SCREENSHOTS_DIR, app), { recursive: true });
  }
  mkdirSync(DIFFS_DIR, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const results: ScreenshotResult[] = [];

  for (const [vpName, vpSize] of Object.entries(VIEWPORTS)) {
    console.log(`\n${"=".repeat(60)}`);
    console.log(`VIEWPORT: ${vpName} (${vpSize.width}x${vpSize.height})`);
    console.log("=".repeat(60));

    const angularCtx = await browser.newContext({ viewport: vpSize });
    const reactCtx = await browser.newContext({ viewport: vpSize });
    const angularPage = await angularCtx.newPage();
    const reactPage = await reactCtx.newPage();

    // Mock API endpoints for both apps (WAF blocks real API in dev)
    await mockAngularAPIs(angularPage);
    await mockAngularAPIs(reactPage);

    // Override popular requests mock for React (React uses different field names)
    await reactPage.route("**/Requests/*/getpopular", (route) => {
      route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify([
          { mode: "FlightNumber", carrier: "SU", flightNumber: "0654", type: "Onlineboard" },
          { mode: "Route", departure: "LED", arrival: "KRR", type: "Onlineboard" },
          { mode: "Route", departure: "VKO", arrival: "KUF", type: "Onlineboard" },
          { mode: "Arrival", arrival: "VKO", type: "Onlineboard" },
        ]),
      });
    });

    for (const route of ROUTES) {
      const fileName = `${route.name}-${vpName}.png`;
      const angularPath = join(SCREENSHOTS_DIR, "angular", fileName);
      const reactPath = join(SCREENSHOTS_DIR, "react", fileName);
      const diffPath = join(DIFFS_DIR, fileName);
      const waitMs = route.waitMs ?? SETTLE_MS;
      const fullPage = route.fullPage !== false;
      const maskSelectors = route.maskSelectors ?? [];

      console.log(`\n  ${route.name}`);
      console.log(`    Angular: ${ANGULAR_BASE}${route.angular}`);
      console.log(`    React:   ${REACT_BASE}${route.react}`);

      try {
        const [angularBuf, reactBuf] = await Promise.all([
          captureScreenshot(angularPage, `${ANGULAR_BASE}${route.angular}`, waitMs, fullPage, maskSelectors),
          captureScreenshot(reactPage, `${REACT_BASE}${route.react}`, waitMs, fullPage, maskSelectors),
        ]);

        writeFileSync(angularPath, angularBuf);
        writeFileSync(reactPath, reactBuf);

        const { diffPng, mismatchCount, totalPixels } = alignAndDiff(angularBuf, reactBuf);
        const diffBuf = PNG.sync.write(diffPng);
        writeFileSync(diffPath, diffBuf);

        const angularPng = PNG.sync.read(angularBuf);
        const reactPng = PNG.sync.read(reactBuf);
        const heightDiff = reactPng.height - angularPng.height;
        const mismatchPct = (mismatchCount / totalPixels) * 100;

        results.push({
          route: route.name,
          viewport: vpName,
          angularPath,
          reactPath,
          diffPath,
          mismatchPct,
          mismatchCount,
          totalPixels,
          heightDiff,
          angularDimensions: { width: angularPng.width, height: angularPng.height },
          reactDimensions: { width: reactPng.width, height: reactPng.height },
        });

        console.log(`    ${mismatchPct.toFixed(2)}% diff (${mismatchCount.toLocaleString()} px), height delta=${heightDiff}px`);
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        results.push({
          route: route.name,
          viewport: vpName,
          angularPath,
          reactPath,
          diffPath,
          mismatchPct: -1,
          mismatchCount: -1,
          totalPixels: -1,
          heightDiff: 0,
          angularDimensions: { width: 0, height: 0 },
          reactDimensions: { width: 0, height: 0 },
          error: msg,
        });
        console.log(`    ERROR: ${msg}`);
      }
    }

    await angularCtx.close();
    await reactCtx.close();
  }

  // ---------------------------------------------------------------------------
  // Summary table
  // ---------------------------------------------------------------------------

  console.log("\n" + "=".repeat(85));
  console.log("MULTI-VIEWPORT VISUAL PARITY SUMMARY");
  console.log("=".repeat(85));
  console.log(
    "Route".padEnd(28) +
    "Viewport".padEnd(10) +
    "Diff %".padEnd(10) +
    "Mismatch px".padEnd(15) +
    "Height delta".padEnd(14) +
    "Status",
  );
  console.log("-".repeat(85));

  for (const r of results) {
    const pct = r.error ? "ERR" : r.mismatchPct.toFixed(2);
    const status = r.error
      ? `FAIL ${r.error.slice(0, 25)}`
      : r.mismatchPct < 0.5
        ? "PASS"
        : "DIFF";
    console.log(
      r.route.padEnd(28) +
      r.viewport.padEnd(10) +
      pct.padEnd(10) +
      (r.mismatchCount >= 0 ? r.mismatchCount.toLocaleString() : "N/A").padEnd(15) +
      `${r.heightDiff}px`.padEnd(14) +
      status,
    );
  }
  console.log("=".repeat(85));

  // ---------------------------------------------------------------------------
  // JSON report
  // ---------------------------------------------------------------------------

  const report: MultiViewportReport = {
    timestamp: new Date().toISOString(),
    viewports: { ...VIEWPORTS },
    results,
  };

  const reportPath = join(REPORT_DIR, "report.json");
  writeFileSync(reportPath, JSON.stringify(report, null, 2));
  console.log(`\nReport:      ${reportPath}`);
  console.log(`Screenshots: ${SCREENSHOTS_DIR}/`);
  console.log(`Diffs:       ${DIFFS_DIR}/`);

  await browser.close();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
