import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import type { Page } from "playwright";
import { ADVISORY_AXE_RULES, BLOCKING_AXE_RULES } from "./constants.js";
import type { AxeResultEntry, AxeRunSnapshot } from "./types.js";

const AXE_SOURCE = readFileSync(
  createRequire(import.meta.url).resolve("axe-core/axe.min.js"),
  "utf8",
);

const AXE_RUN_RULES = [...BLOCKING_AXE_RULES, ...ADVISORY_AXE_RULES];

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function parseAxeResultEntry(value: unknown, index: number, bucket: string): AxeResultEntry {
  if (!isRecord(value)) {
    throw new Error(`axe ${bucket}[${String(index)}] must be an object`);
  }
  if (typeof value.id !== "string" || value.id.trim().length === 0) {
    throw new Error(`axe ${bucket}[${String(index)}].id must be a non-empty string`);
  }
  const impact = value.impact === null || typeof value.impact === "string" ? value.impact : null;
  if (typeof value.description !== "string" || typeof value.help !== "string") {
    throw new Error(`axe ${bucket}[${String(index)}] missing description/help`);
  }
  if (!Array.isArray(value.nodes)) {
    throw new Error(`axe ${bucket}[${String(index)}].nodes must be an array`);
  }
  const nodes = value.nodes.map((node, nodeIndex) => {
    if (!isRecord(node)) {
      throw new Error(`axe ${bucket}[${String(index)}].nodes[${String(nodeIndex)}] must be an object`);
    }
    if (!Array.isArray(node.target)) {
      throw new Error(`axe node target must be an array`);
    }
    if (typeof node.html !== "string") {
      throw new Error(`axe node html must be a string`);
    }
    return {
      target: node.target.map((entry) => String(entry)),
      html: node.html,
      ...(typeof node.failureSummary === "string"
        ? { failureSummary: node.failureSummary }
        : {}),
    };
  });
  return {
    id: value.id,
    impact,
    description: value.description,
    help: value.help,
    nodes,
  };
}

function parseAxeBucket(value: unknown, bucket: string): AxeResultEntry[] {
  if (!Array.isArray(value)) {
    throw new Error(`axe ${bucket} must be an array`);
  }
  return value.map((entry, index) => parseAxeResultEntry(entry, index, bucket));
}

export function parseAxeRunSnapshot(input: unknown): AxeRunSnapshot {
  if (!isRecord(input)) {
    throw new Error("axe snapshot must be an object");
  }
  const testEngine = isRecord(input.testEngine) ? input.testEngine : undefined;
  const version =
    typeof input.version === "string" && input.version.trim().length > 0
      ? input.version
      : typeof testEngine?.version === "string" && testEngine.version.trim().length > 0
        ? testEngine.version
        : "";
  if (version.length === 0) {
    throw new Error("axe snapshot version must be a non-empty string");
  }
  return {
    version,
    violations: parseAxeBucket(input.violations, "violations"),
    incomplete: parseAxeBucket(input.incomplete, "incomplete"),
    passes: parseAxeBucket(input.passes, "passes"),
    inapplicable: parseAxeBucket(input.inapplicable, "inapplicable"),
  };
}

export function axeLaneForRule(ruleId: string): "blocking" | "advisory" {
  if (ADVISORY_AXE_RULES.has(ruleId)) {
    return "advisory";
  }
  if (BLOCKING_AXE_RULES.has(ruleId)) {
    return "blocking";
  }
  return "advisory";
}

export function axeLocator(entry: AxeResultEntry): string {
  const firstNode = entry.nodes[0];
  if (firstNode === undefined) {
    return `[axe-rule="${entry.id}"]`;
  }
  return firstNode.target.join(" ");
}

export async function injectAxe(page: Page): Promise<void> {
  await page.addScriptTag({ content: AXE_SOURCE });
}

export async function runAxeOnPage(
  page: Page,
  options: { disabledRules?: string[] } = {},
): Promise<AxeRunSnapshot> {
  await injectAxe(page);
  const disabledRules = new Set(options.disabledRules ?? []);
  const activeRules = AXE_RUN_RULES.filter((ruleId) => !disabledRules.has(ruleId));
  const raw = await page.evaluate(
    async ({ runRules }) => {
      const globalWindow = window as typeof window & {
        axe?: {
          run: (
            context?: unknown,
            runOptions?: { runOnly?: { type: "rule"; values: string[] } },
          ) => Promise<unknown>;
        };
      };
      if (globalWindow.axe === undefined) {
        throw new Error("axe-core is not available on the page");
      }
      const result = await globalWindow.axe.run(document, {
        runOnly: { type: "rule", values: runRules },
      });
      return result;
    },
    { runRules: activeRules },
  );
  return parseAxeRunSnapshot(raw);
}
