/**
 * Gap analysis: Angular vs React DOM structure comparison.
 *
 * Crawls both apps per route, counts DOM elements matching CSS selectors,
 * compares counts, and generates a Markdown + JSON gap report.
 *
 * Usage:
 *   npx tsx tests/parity/gap/gap-analysis.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 { mockAngularAPIs } from "../../e2e-angular/support/angular-api-mock.js";

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

const ANGULAR_BASE = "http://localhost:4200";
const REACT_BASE = "http://localhost:8080";
const OUTPUT_DIR = resolve(
  import.meta.dirname,
  "../../../comparison-report/gaps",
);
const VIEWPORT = { width: 1440, height: 900 };
const DEFAULT_WAIT_MS = 1500;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface SectionDef {
  name: string;
  /** CSS selector — broad enough to match both Angular and React DOM */
  selector: string;
  /** If true, this section is a known gap (already tracked, not a surprise) */
  knownGap?: boolean;
}

interface RouteConfig {
  name: string;
  angular: string;
  react: string;
  waitMs?: number;
  sections: SectionDef[];
}

type GapStatus =
  | "match"
  | "missing-in-react"
  | "extra-in-react"
  | "count-diff"
  | "both-zero";

interface SectionResult {
  section: string;
  selector: string;
  angularCount: number;
  reactCount: number;
  status: GapStatus;
  knownGap: boolean;
}

interface ApiCallSet {
  angular: string[];
  react: string[];
  angularOnly: string[];
  reactOnly: string[];
}

interface RouteResult {
  route: string;
  sections: SectionResult[];
  apiCalls: ApiCallSet;
}

// ---------------------------------------------------------------------------
// Route definitions
// ---------------------------------------------------------------------------

const ROUTES: RouteConfig[] = [
  {
    name: "onlineboard-start",
    angular: "/",
    react: "/ru/onlineboard",
    sections: [
      { name: "page-tabs", selector: '[data-testid*="tab"], [data-testid*="Tab"], .p-tabview-nav li, [role="tablist"] [role="tab"]' },
      { name: "filter-accordion", selector: '[data-testid*="filter"], [data-testid*="Filter"], .p-accordion, [data-testid*="accordion"]' },
      { name: "popular-requests", selector: '[data-testid*="popular"], [data-testid*="Popular"], [class*="popular"]' },
      { name: "search-history", selector: '[data-testid*="history"], [data-testid*="History"], [class*="history"]' },
      { name: "breadcrumbs", selector: '[data-testid*="breadcrumb"], [data-testid*="Breadcrumb"], nav[aria-label*="breadcrumb"], .breadcrumb' },
      { name: "feedback-button", selector: '[data-testid*="feedback"], [data-testid*="Feedback"], [class*="feedback"]' },
    ],
  },
  {
    name: "onlineboard-details",
    angular: "/onlineboard/SU0022-16042026",
    react: "/ru/onlineboard/SU0022-16042026",
    waitMs: 3000,
    sections: [
      { name: "flight-number-header", selector: '[data-testid*="flight-number"], [data-testid*="flightNumber"], [class*="flight-number"], h1, h2' },
      { name: "status-badge", selector: '[data-testid*="status"], [data-testid*="Status"], [class*="status-badge"], [class*="statusBadge"]' },
      { name: "departure-station", selector: '[data-testid*="departure"], [data-testid*="Departure"], [class*="departure"]' },
      { name: "arrival-station", selector: '[data-testid*="arrival"], [data-testid*="Arrival"], [class*="arrival"]' },
      { name: "aircraft-info", selector: '[data-testid*="aircraft"], [data-testid*="Aircraft"], [class*="aircraft"]' },
      { name: "accordion-panels", selector: '[data-testid*="accordion"], .p-accordion-tab, [class*="accordion"]', knownGap: true },
      { name: "mini-flight-list", selector: '[data-testid*="mini-flight"], [data-testid*="miniFlight"], [class*="mini-flight"]', knownGap: true },
      { name: "day-tabs", selector: '[data-testid*="day-tab"], [data-testid*="dayTab"], [class*="day-tab"]', knownGap: true },
      { name: "back-button", selector: '[data-testid*="back"], [data-testid*="Back"], [class*="back-btn"], a[href*="back"]', knownGap: true },
      { name: "flight-actions", selector: '[data-testid*="action"], [data-testid*="Action"], [class*="flight-action"]', knownGap: true },
      { name: "boarding-info", selector: '[data-testid*="boarding"], [data-testid*="Boarding"], [class*="boarding"]', knownGap: true },
      { name: "meal-services", selector: '[data-testid*="meal"], [data-testid*="Meal"], [class*="meal"]', knownGap: true },
      { name: "transfer-section", selector: '[data-testid*="transfer"], [data-testid*="Transfer"], [class*="transfer"]', knownGap: true },
      { name: "last-update", selector: '[data-testid*="last-update"], [data-testid*="lastUpdate"], [class*="last-update"]', knownGap: true },
      { name: "operator-logo", selector: '[data-testid*="operator"], [data-testid*="Operator"], [class*="operator-logo"]', knownGap: true },
    ],
  },
  {
    name: "schedule-start",
    angular: "/schedule",
    react: "/ru/schedule",
    sections: [
      { name: "departure-input", selector: '[data-testid*="departure"], [data-testid*="Departure"], input[placeholder*="departure" i], input[placeholder*="departure" i], [class*="departure"] input' },
      { name: "arrival-input", selector: '[data-testid*="arrival-input"], [data-testid*="arrivalInput"], input[placeholder*="arrival" i], [class*="arrival"] input' },
      { name: "calendar", selector: '[data-testid*="calendar"], [data-testid*="Calendar"], .p-calendar, [class*="calendar"], [data-testid*="date"]' },
      { name: "direct-only-checkbox", selector: '[data-testid*="direct"], [data-testid*="Direct"], [class*="direct"] input[type="checkbox"], [class*="direct"]' },
      { name: "return-checkbox", selector: '[data-testid*="return"], [data-testid*="Return"], [class*="return"] input[type="checkbox"], [class*="return-date"]' },
      { name: "search-button", selector: '[data-testid*="search"], [data-testid*="Search"], button[type="submit"], [class*="search"] button' },
      { name: "popular-requests", selector: '[data-testid*="popular"], [data-testid*="Popular"], [class*="popular"]' },
    ],
  },
  {
    name: "flights-map",
    angular: "/flights-map",
    react: "/ru/flights-map",
    waitMs: 3000,
    sections: [
      { name: "map-container", selector: ".leaflet-container" },
      { name: "filter-departure", selector: '[data-testid*="departure"], [data-testid*="Departure"], [class*="departure"] input, [class*="filter"] [class*="departure"]' },
      { name: "filter-arrival", selector: '[data-testid*="arrival-filter"], [data-testid*="arrivalFilter"], [class*="arrival"] input, [class*="filter"] [class*="arrival"]' },
      { name: "domestic-toggle", selector: '[data-testid*="domestic"], [data-testid*="Domestic"], [class*="domestic"]' },
      { name: "international-toggle", selector: '[data-testid*="international"], [data-testid*="International"], [class*="international"]' },
      { name: "connecting-toggle", selector: '[data-testid*="connecting"], [data-testid*="Connecting"], [class*="connecting"]' },
      { name: "map-markers", selector: ".leaflet-marker-icon", knownGap: true },
      { name: "map-polylines", selector: ".leaflet-interactive", knownGap: true },
    ],
  },
];

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

function classifyGap(
  angularCount: number,
  reactCount: number,
): GapStatus {
  if (angularCount === 0 && reactCount === 0) return "both-zero";
  if (angularCount === reactCount) return "match";
  if (angularCount > 0 && reactCount === 0) return "missing-in-react";
  if (angularCount === 0 && reactCount > 0) return "extra-in-react";
  return "count-diff";
}

async function countElements(page: Page, selector: string): Promise<number> {
  return page.locator(selector).count();
}

/** Normalize API URL to a comparable path (strip host, query params, locale prefix). */
function normalizeApiPath(url: string): string {
  try {
    const u = new URL(url);
    return u.pathname.replace(/\/(ru|en)\//g, "/*/");
  } catch {
    return url;
  }
}

function collectApiCalls(urls: string[]): string[] {
  return [...new Set(urls.map(normalizeApiPath))].sort();
}

// ---------------------------------------------------------------------------
// Report generation
// ---------------------------------------------------------------------------

function generateMarkdown(
  results: RouteResult[],
  timestamp: string,
): string {
  // Aggregate stats
  let totalSections = 0;
  let matching = 0;
  let missingNew = 0;
  let missingKnown = 0;
  let countDiffs = 0;
  let extraInReact = 0;
  let bothZero = 0;

  for (const r of results) {
    for (const s of r.sections) {
      totalSections++;
      switch (s.status) {
        case "match":
          matching++;
          break;
        case "missing-in-react":
          if (s.knownGap) missingKnown++;
          else missingNew++;
          break;
        case "extra-in-react":
          extraInReact++;
          break;
        case "count-diff":
          countDiffs++;
          break;
        case "both-zero":
          bothZero++;
          break;
      }
    }
  }

  const lines: string[] = [];
  const ln = (s = "") => lines.push(s);

  ln("# Gap Analysis Report");
  ln(`Generated: ${timestamp}`);
  ln();
  ln("## Summary");
  ln(`- Total sections checked: ${totalSections}`);
  ln(`- Matching: ${matching}`);
  ln(`- Both zero (no elements in either): ${bothZero}`);
  ln(`- Missing in React (NEW): ${missingNew}`);
  ln(`- Missing in React (known): ${missingKnown}`);
  ln(`- Count differences: ${countDiffs}`);
  ln(`- Extra in React: ${extraInReact}`);
  ln();

  // NEW gaps
  const newGaps = results.flatMap((r) =>
    r.sections
      .filter((s) => s.status === "missing-in-react" && !s.knownGap)
      .map((s) => ({ route: r.route, ...s })),
  );
  ln("## NEW Gaps (unexpected)");
  if (newGaps.length === 0) {
    ln("None -- all missing sections are accounted for in known gaps.");
  } else {
    ln("| Route | Section | Angular | React |");
    ln("|-------|---------|---------|-------|");
    for (const g of newGaps) {
      ln(`| ${g.route} | ${g.section} | ${g.angularCount} | ${g.reactCount} |`);
    }
  }
  ln();

  // Known gaps
  const knownGaps = results.flatMap((r) =>
    r.sections
      .filter((s) => s.knownGap && s.status === "missing-in-react")
      .map((s) => ({ route: r.route, ...s })),
  );
  ln("## Known Gaps");
  if (knownGaps.length === 0) {
    ln("None -- all known gaps have been resolved.");
  } else {
    ln("| Route | Section | Angular | React |");
    ln("|-------|---------|---------|-------|");
    for (const g of knownGaps) {
      ln(`| ${g.route} | ${g.section} | ${g.angularCount} | ${g.reactCount} |`);
    }
  }
  ln();

  // Count differences
  const diffs = results.flatMap((r) =>
    r.sections
      .filter((s) => s.status === "count-diff")
      .map((s) => ({ route: r.route, ...s })),
  );
  ln("## Count Differences");
  if (diffs.length === 0) {
    ln("None -- element counts match where both apps render content.");
  } else {
    ln("| Route | Section | Angular | React |");
    ln("|-------|---------|---------|-------|");
    for (const d of diffs) {
      ln(`| ${d.route} | ${d.section} | ${d.angularCount} | ${d.reactCount} |`);
    }
  }
  ln();

  // Extra in React
  const extras = results.flatMap((r) =>
    r.sections
      .filter((s) => s.status === "extra-in-react")
      .map((s) => ({ route: r.route, ...s })),
  );
  if (extras.length > 0) {
    ln("## Extra in React");
    ln("| Route | Section | Angular | React |");
    ln("|-------|---------|---------|-------|");
    for (const e of extras) {
      ln(`| ${e.route} | ${e.section} | ${e.angularCount} | ${e.reactCount} |`);
    }
    ln();
  }

  // API call comparison
  ln("## API Call Comparison");
  for (const r of results) {
    ln(`### ${r.route}`);
    if (r.apiCalls.angularOnly.length > 0) {
      ln(`Angular-only API calls:`);
      for (const c of r.apiCalls.angularOnly) ln(`- \`${c}\``);
    } else {
      ln(`Angular-only API calls: none`);
    }
    if (r.apiCalls.reactOnly.length > 0) {
      ln(`React-only API calls:`);
      for (const c of r.apiCalls.reactOnly) ln(`- \`${c}\``);
    } else {
      ln(`React-only API calls: none`);
    }
    ln();
  }

  return lines.join("\n");
}

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

async function main() {
  mkdirSync(OUTPUT_DIR, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const angularCtx = await browser.newContext({ viewport: VIEWPORT });
  const reactCtx = await browser.newContext({ viewport: VIEWPORT });
  const angularPage = await angularCtx.newPage();
  const reactPage = await reactCtx.newPage();

  // Mock API endpoints for both apps
  await mockAngularAPIs(angularPage);
  await mockAngularAPIs(reactPage);

  // Override popular requests mock for React (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",
        },
      ]),
    });
  });

  const allResults: RouteResult[] = [];

  for (const route of ROUTES) {
    const waitMs = route.waitMs ?? DEFAULT_WAIT_MS;
    console.log(`\n--- ${route.name} ---`);
    console.log(`  Angular: ${ANGULAR_BASE}${route.angular}`);
    console.log(`  React:   ${REACT_BASE}${route.react}`);

    // Track API calls per app
    const angularApiCalls: string[] = [];
    const reactApiCalls: string[] = [];

    const angularListener = (request: { url: () => string }) => {
      const url = request.url();
      if (url.includes("/api/") || url.includes("/Api/")) {
        angularApiCalls.push(url);
      }
    };
    const reactListener = (request: { url: () => string }) => {
      const url = request.url();
      if (url.includes("/api/") || url.includes("/Api/")) {
        reactApiCalls.push(url);
      }
    };

    angularPage.on("request", angularListener);
    reactPage.on("request", reactListener);

    try {
      // Navigate both pages
      await Promise.all([
        angularPage.goto(`${ANGULAR_BASE}${route.angular}`, {
          waitUntil: "networkidle",
          timeout: 30_000,
        }),
        reactPage.goto(`${REACT_BASE}${route.react}`, {
          waitUntil: "networkidle",
          timeout: 30_000,
        }),
      ]);

      // Extra settle time
      await Promise.all([
        angularPage.waitForTimeout(waitMs),
        reactPage.waitForTimeout(waitMs),
      ]);

      // Count elements for each section
      const sectionResults: SectionResult[] = [];

      for (const section of route.sections) {
        const [angularCount, reactCount] = await Promise.all([
          countElements(angularPage, section.selector),
          countElements(reactPage, section.selector),
        ]);

        const status = classifyGap(angularCount, reactCount);
        const knownGap = section.knownGap ?? false;

        sectionResults.push({
          section: section.name,
          selector: section.selector,
          angularCount,
          reactCount,
          status,
          knownGap,
        });

        const icon =
          status === "match"
            ? "[OK]"
            : status === "both-zero"
              ? "[--]"
              : knownGap
                ? "[KG]"
                : "[!!]";
        console.log(
          `  ${icon} ${section.name}: angular=${angularCount} react=${reactCount} (${status})`,
        );
      }

      // Compute API call diff
      const angularNorm = collectApiCalls(angularApiCalls);
      const reactNorm = collectApiCalls(reactApiCalls);
      const angularOnly = angularNorm.filter((c) => !reactNorm.includes(c));
      const reactOnly = reactNorm.filter((c) => !angularNorm.includes(c));

      allResults.push({
        route: route.name,
        sections: sectionResults,
        apiCalls: {
          angular: angularNorm,
          react: reactNorm,
          angularOnly,
          reactOnly,
        },
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      console.log(`  ERROR: ${msg}`);
      allResults.push({
        route: route.name,
        sections: route.sections.map((s) => ({
          section: s.name,
          selector: s.selector,
          angularCount: -1,
          reactCount: -1,
          status: "both-zero" as GapStatus,
          knownGap: s.knownGap ?? false,
        })),
        apiCalls: {
          angular: [],
          react: [],
          angularOnly: [],
          reactOnly: [],
        },
      });
    } finally {
      angularPage.removeListener("request", angularListener);
      reactPage.removeListener("request", reactListener);
    }
  }

  // Generate reports
  const timestamp = new Date().toISOString();

  const markdown = generateMarkdown(allResults, timestamp);
  writeFileSync(join(OUTPUT_DIR, "gap-report.md"), markdown);
  console.log(`\nMarkdown report: ${join(OUTPUT_DIR, "gap-report.md")}`);

  const jsonData = { timestamp, viewport: VIEWPORT, results: allResults };
  writeFileSync(
    join(OUTPUT_DIR, "gap-data.json"),
    JSON.stringify(jsonData, null, 2),
  );
  console.log(`JSON data: ${join(OUTPUT_DIR, "gap-data.json")}`);

  // Print summary to console
  console.log("\n" + "=".repeat(60));
  console.log("GAP ANALYSIS SUMMARY");
  console.log("=".repeat(60));

  let total = 0,
    matches = 0,
    newGaps = 0,
    knownGaps = 0,
    diffs = 0;
  for (const r of allResults) {
    for (const s of r.sections) {
      total++;
      if (s.status === "match") matches++;
      else if (s.status === "missing-in-react" && !s.knownGap) newGaps++;
      else if (s.status === "missing-in-react" && s.knownGap) knownGaps++;
      else if (s.status === "count-diff") diffs++;
    }
  }

  console.log(`Total sections: ${total}`);
  console.log(`Matching:       ${matches}`);
  console.log(`NEW gaps:       ${newGaps}`);
  console.log(`Known gaps:     ${knownGaps}`);
  console.log(`Count diffs:    ${diffs}`);
  console.log("=".repeat(60));

  await browser.close();
}

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