import { describe, expect, it } from "vitest";
import { asComponentId } from "@fragments-sdk/core";
import { extractJsxFacts } from "../usage/jsx.js";
import { createMemoryScanSource } from "./memory-source.js";
import { createSourceBackedComponentResolver } from "./component-resolver.js";

function componentIds(
  source: string,
  resolver: Parameters<typeof extractJsxFacts>[2],
  filePath = "src/page.tsx"
) {
  return extractJsxFacts(source, filePath, resolver)
    .filter((fact) => fact.kind === "usage_component")
    .map((fact) => fact.componentId);
}

describe("createSourceBackedComponentResolver", () => {
  it("resolves tsconfig path aliases through barrel exports", async () => {
    const files = {
      "tsconfig.json": JSON.stringify({
        compilerOptions: {
          baseUrl: ".",
          paths: { "@/*": ["src/*"] },
        },
      }),
      "src/ui/index.ts": `export { Button } from "./Button";\n`,
      "src/ui/Button.tsx": `export function Button() { return null; }\n`,
      "src/page.tsx": `import { Button as DsButton } from "@/ui";

export function Page() {
  return <DsButton />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/ui/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([
      "@acme/ui#Button",
    ]);
  });

  it("resolves relative star barrels and compound component exports", async () => {
    const files = {
      "src/components/index.ts": `export * from "./Card";\n`,
      "src/components/Card.tsx": `export const Card = Object.assign(() => null, { Header: () => null });\n`,
      "src/page.tsx": `import { Card } from "./components";

export function Page() {
  return <Card.Header />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/components/Card.tsx",
          exportName: "Card.Header",
          componentId: asComponentId("@acme/ui#Card.Header"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([
      "@acme/ui#Card.Header",
    ]);
  });

  it("resolves local import-export barrels", async () => {
    const files = {
      "src/components/index.ts": [
        `import { Button as RawButton } from "./Button";`,
        `export { RawButton as Button };`,
      ].join("\n"),
      "src/components/Button.tsx": `export function Button() { return null; }\n`,
      "src/page.tsx": `import { Button } from "./components";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/components/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([
      "@acme/ui#Button",
    ]);
  });

  it("treats duplicate named re-exports as local ambiguity", async () => {
    const files = {
      "src/components/index.ts": [
        `export { Button } from "./GovernedButton";`,
        `export { Button } from "./LocalButton";`,
      ].join("\n"),
      "src/components/GovernedButton.tsx": `export function Button() { return null; }\n`,
      "src/components/LocalButton.tsx": `export function Button() { return null; }\n`,
      "src/page.tsx": `import { Button } from "./components";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/components/GovernedButton.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([]);
    expect(
      resolver({
        filePath: "src/page.tsx",
        elementName: "Button",
        local: "Button",
        imported: "Button",
        source: "./components",
      })
    ).toBeNull();
  });

  it("treats conflicting star barrel exports as local ambiguity", async () => {
    const files = {
      "src/components/index.ts": [
        `export * from "./GovernedButton";`,
        `export * from "./LocalButton";`,
      ].join("\n"),
      "src/components/GovernedButton.tsx": `export function Button() { return null; }\n`,
      "src/components/LocalButton.tsx": `export function Button() { return null; }\n`,
      "src/page.tsx": `import { Button } from "./components";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/components/GovernedButton.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([]);
    expect(
      resolver({
        filePath: "src/page.tsx",
        elementName: "Button",
        local: "Button",
        imported: "Button",
        source: "./components",
      })
    ).toBeNull();
  });

  it("does not resolve compound members through unrelated tail exports", async () => {
    const files = {
      "src/components/index.ts": [
        `export { Card } from "./Card";`,
        `export { Header } from "./Header";`,
      ].join("\n"),
      "src/components/Card.tsx": `export function Card() { return null; }\n`,
      "src/components/Header.tsx": `export function Header() { return null; }\n`,
      "src/page.tsx": `import { Card } from "./components";

export function Page() {
  return <Card.Header />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [
        {
          modulePath: "src/components/Header.tsx",
          exportName: "Header",
          componentId: asComponentId("@acme/ui#Header"),
        },
      ],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([]);
  });

  it("marks resolved local aliases without component aliases as local, not external", async () => {
    const files = {
      "tsconfig.json": JSON.stringify({
        compilerOptions: {
          baseUrl: ".",
          paths: { "@/*": ["src/*"] },
        },
      }),
      "src/internal/Thing.tsx": `export function Thing() { return null; }\n`,
      "src/page.tsx": `import { Thing } from "@/internal/Thing";

export function Page() {
  return <Thing />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "src/page.tsx" }],
      aliases: [],
    });

    expect(componentIds(files["src/page.tsx"], { resolveComponentId: resolver })).toEqual([]);
  });

  it("resolves workspace package root exports through barrels", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: { ".": "./src/index.ts", "./button": "./src/Button.tsx" },
      }),
      "packages/ui/src/index.ts": `export { Button } from "./Button";\n`,
      "packages/ui/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/ui/src/Button.tsx" }],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("local-design-system#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["local-design-system#Button"]);
  });

  it("resolves exact scoped tsconfig aliases through multi-export barrels", async () => {
    const files = {
      "tsconfig.json": JSON.stringify({
        compilerOptions: {
          baseUrl: ".",
          paths: {
            "@acme/ui": ["packages/ui/src/index.ts"],
            "@acme/ui/*": ["packages/ui/src/*"],
          },
        },
      }),
      "packages/ui/src/index.ts": [
        `export { Button } from "./components/Button";`,
        `export { Card } from "./components/Card";`,
      ].join("\n"),
      "packages/ui/src/components/Button.tsx": `export function Button() { return null; }\n`,
      "packages/ui/src/components/Card.tsx": `export function Card() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button, Card } from "@acme/ui";

export function Page() {
  return <><Button /><Card /></>;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "packages/ui/src/components/Button.tsx" },
        { relPath: "packages/ui/src/components/Card.tsx" },
      ],
      aliases: [
        {
          modulePath: "packages/ui/src/components/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
        {
          modulePath: "packages/ui/src/components/Card.tsx",
          exportName: "Card",
          componentId: asComponentId("@acme/ui#Card"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@acme/ui#Button", "@acme/ui#Card"]);
  });

  it("resolves workspace package subpath exports", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: { ".": "./src/index.ts", "./button": "./src/Button.tsx" },
      }),
      "packages/ui/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui/button";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/ui/src/Button.tsx" }],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("local-design-system#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["local-design-system#Button"]);
  });

  it("keeps true external package imports on the existing fallback path", async () => {
    const files = {
      "apps/web/src/page.tsx": `import { Button } from "@vendor/ui";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }],
      aliases: [],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@vendor/ui#Button"]);
  });

  it("resolves explicit source aliases for self-contained package fragments", async () => {
    const files = {
      "package.json": JSON.stringify({ name: "agent-design-system", private: true }),
      "apps/web/src/page.tsx": `import { Button } from "agent-design-system";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "package.json" }],
      aliases: [
        {
          source: "agent-design-system",
          exportName: "Button",
          componentId: asComponentId("agent-design-system#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["agent-design-system#Button"]);
  });

  it("treats known workspace packages with missing export targets as local", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: { ".": "./src/missing.ts" },
      }),
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/ui/package.json" }],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual([]);
    expect(
      resolver({
        filePath: "apps/web/src/page.tsx",
        elementName: "Button",
        local: "Button",
        imported: "Button",
        source: "@acme/ui",
      })
    ).toBeNull();
  });

  it("treats declaration-only workspace package exports as local", async () => {
    const files = {
      "packages/types-only/package.json": JSON.stringify({
        name: "@acme/types-only",
        exports: { ".": { types: "./dist/index.d.ts" } },
      }),
      "packages/types-only/dist/index.d.ts": `export declare function Button(): null;\n`,
      "packages/types-only/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/types-only";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "packages/types-only/src/Button.tsx" },
      ],
      aliases: [],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual([]);
    expect(
      resolver({
        filePath: "apps/web/src/page.tsx",
        elementName: "Button",
        local: "Button",
        imported: "Button",
        source: "@acme/types-only",
      })
    ).toBeNull();
  });

  it("marks local workspace packages without component aliases as local", async () => {
    const files = {
      "packages/internal/package.json": JSON.stringify({
        name: "@acme/internal",
        exports: { ".": "./src/index.ts" },
      }),
      "packages/internal/src/index.ts": `export { Thing } from "./Thing";\n`,
      "packages/internal/src/Thing.tsx": `export function Thing() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Thing } from "@acme/internal";

export function Page() {
  return <Thing />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/internal/src/Thing.tsx" }],
      aliases: [],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual([]);
  });

  it("resolves aliases from extended tsconfigs", async () => {
    const files = {
      "tsconfig.base.json": JSON.stringify({
        compilerOptions: {
          baseUrl: ".",
          paths: { "@ui/*": ["packages/ui/src/*"] },
        },
      }),
      "tsconfig.json": JSON.stringify({
        extends: "./tsconfig.base.json",
      }),
      "packages/ui/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@ui/Button";

export function Page() {
  return <Button />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/ui/src/Button.tsx" }],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@acme/ui#Button"]);
  });

  it("discovers workspace packages from package metadata files", async () => {
    const files = {
      "package.json": JSON.stringify({
        workspaces: ["packages/*"],
      }),
      "packages/internal/package.json": JSON.stringify({
        name: "@acme/internal",
        exports: { ".": "./src/index.ts" },
      }),
      "packages/internal/src/index.ts": `export { Thing } from "./Thing";\n`,
      "packages/internal/src/Thing.tsx": `export function Thing() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Thing } from "@acme/internal";

export function Page() {
  return <Thing />;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "package.json" },
        { relPath: "packages/internal/package.json" },
      ],
      aliases: [],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual([]);
  });

  it("resolves repo-shaped package export patterns through nested barrels", async () => {
    const files = {
      "pnpm-workspace.yaml": "packages:\n  - apps/*\n  - packages/*\n",
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: {
          ".": {
            types: "./dist/index.d.ts",
            import: "./src/index.ts",
          },
          "./components/*": {
            types: "./dist/components/*.d.ts",
            import: "./src/components/*.tsx",
          },
        },
      }),
      "packages/ui/src/index.ts": `export * from "./components/index";\n`,
      "packages/ui/src/components/index.ts": `export { Button } from "./Button";\n`,
      "packages/ui/src/components/Button.tsx": `export function Button() { return null; }\n`,
      "packages/ui/dist/components/Button.d.ts": `export declare function Button(): null;\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";
import { Button as DirectButton } from "@acme/ui/components/Button";

export function Page() {
  return <><Button /><DirectButton /></>;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "pnpm-workspace.yaml" },
        { relPath: "packages/ui/package.json" },
        { relPath: "packages/ui/src/components/Button.tsx" },
      ],
      aliases: [
        {
          modulePath: "packages/ui/src/components/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@acme/ui#Button", "@acme/ui#Button"]);
  });

  it("resolves default component exports re-exported through package barrels", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: {
          ".": "./src/index.ts",
          "./components/*": "./src/components/*.tsx",
        },
      }),
      "packages/ui/src/index.ts": [
        `export { default as Button } from "./components/Button";`,
        `export { NamedOnly } from "./components/NamedOnly";`,
      ].join("\n"),
      "packages/ui/src/components/Button.tsx": `export default function Button() { return null; }\n`,
      "packages/ui/src/components/NamedOnly.tsx": `export function NamedOnly() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";
import DirectButton from "@acme/ui/components/Button";
import NamedOnlyDefault from "@acme/ui/components/NamedOnly";

export function Page() {
  return <><Button /><DirectButton /><NamedOnlyDefault /></>;
}
`,
    };
    const resolver = await createSourceBackedComponentResolver({
      source: createMemoryScanSource(files),
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "packages/ui/package.json" },
        { relPath: "packages/ui/src/components/Button.tsx" },
        { relPath: "packages/ui/src/components/NamedOnly.tsx" },
      ],
      aliases: [
        {
          modulePath: "packages/ui/src/components/Button.tsx",
          exportName: "default",
          componentId: asComponentId("@acme/ui#Button"),
        },
        {
          modulePath: "packages/ui/src/components/NamedOnly.tsx",
          exportName: "NamedOnly",
          componentId: asComponentId("@acme/ui#NamedOnly"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@acme/ui#Button", "@acme/ui#Button"]);
  });

  it("treats missing optional resolver files as absent", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: { ".": "./src/index.ts" },
      }),
      "packages/ui/src/index.ts": `export { Button } from "./Button";\n`,
      "packages/ui/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";

export function Page() {
  return <Button />;
}
`,
    };
    const memory = createMemoryScanSource(files);
    const source = {
      ...memory,
      async readFile(path: string) {
        const text = await memory.readFile(path);
        if (text === null) throw new Error(`missing: ${path}`);
        return text;
      },
    };
    const resolver = await createSourceBackedComponentResolver({
      source,
      files: [{ relPath: "apps/web/src/page.tsx" }, { relPath: "packages/ui/src/Button.tsx" }],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(
      componentIds(
        files["apps/web/src/page.tsx"],
        { resolveComponentId: resolver },
        "apps/web/src/page.tsx"
      )
    ).toEqual(["@acme/ui#Button"]);
  });

  it("memoizes resolver source reads across repeated package imports", async () => {
    const files = {
      "packages/ui/package.json": JSON.stringify({
        name: "@acme/ui",
        exports: { ".": "./src/index.ts" },
      }),
      "packages/ui/src/index.ts": `export { Button } from "./Button";\n`,
      "packages/ui/src/Button.tsx": `export function Button() { return null; }\n`,
      "apps/web/src/page.tsx": `import { Button } from "@acme/ui";
export function Page() { return <Button />; }
`,
      "apps/web/src/other.tsx": `import { Button } from "@acme/ui";
export function Other() { return <Button />; }
`,
    };
    const memory = createMemoryScanSource(files);
    const reads = new Map<string, number>();
    const source = {
      ...memory,
      async readFile(path: string) {
        reads.set(path, (reads.get(path) ?? 0) + 1);
        return memory.readFile(path);
      },
    };

    await createSourceBackedComponentResolver({
      source,
      files: [
        { relPath: "apps/web/src/page.tsx" },
        { relPath: "apps/web/src/other.tsx" },
        { relPath: "packages/ui/package.json" },
      ],
      aliases: [
        {
          modulePath: "packages/ui/src/Button.tsx",
          exportName: "Button",
          componentId: asComponentId("@acme/ui#Button"),
        },
      ],
    });

    expect(reads.get("packages/ui/src/index.ts")).toBe(1);
  });
});
