import fs from "node:fs";
import path from "node:path";
import fg from "fast-glob";
import {
  parseTestCasesFromDoc,
  parseItDescriptionsFromTest,
  parseScenarioPatternsFromDoc,
  parseDescribeBlocksFromSpec,
} from "../parse-doc-test-cases";
import { moduleTestCaseCategories, appTestCaseCategories, appE2eCategory } from "./discovery";
import { APP_PATHS } from "./paths";
import type { CheckError } from "./sync-check-source";

function toCamelCase(pascalCase: string): string {
  return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
}

export async function runModuleTestCaseSyncCheck(root: string, cwd: string): Promise<CheckError[]> {
  const errors: CheckError[] = [];
  const categories = moduleTestCaseCategories(root);

  for (const category of categories) {
    const docPaths = await fg(category.docPattern, { cwd });

    for (const docPath of docPaths) {
      const docFullPath = path.join(cwd, docPath);
      const docContent = fs.readFileSync(docFullPath, "utf-8");
      const docTestCases = parseTestCasesFromDoc(docContent);

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

      const docBasename = path.basename(docPath, ".md");
      const docsIndex = docPath.indexOf("/docs/");
      if (docsIndex === -1) continue;
      const modulePath = docPath.substring(0, docsIndex);
      const testFileName = `${toCamelCase(docBasename)}.test.ts`;
      const testPath = path.join(modulePath, category.testDir, testFileName);
      const testFullPath = path.join(cwd, testPath);

      if (!fs.existsSync(testFullPath)) {
        errors.push({
          type: "missing-test-file",
          category: category.name,
          docPath,
          sourcePath: testPath,
          expectedBasename: testFileName,
        });
        continue;
      }

      const testContent = fs.readFileSync(testFullPath, "utf-8");
      const itDescriptions = parseItDescriptionsFromTest(testContent);

      const docSet = new Set(docTestCases);
      const testSet = new Set(itDescriptions);

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

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

  return errors;
}

function resolveStoryTestPath(docPath: string, testDir: string): string | null {
  const regex = /^(.+)\/docs\/business-flow\/([^/]+)\/story\/([^/]+)\.md$/;
  const match = regex.exec(docPath);
  if (!match) return null;
  const [, appPath, flow, name] = match;
  return `${appPath}/${testDir}/${flow}/${name}.test.ts`;
}

export async function runAppTestCaseSyncCheck(appRoot: string, cwd: string): Promise<CheckError[]> {
  const errors: CheckError[] = [];
  const config = appTestCaseCategories(appRoot);
  const docPaths = await fg(config.docPattern, { cwd });

  for (const docPath of docPaths) {
    const docFullPath = path.join(cwd, docPath);
    const docContent = fs.readFileSync(docFullPath, "utf-8");
    const docTestCases = parseTestCasesFromDoc(docContent);

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

    const testPath = resolveStoryTestPath(docPath, config.testDir);
    if (!testPath) continue;

    const testFullPath = path.join(cwd, testPath);
    if (!fs.existsSync(testFullPath)) {
      errors.push({
        type: "missing-test-file",
        category: config.name,
        docPath,
        sourcePath: testPath,
        expectedBasename: path.basename(testPath),
      });
      continue;
    }

    const testContent = fs.readFileSync(testFullPath, "utf-8");
    const itDescriptions = parseItDescriptionsFromTest(testContent);

    const docSet = new Set(docTestCases);
    const testSet = new Set(itDescriptions);

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

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

  return errors;
}

/**
 * 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<CheckError[]> {
  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;
}

/**
 * Screen sync check: validates that each screen doc in docs/screen/
 * has a corresponding page object in e2e/pages/.
 * Page objects without a screen doc are allowed (e.g., login).
 */
export async function runScreenPageSyncCheck(appRoot: string, cwd: string): Promise<CheckError[]> {
  const errors: CheckError[] = [];

  const screenDocPattern = `${appRoot}/${APP_PATHS.docs.screen}/*.md`;
  const screenDocs = await fg(screenDocPattern, { cwd });
  const docBasenames = new Set(screenDocs.map((p) => path.basename(p, ".md")));

  const pagePattern = `${appRoot}/${APP_PATHS.tests.e2e}/pages/**/*.ts`;
  const pageFiles = await fg(pagePattern, { cwd });
  const pageBasenames = new Set(pageFiles.map((p) => path.basename(p, ".ts")));

  for (const name of docBasenames) {
    if (!pageBasenames.has(name)) {
      errors.push({
        type: "missing-page-object",
        category: "screen",
        docPath: `${appRoot}/${APP_PATHS.docs.screen}/${name}.md`,
        expectedBasename: `${name}.ts`,
      });
    }
  }

  return errors;
}

/**
 * Parse actor names from the `export type Actor = "a" | "b"` union in e2e/fixtures/user.ts.
 */
export function parseActorNamesFromFixture(content: string): string[] {
  const match = /export\s+type\s+Actor\s*=\s*([^;]+)/.exec(content);
  if (!match) return [];
  const names: string[] = [];
  const regex = /["']([^"']+)["']/g;
  let m: RegExpExecArray | null;
  while ((m = regex.exec(match[1])) !== null) {
    names.push(m[1]);
  }
  return names;
}

/**
 * Actor sync check: validates that each actor defined in e2e/fixtures/user.ts
 * has a corresponding doc file in docs/actor/, and vice versa.
 */
export async function runActorSyncCheck(appRoot: string, cwd: string): Promise<CheckError[]> {
  const errors: CheckError[] = [];
  const fixturePath = `${appRoot}/${APP_PATHS.tests.e2e}/fixtures/user.ts`;
  const fixtureFullPath = path.join(cwd, fixturePath);

  if (!fs.existsSync(fixtureFullPath)) return errors;

  const content = fs.readFileSync(fixtureFullPath, "utf-8");
  const fixtureActors = new Set(parseActorNamesFromFixture(content));

  const actorDocPattern = `${appRoot}/${APP_PATHS.docs.actor}/*.md`;
  const actorDocs = await fg(actorDocPattern, { cwd });
  const docActors = new Set(actorDocs.map((p) => path.basename(p, ".md")));

  for (const actor of fixtureActors) {
    if (!docActors.has(actor)) {
      errors.push({
        type: "missing-actor-doc",
        category: "actor",
        sourcePath: fixturePath,
        expectedBasename: `${actor}.md`,
      });
    }
  }

  for (const actor of docActors) {
    if (!fixtureActors.has(actor)) {
      errors.push({
        type: "missing-actor-fixture",
        category: "actor",
        docPath: `${appRoot}/${APP_PATHS.docs.actor}/${actor}.md`,
        expectedBasename: actor,
      });
    }
  }

  return errors;
}

/**
 * Spec-fixture sync check: validates that each e2e spec file
 * has a corresponding fixture file with the same basename under e2e/fixtures/.
 * e2e/tests/<name>.spec.ts → e2e/fixtures/<name>.ts (or e2e/fixtures/user.ts as fallback)
 */
export async function runSpecFixtureSyncCheck(appRoot: string, cwd: string): Promise<CheckError[]> {
  const errors: CheckError[] = [];

  const specPattern = `${appRoot}/${APP_PATHS.tests.e2e}/tests/**/*.spec.ts`;
  const specFiles = await fg(specPattern, { cwd });

  const fixturePattern = `${appRoot}/${APP_PATHS.tests.e2e}/fixtures/*.ts`;
  const fixtureFiles = await fg(fixturePattern, { cwd });
  const fixtureBasenames = new Set(fixtureFiles.map((p) => path.basename(p, ".ts")));

  for (const specFile of specFiles) {
    const specBasename = path.basename(specFile, ".spec.ts");

    if (!fixtureBasenames.has(specBasename)) {
      errors.push({
        type: "missing-spec-fixture",
        category: "e2e-fixture",
        sourcePath: specFile,
        expectedBasename: `${specBasename}.ts`,
      });
    }
  }

  return errors;
}
