import { describe, it, expect } from "vitest";
import { discoverStorybook } from "../index.js";

const BUTTON_STORY = `
  import type { Meta, StoryObj } from "@storybook/react";
  import { Button } from "@acme/ui";

  const meta = {
    title: "Forms/Button",
    component: Button,
    tags: ["autodocs", "canonical"],
    argTypes: {
      variant: {
        control: "select",
        options: ["primary", "secondary", "ghost"],
        description: "Visual style",
      },
      size: { control: "radio", options: ["sm", "md", "lg"] },
      disabled: { control: "boolean" },
    },
  } satisfies Meta<typeof Button>;
  export default meta;

  type Story = StoryObj<typeof meta>;

  export const Primary: Story = { args: { variant: "primary", children: "Save" } };
  export const Ghost: Story = { args: { variant: "ghost", disabled: true } };
`;

const CARD_STORY = `
  import type { Meta, StoryObj } from "@storybook/react";
  import { Card } from "../Card";

  const meta = {
    title: "Layout/Card",
    component: Card,
  } satisfies Meta<typeof Card>;
  export default meta;

  type Story = StoryObj<typeof meta>;

  export const Default: Story = { args: {} };
`;

describe("discoverStorybook", () => {
  it("distills a single component's variants, props, and import facts", () => {
    const result = discoverStorybook({
      files: [{ path: "src/Button.stories.tsx", content: BUTTON_STORY }],
    });

    expect(result.storyFileCount).toBe(1);
    expect(result.warnings).toEqual([]);
    expect(result.components).toHaveLength(1);

    const button = result.components[0];
    expect(button.componentName).toBe("Button");
    expect(button.importSpecifier).toBe("@acme/ui");
    expect(button.importIsPackage).toBe(true);
    expect(button.title).toBe("Forms/Button");
    expect(button.category).toBe("forms");
    expect(button.tags).toEqual(["autodocs", "canonical"]);
    expect(button.storyCount).toBe(2);
    expect(button.storyNames.sort()).toEqual(["Ghost", "Primary"]);

    // Concrete per-story args are threaded through (the CSF seed for Slice 4).
    const primary = button.stories.find((s) => s.name === "Primary");
    expect(primary?.args).toEqual({ variant: "primary", children: "Save" });
    const ghost = button.stories.find((s) => s.name === "Ghost");
    expect(ghost?.args).toEqual({ variant: "ghost", disabled: true });

    const variantProps = button.variants.map((v) => v.prop).sort();
    expect(variantProps).toEqual(["size", "variant"]);
    const variant = button.variants.find((v) => v.prop === "variant");
    expect(variant?.options).toEqual(["primary", "secondary", "ghost"]);
    expect(variant?.control).toBe("select");

    // documentedProps is the superset: option-bearing AND boolean controls.
    expect(button.documentedProps.sort()).toEqual(["disabled", "size", "variant"]);
  });

  it("classifies relative imports as non-package", () => {
    const result = discoverStorybook({
      files: [{ path: "src/Card.stories.tsx", content: CARD_STORY }],
    });
    const card = result.components[0];
    expect(card.importSpecifier).toBe("../Card");
    expect(card.importIsPackage).toBe(false);
  });

  it("aggregates multiple distinct components sorted by name", () => {
    const result = discoverStorybook({
      files: [
        { path: "src/Card.stories.tsx", content: CARD_STORY },
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
      ],
    });
    expect(result.components.map((c) => c.componentName)).toEqual(["Button", "Card"]);
  });

  it("merges multiple story files documenting the same component", () => {
    const extraButton = `
      import type { Meta, StoryObj } from "@storybook/react";
      import { Button } from "@acme/ui";
      const meta = {
        title: "Forms/Button",
        component: Button,
        argTypes: {
          variant: { control: "select", options: ["link", "danger"] },
          fullWidth: { control: "boolean" },
        },
      } satisfies Meta<typeof Button>;
      export default meta;
      type Story = StoryObj<typeof meta>;
      export const Danger: Story = { args: { variant: "danger" } };
    `;

    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "src/Button.extra.stories.tsx", content: extraButton },
      ],
    });

    expect(result.components).toHaveLength(1);
    const button = result.components[0];
    expect(button.storyCount).toBe(3);
    expect(button.storyFilePaths.sort()).toEqual([
      "src/Button.extra.stories.tsx",
      "src/Button.stories.tsx",
    ]);

    // Variant options union across both files, deduped.
    const variant = button.variants.find((v) => v.prop === "variant");
    expect(variant?.options).toEqual(["primary", "secondary", "ghost", "link", "danger"]);
    expect(button.documentedProps.sort()).toEqual(["disabled", "fullWidth", "size", "variant"]);
  });

  it("does not collapse distinct title-less story files into one bucket", () => {
    // With no title, the parser derives a name from the file basename, so two
    // distinct files must remain two distinct components (never merged).
    const titleless = `
      const meta = {};
      export default meta;
      export const Default = {};
    `;
    const result = discoverStorybook({
      files: [
        { path: "alpha.stories.tsx", content: titleless },
        { path: "beta.stories.tsx", content: titleless },
      ],
    });
    expect(result.components).toHaveLength(2);
    expect(result.components.map((c) => c.componentName).sort()).toEqual(["alpha", "beta"]);
  });

  it("ignores non-story files", () => {
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "src/Button.tsx", content: "export const Button = () => null;" },
        { path: "README.md", content: "# Docs" },
      ],
    });
    expect(result.storyFileCount).toBe(1);
    expect(result.components).toHaveLength(1);
  });

  it("parses config facts when a config is supplied", () => {
    const config = `
      const config = {
        framework: { name: "@storybook/react-vite" },
        stories: ["../src/**/*.stories.@(ts|tsx)"],
        core: { builder: "vite" },
      };
      export default config;
    `;
    const result = discoverStorybook({
      files: [{ path: "src/Button.stories.tsx", content: BUTTON_STORY }],
      configContent: config,
      configPath: ".storybook/main.ts",
    });
    expect(result.config.detected).toBe(true);
    expect(result.config.configPath).toBe(".storybook/main.ts");
    expect(result.config.framework).toBe("@storybook/react-vite");
    expect(result.config.builder).toBe("vite");
    expect(result.config.storyPatterns).toEqual(["../src/**/*.stories.@(ts|tsx)"]);
  });

  it("uses Storybook config globs as the source of truth", () => {
    const config = `
      const config = {
        stories: ["../src/**/*.stories.@(ts|tsx)"],
      };
      export default config;
    `;
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "stories/Card.stories.tsx", content: CARD_STORY },
      ],
      configContent: config,
      configPath: ".storybook/main.ts",
    });

    expect(result.storyFileCount).toBe(1);
    expect(result.components.map((component) => component.componentName)).toEqual(["Button"]);
  });

  it("supports scaffold-style nested extglob story globs", () => {
    const config = `
      export default {
        stories: ["../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))"],
      };
    `;
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "stories/Card.stories.tsx", content: CARD_STORY },
      ],
      configContent: config,
      configPath: ".storybook/main.ts",
    });

    expect(result.storyFileCount).toBe(1);
    expect(result.components.map((component) => component.componentName)).toEqual(["Button"]);
  });

  it("supports object-form Storybook story config entries", () => {
    const config = `
      export default {
        stories: [
          { directory: "../src", files: "**/*.stories.@(ts|tsx)" },
        ],
      };
    `;
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "stories/Card.stories.tsx", content: CARD_STORY },
      ],
      configContent: config,
      configPath: ".storybook/main.ts",
    });

    expect(result.config.storyPatterns).toEqual(["../src/**/*.stories.@(ts|tsx)"]);
    expect(result.storyFileCount).toBe(1);
    expect(result.components.map((component) => component.componentName)).toEqual(["Button"]);
  });

  it("falls back to default patterns when no config is supplied", () => {
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "stories/Card.stories.tsx", content: CARD_STORY },
      ],
    });
    expect(result.config.detected).toBe(false);
    expect(result.config.framework).toBeUndefined();
    expect(result.config.storyPatterns.length).toBeGreaterThan(0);
    expect(result.storyFileCount).toBe(2);
  });

  it("is a silent no-op when no config and no story files exist", () => {
    const result = discoverStorybook({
      files: [{ path: "src/Button.tsx", content: "export const Button = () => null;" }],
    });

    expect(result.storyFileCount).toBe(0);
    expect(result.components).toEqual([]);
    expect(result.warnings).toEqual([]);
  });

  it("surfaces per-file parse warnings while still discovering valid stories", () => {
    // The parser recovers from syntax errors rather than throwing; discover must
    // surface those per-file warnings (prefixed with the path) so a partially
    // parsed story is visible downstream, not silently trusted.
    const broken = "const meta = { title: 'Broken";
    const result = discoverStorybook({
      files: [
        { path: "src/Button.stories.tsx", content: BUTTON_STORY },
        { path: "src/Broken.stories.tsx", content: broken },
      ],
    });
    // The valid component is still discovered.
    expect(result.components.some((c) => c.componentName === "Button")).toBe(true);
    expect(result.warnings.some((w) => w.startsWith("src/Broken.stories.tsx:"))).toBe(true);
  });
});
