import fs from "node:fs";
import path from "node:path";
import fg from "fast-glob";
import {
  parseScenarioPatternsFromDoc,
  parseDescribeBlocksFromSpec,
} from "../../lib/parse-doc-test-cases";
import type { CheckError, SyncCheckResult } from "../../lib/sync-check/shared";
import { appE2eCategory } from "./categories";

/**
 * Resolve e2e spec file path from a story doc path.
 * docs/business-flow/<flow>/story/<actor>--<name>.md → frontend/e2e/tests/<flow>.spec.ts
 */
function resolveE2eSpecPath(
  docPath: string,
  testDir: string,
): { specPath: string; flow: string } | null {
  const regex = /^(.+)\/docs\/business-flow\/([^/]+)\/story\/([^/]+)\.md$/;
  const match = regex.exec(docPath);
  if (!match) return null;
  const [, appPath, flow] = match;
  return { specPath: `${appPath}/${testDir}/${flow}.spec.ts`, flow };
}

/**
 * E2e sync check: validates that each scenario pattern in story docs
 * has a corresponding test() in the e2e spec file.
 *
 * Structure:
 *   - 1 spec file per business flow (<flow>.spec.ts)
 *   - 1 describe block per story (describe("<actor>--<name>", ...))
 *   - 1 test per scenario pattern (test("<pattern>", ...))
 */
export async function runE2eSyncCheck(appRoot: string, cwd: string): Promise<SyncCheckResult> {
  const errors: CheckError[] = [];
  const config = appE2eCategory(appRoot);
  const docPaths = await fg(config.docPattern, { cwd });

  // Group story docs by flow → spec file
  const specGroups = new Map<
    string,
    { specPath: string; docs: { docPath: string; storyName: string }[] }
  >();

  for (const docPath of docPaths) {
    const resolved = resolveE2eSpecPath(docPath, config.testDir);
    if (!resolved) continue;

    const storyName = path.basename(docPath, ".md");
    let group = specGroups.get(resolved.specPath);
    if (!group) {
      group = { specPath: resolved.specPath, docs: [] };
      specGroups.set(resolved.specPath, group);
    }
    group.docs.push({ docPath, storyName });
  }

  for (const [, { specPath, docs }] of specGroups) {
    const specFullPath = path.join(cwd, specPath);

    if (!fs.existsSync(specFullPath)) {
      for (const { docPath } of docs) {
        const docFullPath = path.join(cwd, docPath);
        const docContent = fs.readFileSync(docFullPath, "utf-8");
        const scenarios = parseScenarioPatternsFromDoc(docContent);
        if (scenarios.length === 0) continue;

        errors.push({
          type: "missing-test-file",
          category: config.name,
          docPath,
          sourcePath: specPath,
          expectedBasename: path.basename(specPath),
        });
      }
      continue;
    }

    const specContent = fs.readFileSync(specFullPath, "utf-8");
    const describeBlocks = parseDescribeBlocksFromSpec(specContent);

    for (const { docPath, storyName } of docs) {
      const docFullPath = path.join(cwd, docPath);
      const docContent = fs.readFileSync(docFullPath, "utf-8");
      const scenarios = parseScenarioPatternsFromDoc(docContent);

      if (scenarios.length === 0) continue;

      const testNames = describeBlocks.get(storyName);
      if (!testNames) {
        errors.push({
          type: "missing-test-file",
          category: config.name,
          docPath,
          sourcePath: specPath,
          expectedBasename: `describe("${storyName}")`,
        });
        continue;
      }

      const docSet = new Set(scenarios);
      const testSet = new Set(testNames);

      for (const scenario of docSet) {
        if (!testSet.has(scenario)) {
          errors.push({
            type: "missing-test-case",
            category: config.name,
            docPath,
            sourcePath: specPath,
            expectedBasename: scenario,
          });
        }
      }

      for (const testName of testSet) {
        if (!docSet.has(testName)) {
          errors.push({
            type: "extra-test-case",
            category: config.name,
            docPath,
            sourcePath: specPath,
            expectedBasename: testName,
          });
        }
      }
    }
  }

  // Check for orphaned spec files (spec exists but no matching business flow)
  const expectedFlows = new Set<string>();
  for (const docPath of docPaths) {
    const resolved = resolveE2eSpecPath(docPath, config.testDir);
    if (resolved) expectedFlows.add(resolved.flow);
  }

  const specPattern = `${appRoot}/${config.testDir}/**/*.spec.ts`;
  const allSpecFiles = await fg(specPattern, { cwd });
  for (const specFile of allSpecFiles) {
    const flow = path.basename(specFile, ".spec.ts");
    if (!expectedFlows.has(flow)) {
      errors.push({
        type: "orphaned-spec",
        category: config.name,
        sourcePath: specFile,
        expectedBasename: path.basename(specFile),
      });
    }
  }

  return { errors, filesChecked: docPaths.length + allSpecFiles.length };
}
