import { describe, expect, it } from "vitest";
import { createMemoryScanSource } from "./memory-source.js";
import {
  extractReactUcfsFromSource,
  extractReactUcfsFromSourceWithDiagnostics,
} from "./react-ucf-source.js";

describe("extractReactUcfsFromSource", () => {
  it("extracts props from imported interfaces through a source-backed TypeScript program", async () => {
    const files = {
      "src/types.ts": `export interface ButtonProps {
  /** Visual treatment. */
  variant?: "primary" | "secondary";
  size: "sm" | "md";
  disabled?: boolean;
}
`,
      "src/Button.tsx": `import type { ButtonProps } from "./types";

export function Button({ variant = "primary", size, disabled }: ButtonProps) {
  return <button disabled={disabled} data-size={size}>{variant}</button>;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }, { relPath: "src/types.ts" }],
      sourceCommit: "abc123",
    });

    expect(facts).toHaveLength(1);
    expect(facts[0].filePath).toBe("src/Button.tsx");

    const variant = facts[0].props.find((prop) => prop.name === "variant");
    expect(variant).toMatchObject({
      typeText: '"primary" | "secondary"',
      optional: true,
      hasDefault: true,
      defaultValueText: '"primary"',
      isUnion: true,
      unionMembers: ['"primary"', '"secondary"'],
      jsdoc: "Visual treatment.",
    });
    expect(variant?.loc).toMatchObject({
      file: "/__fragments_scan__/src/types.ts",
      line: 3,
    });

    const size = facts[0].props.find((prop) => prop.name === "size");
    expect(size).toMatchObject({
      typeText: '"sm" | "md"',
      optional: false,
      hasDefault: false,
      isUnion: true,
    });
  });

  it("resolves tsconfig path aliases for imported prop types", async () => {
    const files = {
      "tsconfig.json": JSON.stringify({
        compilerOptions: {
          baseUrl: ".",
          paths: { "@ui/*": ["src/ui/*"] },
        },
      }),
      "src/ui/Button.types.ts": `export type ButtonProps = {
  tone?: "neutral" | "danger";
};
`,
      "src/Button.tsx": `import type { ButtonProps } from "@ui/Button.types";

export function Button(props: ButtonProps) {
  return <button>{props.tone}</button>;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }, { relPath: "src/ui/Button.types.ts" }],
      sourceCommit: "abc123",
    });

    expect(facts).toHaveLength(1);
    expect(facts[0].props.find((prop) => prop.name === "tone")).toMatchObject({
      typeText: '"neutral" | "danger"',
      optional: true,
      isUnion: true,
    });
  });

  it("extracts props from React.FC and interface extension", async () => {
    const files = {
      "src/Button.tsx": `import type { ReactNode } from "react";

interface BaseProps {
  disabled?: boolean;
}

interface ButtonProps extends BaseProps {
  children?: ReactNode;
  kind: "submit" | "reset";
}

export const Button: React.FC<ButtonProps> = ({ children, kind }) => {
  return <button type={kind}>{children}</button>;
};
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }],
      sourceCommit: "abc123",
    });

    expect(facts).toHaveLength(1);
    expect(facts[0].props.find((prop) => prop.name === "disabled")).toMatchObject({
      optional: true,
      typeText: "boolean",
    });
    expect(facts[0].props.find((prop) => prop.name === "kind")).toMatchObject({
      optional: false,
      typeText: '"submit" | "reset"',
      isUnion: true,
    });
  });

  it("extracts props from ComponentProps utility aliases", async () => {
    const files = {
      "src/Base.tsx": `export interface BaseButtonProps {
  variant?: "primary" | "secondary";
  pressed: boolean;
}

export function BaseButton(_props: BaseButtonProps) {
  return <button />;
}
`,
      "src/Button.tsx": `import type { ComponentProps } from "react";
import { BaseButton } from "./Base";

type ButtonProps = ComponentProps<typeof BaseButton> & {
  analyticsId?: string;
};

export function Button(props: ButtonProps) {
  return <BaseButton {...props} />;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }, { relPath: "src/Base.tsx" }],
      sourceCommit: "abc123",
    });

    const button = facts.find((fact) => fact.componentName === "Button");
    expect(button).toBeDefined();
    expect(button?.props.find((prop) => prop.name === "variant")).toMatchObject({
      optional: true,
      typeText: '"primary" | "secondary"',
    });
    expect(button?.props.find((prop) => prop.name === "pressed")).toMatchObject({
      optional: false,
      typeText: "boolean",
    });
    expect(button?.props.find((prop) => prop.name === "analyticsId")).toMatchObject({
      optional: true,
      typeText: "string",
      schemaConfidence: "exact",
    });
  });

  it("extracts props from ComponentPropsWithoutRef utility aliases", async () => {
    const files = {
      "src/Base.tsx": `export interface BaseButtonProps {
  variant?: "primary" | "secondary";
}

export function BaseButton(_props: BaseButtonProps) {
  return <button />;
}
`,
      "src/Button.tsx": `import type { ComponentPropsWithoutRef } from "react";
import { BaseButton } from "./Base";

type ButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;

export function Button(props: ButtonProps) {
  return <BaseButton {...props} />;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }, { relPath: "src/Base.tsx" }],
      sourceCommit: "abc123",
    });

    const button = facts.find((fact) => fact.componentName === "Button");
    expect(button?.props.find((prop) => prop.name === "variant")).toMatchObject({
      optional: true,
      typeText: '"primary" | "secondary"',
      schemaConfidence: "exact",
    });
  });

  it("preserves default export identity through source-backed extraction", async () => {
    const files = {
      "src/Button.tsx": [
        "type ButtonProps = { variant?: 'primary' | 'secondary' };",
        "export default function Button(props: ButtonProps) {",
        "  return <button>{props.variant}</button>;",
        "}",
      ].join("\n"),
      "src/index.ts": "export { default as Button } from './Button';\n",
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }, { relPath: "src/index.ts" }],
      sourceCommit: "abc123",
    });

    const sourceButton = facts.find((fact) => fact.filePath === "src/Button.tsx");
    const barrelButton = facts.find((fact) => fact.filePath === "src/index.ts");
    expect(sourceButton).toMatchObject({
      componentName: "Button",
      exports: [
        expect.objectContaining({
          exportedAs: "default",
          localName: "Button",
          isDefault: true,
        }),
      ],
    });
    expect(barrelButton).toMatchObject({
      componentName: "Button",
      exportedFromBarrel: true,
      exports: [
        expect.objectContaining({
          exportedAs: "Button",
          localName: "default",
          isDefault: false,
        }),
      ],
    });
  });

  it("extracts constrained generic component props", async () => {
    const files = {
      "src/Select.tsx": `interface SelectProps<TValue extends string> {
  value: TValue;
  onChange: (value: TValue) => void;
}

export function Select<TValue extends "sm" | "lg">(props: SelectProps<TValue>) {
  return <button>{props.value}</button>;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Select.tsx" }],
      sourceCommit: "abc123",
    });

    expect(facts).toHaveLength(1);
    expect(facts[0].props.find((prop) => prop.name === "value")).toMatchObject({
      optional: false,
      typeText: "TValue",
      schemaConfidence: "partial",
    });
    expect(facts[0].props.find((prop) => prop.name === "onChange")).toMatchObject({
      optional: false,
      schemaConfidence: "partial",
    });
  });

  it("marks open prop surfaces as partial and reports diagnostics", async () => {
    const files = {
      "src/Button.tsx": `interface ButtonProps {
  variant?: "primary" | "secondary";
  [key: string]: unknown;
}

export function Button(props: ButtonProps) {
  return <button>{props.variant}</button>;
}
`,
    };

    const result = await extractReactUcfsFromSourceWithDiagnostics({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }],
      sourceCommit: "abc123",
    });

    expect(result.ucfs[0]?.props.find((prop) => prop.name === "variant")).toMatchObject({
      typeText: '"primary" | "secondary"',
      schemaConfidence: "partial",
    });
    expect(result.diagnostics).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          code: "prop_metadata_partial",
          filePath: "src/Button.tsx",
        }),
      ])
    );
  });

  it("marks polymorphic asChild prop surfaces as partial", async () => {
    const files = {
      "src/Button.tsx": `interface ButtonProps {
  asChild?: boolean;
  variant?: "primary" | "secondary";
}

export function Button(props: ButtonProps) {
  return <button>{props.variant}</button>;
}
`,
    };

    const facts = await extractReactUcfsFromSource({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/Button.tsx" }],
      sourceCommit: "abc123",
    });

    expect(facts[0]?.props.find((prop) => prop.name === "variant")).toMatchObject({
      schemaConfidence: "partial",
    });
  });

  it("reports diagnostics for unreadable, unsupported, invalid, and component-less files", async () => {
    const files = {
      "src/Broken.tsx": `export function Broken( { return <div />;`,
      "src/util.ts": `export const helper = 1;`,
      "src/readme.md": `# ignored`,
      "tsconfig.json": `{ "compilerOptions": { "moduleResolution": "not-real" } }`,
    };

    const result = await extractReactUcfsFromSourceWithDiagnostics({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "src/Broken.tsx" },
        { relPath: "src/util.ts" },
        { relPath: "src/readme.md" },
        { relPath: "src/missing.tsx" },
      ],
      sourceCommit: "abc123",
    });

    expect(result.ucfs).toEqual([]);
    const codes = result.diagnostics.map((diagnostic) => diagnostic.code);
    expect(codes).toContain("invalid_tsconfig");
    expect(codes).toContain("typescript_syntax");
    expect(codes).toContain("unsupported_file");
    expect(codes).toContain("missing_file");
    expect(codes).toContain("no_component_exports");
  });

  it("reads files concurrently in batches and reports progress", async () => {
    const files: Record<string, string> = {};
    for (let i = 0; i < 20; i += 1) {
      files[`src/C${i}.tsx`] = `export function C${i}() { return <div />; }\n`;
    }
    const base = createMemoryScanSource(files);
    let inFlight = 0;
    let maxInFlight = 0;
    const source = {
      ...base,
      async readFile(path: string) {
        inFlight += 1;
        maxInFlight = Math.max(maxInFlight, inFlight);
        try {
          await new Promise((resolve) => setTimeout(resolve, 1));
          return await base.readFile(path);
        } finally {
          inFlight -= 1;
        }
      },
    };

    const progress: Array<{ filesProcessed: number; totalFiles: number }> = [];
    const result = await extractReactUcfsFromSourceWithDiagnostics({
      source,
      files: Object.keys(files).map((relPath) => ({ relPath })),
      sourceCommit: "abc123",
      onProgress: (p) => progress.push({ ...p }),
    });

    // Reads happen in parallel batches, not one-at-a-time.
    expect(maxInFlight).toBeGreaterThan(1);
    // Progress is reported and ends at the full total.
    expect(progress.length).toBeGreaterThan(1);
    expect(progress.at(-1)).toEqual({ filesProcessed: 20, totalFiles: 20 });
    for (let i = 1; i < progress.length; i += 1) {
      expect(progress[i].filesProcessed).toBeGreaterThanOrEqual(
        progress[i - 1].filesProcessed,
      );
    }
    // Parallelism doesn't change the result: every component is still found.
    expect(new Set(result.ucfs.map((ucf) => ucf.componentName)).size).toBe(20);
  });
});
