import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { chromium } from "playwright";

const here = dirname(fileURLToPath(import.meta.url));
const outputDir = join(here, "outputs");

const cases = [
  {
    id: "native-text",
    mechanism: "name from button contents",
    hypothesis: "Native text is exposed as the name.",
    expectedName: "Save changes",
    expectedIgnored: false,
  },
  {
    id: "aria-label-override",
    mechanism: "aria-label takes precedence over contents",
    hypothesis: "The exposed name differs from the visible text.",
    expectedName: "Delete account",
    expectedIgnored: false,
  },
  {
    id: "hidden-child",
    mechanism: "visible child text marked aria-hidden",
    hypothesis: "The visible button reaches the tree with an empty name.",
    expectedName: "",
    expectedIgnored: false,
  },
  {
    id: "icon-only",
    mechanism: "icon-only button whose SVG is aria-hidden",
    hypothesis: "The visible button reaches the tree with an empty name.",
    expectedName: "",
    expectedIgnored: false,
  },
  {
    id: "hidden-reference",
    mechanism: "aria-labelledby points to a hidden element",
    hypothesis: "Hidden referenced text overrides the visible button text.",
    expectedName: "Activate self-destruct mode",
    expectedIgnored: false,
  },
  {
    id: "title-fallback",
    mechanism: "title attribute fallback",
    hypothesis: "A textless button receives its title as a name.",
    expectedName: "Download invoices",
    expectedIgnored: false,
  },
  {
    id: "duplicate-more-one",
    mechanism: "generic aria-label",
    hypothesis: "The first distinct action is exposed only as More.",
    expectedName: "More",
    expectedIgnored: false,
  },
  {
    id: "duplicate-more-two",
    mechanism: "generic aria-label",
    hypothesis: "The second distinct action is also exposed only as More.",
    expectedName: "More",
    expectedIgnored: false,
  },
  {
    id: "aria-hidden-button",
    mechanism: "aria-hidden on a focusable button",
    hypothesis: "The visible button is ignored by the accessibility tree.",
    expectedName: "",
    expectedIgnored: true,
  },
  {
    id: "empty-aria-label",
    mechanism: "empty aria-label with usable contents",
    hypothesis: "The empty author label does not erase native text.",
    expectedName: "Still named from content",
    expectedIgnored: false,
  },
];

function propertyValue(properties, name) {
  return properties?.find((property) => property.name === name)?.value?.value ?? null;
}

function simplifySource(source) {
  return {
    type: source.type ?? null,
    attribute: source.attribute ?? null,
    value: source.value?.value ?? null,
    superseded: source.superseded ?? false,
    invalid: source.invalid ?? false,
  };
}

await mkdir(outputDir, { recursive: true });

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1 });
const fixtureUrl = pathToFileURL(join(here, "fixture.html")).href;
await page.goto(fixtureUrl, { waitUntil: "load" });

const session = await page.context().newCDPSession(page);
await session.send("Accessibility.enable");
await session.send("DOM.enable");

const { root } = await session.send("DOM.getDocument", { depth: -1, pierce: true });
const results = [];

for (const testCase of cases) {
  const selector = `#${testCase.id}`;
  const { nodeId } = await session.send("DOM.querySelector", { nodeId: root.nodeId, selector });

  if (!nodeId) {
    throw new Error(`Could not find fixture ${selector}`);
  }

  const [{ nodes }, domSnapshot] = await Promise.all([
    session.send("Accessibility.getPartialAXTree", { nodeId, fetchRelatives: false }),
    page.locator(selector).evaluate((element) => {
      const style = getComputedStyle(element);
      return {
        visibleText: element.innerText.replace(/\s+/g, " ").trim(),
        ariaLabel: element.getAttribute("aria-label"),
        ariaLabelledBy: element.getAttribute("aria-labelledby"),
        ariaHidden: element.getAttribute("aria-hidden"),
        title: element.getAttribute("title"),
        display: style.display,
        visibility: style.visibility,
        opacity: style.opacity,
        rect: element.getBoundingClientRect().toJSON(),
      };
    }),
  ]);

  const axNode = nodes[0];
  const actualName = axNode?.name?.value ?? "";
  const actualIgnored = axNode?.ignored ?? true;

  results.push({
    ...testCase,
    passed: actualName === testCase.expectedName && actualIgnored === testCase.expectedIgnored,
    dom: domSnapshot,
    accessibility: {
      role: axNode?.role?.value ?? null,
      name: actualName,
      ignored: actualIgnored,
      ignoredReasons: axNode?.ignoredReasons?.map((reason) => ({
        name: reason.name,
        value: reason.value?.value ?? null,
      })) ?? [],
      focusable: propertyValue(axNode?.properties, "focusable"),
      nameSources: axNode?.name?.sources?.map(simplifySource) ?? [],
    },
  });
}

const exposedResults = results.filter((result) => !result.accessibility.ignored);
const nameGroups = new Map();
for (const result of exposedResults) {
  const name = result.accessibility.name;
  if (!name) continue;
  const ids = nameGroups.get(name) ?? [];
  ids.push(result.id);
  nameGroups.set(name, ids);
}

const duplicateNames = [...nameGroups.entries()]
  .filter(([, ids]) => ids.length > 1)
  .map(([name, ids]) => ({ name, ids }));

const report = {
  generatedAt: new Date().toISOString(),
  fixture: "fixture.html",
  environment: {
    node: process.version,
    platform: process.platform,
    architecture: process.arch,
    browser: await browser.version(),
    protocol: "Chrome DevTools Protocol Accessibility.getPartialAXTree",
  },
  scope: "Ten deliberately selected fixtures. These counts describe this failure zoo only; they are not prevalence estimates.",
  summary: {
    fixtures: results.length,
    hypothesesPassed: results.filter((result) => result.passed).length,
    exposedNodes: exposedResults.length,
    ignoredNodes: results.filter((result) => result.accessibility.ignored).length,
    exposedEmptyNames: exposedResults.filter((result) => result.accessibility.name === "").length,
    visibleTextNameMismatches: exposedResults.filter((result) => {
      const visible = result.dom.visibleText;
      const name = result.accessibility.name;
      return visible && visible !== name;
    }).length,
    duplicateNames,
  },
  results,
};

await Promise.all([
  writeFile(join(outputDir, "accessibility-tree-results.json"), `${JSON.stringify(report, null, 2)}\n`),
  page.screenshot({ path: join(outputDir, "fixture.png"), fullPage: true }),
]);

await session.send("Accessibility.disable");
await browser.close();

if (report.summary.hypothesesPassed !== cases.length) {
  console.error(JSON.stringify(report.summary, null, 2));
  process.exitCode = 1;
} else {
  console.log(JSON.stringify(report.summary, null, 2));
}
