import os from "os"
import path from "path"
import { resolveRegistryTree } from "@/src/registry/resolver"
import { addComponents } from "@/src/utils/add-components"
import type { Config } from "@/src/utils/get-config"
import { findPackageRoot, getWorkspaceConfig } from "@/src/utils/get-config"
import { updateFiles } from "@/src/utils/updaters/update-files"
import { updateFonts } from "@/src/utils/updaters/update-fonts"
import fs from "fs-extra"
import { afterEach, describe, expect, it, vi } from "vitest"

// Mock all external dependencies. Unlike the sibling add-components.test.ts,
// updateFiles and get-config keep their REAL implementations by default —
// this file specifically exercises addComponents' workspace-routing logic
// against actual updateFiles/getWorkspaceConfig behavior, which conflicts
// with the full vi.fn() mocks in add-components.test.ts. vi.mock is
// per-file, so living in a sibling file lets both coexist.
vi.mock("@/src/registry/resolver", () => ({
  resolveRegistryTree: vi.fn(),
}))

vi.mock("@/src/utils/get-config", async () => {
  const actual = (await vi.importActual(
    "@/src/utils/get-config"
  )) as typeof import("@/src/utils/get-config")
  return {
    ...actual,
    getWorkspaceConfig: vi.fn(),
    findPackageRoot: vi.fn(),
  }
})

vi.mock("@/src/utils/updaters/update-files", async () => {
  const actual = (await vi.importActual(
    "@/src/utils/updaters/update-files"
  )) as typeof import("@/src/utils/updaters/update-files")

  return {
    ...actual,
    updateFiles: vi.fn().mockResolvedValue({
      filesCreated: [],
      filesUpdated: [],
      filesSkipped: [],
    }),
  }
})

vi.mock("@/src/utils/updaters/update-dependencies", () => ({
  updateDependencies: vi.fn().mockResolvedValue(undefined),
}))

vi.mock("@/src/utils/updaters/update-tailwind-config", () => ({
  updateTailwindConfig: vi.fn().mockResolvedValue(undefined),
}))

vi.mock("@/src/utils/updaters/update-env-vars", () => ({
  updateEnvVars: vi.fn().mockResolvedValue(undefined),
}))

vi.mock("@/src/utils/updaters/update-fonts", () => ({
  updateFonts: vi.fn().mockResolvedValue(undefined),
  massageTreeForFonts: vi.fn().mockImplementation((tree) => tree),
}))

vi.mock("@/src/utils/updaters/update-css", () => ({
  updateCss: vi.fn().mockResolvedValue(undefined),
}))

vi.mock("@/src/utils/get-project-info", async () => {
  const actual = (await vi.importActual(
    "@/src/utils/get-project-info"
  )) as typeof import("@/src/utils/get-project-info")

  return {
    ...actual,
    getProjectTailwindVersionFromConfig: vi.fn().mockResolvedValue("4"),
  }
})

vi.mock("@/src/utils/spinner", () => ({
  spinner: vi.fn().mockReturnValue({
    start: vi.fn().mockReturnThis(),
    succeed: vi.fn().mockReturnThis(),
    fail: vi.fn().mockReturnThis(),
    info: vi.fn().mockReturnThis(),
  }),
}))

vi.mock("@/src/utils/logger", () => ({
  logger: {
    info: vi.fn(),
    log: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  },
}))

afterEach(() => {
  vi.clearAllMocks()
  vi.mocked(updateFiles).mockResolvedValue({
    filesCreated: [],
    filesUpdated: [],
    filesSkipped: [],
  })
})

function createMockConfig(overrides: Partial<Config> = {}): Config {
  return {
    $schema: "https://ui.shadcn.com/schema.json",
    style: "new-york",
    rsc: true,
    tsx: true,
    tailwind: {
      css: "app/globals.css",
      baseColor: "zinc",
      cssVariables: true,
    },
    aliases: {
      components: "@/components",
      utils: "@/lib/utils",
      ui: "@/components/ui",
      lib: "@/lib",
      hooks: "@/hooks",
    },
    resolvedPaths: {
      cwd: "/apps/web",
      tailwindConfig: "/apps/web/tailwind.config.ts",
      tailwindCss: "/apps/web/app/globals.css",
      utils: "/apps/web/lib/utils",
      components: "/apps/web/components",
      lib: "/apps/web/lib",
      hooks: "/apps/web/hooks",
      ui: "/apps/web/components/ui",
    },
    ...overrides,
  } as Config
}

function createPackageImportConfig(
  cwd: string,
  overrides: Partial<Config> = {}
): Config {
  return {
    $schema: "https://ui.shadcn.com/schema.json",
    style: "new-york",
    rsc: false,
    tsx: true,
    tailwind: {
      config: "",
      css: "src/index.css",
      baseColor: "",
      cssVariables: true,
    },
    aliases: {
      components: "#components",
      ui: "#components/ui",
      lib: "#lib",
      hooks: "#hooks",
      utils: "#utils",
    },
    resolvedPaths: {
      cwd,
      tailwindConfig: "",
      tailwindCss: path.resolve(cwd, "src/index.css"),
      components: path.resolve(cwd, "src/components"),
      ui: path.resolve(cwd, "src/components/ui"),
      lib: path.resolve(cwd, "src/lib"),
      hooks: path.resolve(cwd, "src/hooks"),
      utils: path.resolve(cwd, "src/lib/utils.ts"),
    },
    registries: {},
    ...overrides,
  } as Config
}

async function writePackageImportProject(cwd: string) {
  await fs.ensureDir(path.resolve(cwd, "src"))
  await fs.writeJson(
    path.resolve(cwd, "package.json"),
    {
      name: path.basename(cwd),
      type: "module",
      dependencies: {
        tailwindcss: "^4.0.0",
      },
      imports: {
        "#components/*": "./src/components/*",
        "#hooks": "./src/hooks/index.ts",
        "#utils": "./src/lib/utils.ts",
      },
    },
    { spaces: 2 }
  )
  await fs.writeJson(
    path.resolve(cwd, "tsconfig.json"),
    {
      compilerOptions: {
        module: "esnext",
        moduleResolution: "bundler",
        resolvePackageJsonImports: true,
      },
    },
    { spaces: 2 }
  )
  await fs.writeFile(
    path.resolve(cwd, "src/index.css"),
    '@import "tailwindcss";\n'
  )
  await fs.writeFile(path.resolve(cwd, "vite.config.ts"), "export default {}\n")
}

describe("addComponents workspace routing", () => {
  it("throws when workspace registry items cannot be resolved", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        ...appConfig.resolvedPaths,
        cwd: "/packages/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })
    vi.mocked(resolveRegistryTree).mockResolvedValue(null)

    await expect(
      addComponents(["missing"], appConfig, { silent: true })
    ).rejects.toThrow("Failed to fetch components from registry.")

    expect(updateFiles).not.toHaveBeenCalled()
  })

  it("should route registry:hook files to workspaceConfig.hooks", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    // Hooks config resolves to the same package but is a distinct config object.
    // getWorkspaceConfig builds one config per alias key.
    const hooksConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
      hooks: hooksConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/sidebar.tsx",
          type: "registry:ui",
          content: "export function Sidebar() {}",
        },
        {
          path: "registry/hooks/use-mobile.ts",
          type: "registry:hook",
          content: "export function useMobile() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["sidebar"], appConfig, { silent: true })

    // updateFiles should be called twice: once for registry:ui, once for registry:hook.
    expect(updateFiles).toHaveBeenCalledTimes(2)

    // First call: registry:ui files with the UI workspace config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [
        expect.objectContaining({
          type: "registry:ui",
          path: "registry/ui/sidebar.tsx",
        }),
      ],
      uiConfig,
      expect.any(Object)
    )

    // Second call: registry:hook files with the hooks workspace config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [
        expect.objectContaining({
          type: "registry:hook",
          path: "registry/hooks/use-mobile.ts",
        }),
      ],
      hooksConfig,
      expect.any(Object)
    )
  })

  it("should route registry:lib files to workspaceConfig.lib", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })
    const libConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
      lib: libConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/button.tsx",
          type: "registry:ui",
          content: "export function Button() {}",
        },
        {
          path: "registry/lib/utils.ts",
          type: "registry:lib",
          content: "export function cn() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["button"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(2)

    // First call: registry:ui files with the UI workspace config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:ui" })],
      uiConfig,
      expect.any(Object)
    )

    // Second call: registry:lib files with the lib workspace config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [expect.objectContaining({ type: "registry:lib" })],
      libConfig,
      expect.any(Object)
    )
  })

  it("should fall back to app config for unmapped types like registry:component", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/components/login-form.tsx",
          type: "registry:component",
          content: "export function LoginForm() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/apps/web")

    await addComponents(["login-01"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(1)

    // registry:component should fall back to the app config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:component" })],
      appConfig,
      expect.any(Object)
    )
  })

  it("should fall back to app config when workspace key is missing", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    // Workspace config only has ui — no hooks or lib.
    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/sidebar.tsx",
          type: "registry:ui",
          content: "export function Sidebar() {}",
        },
        {
          path: "registry/hooks/use-mobile.ts",
          type: "registry:hook",
          content: "export function useMobile() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["sidebar"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(2)

    // registry:ui → workspaceConfig.ui.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:ui" })],
      uiConfig,
      expect.any(Object)
    )

    // registry:hook with no workspaceConfig.hooks → falls back to app config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [expect.objectContaining({ type: "registry:hook" })],
      appConfig,
      expect.any(Object)
    )
  })

  it("should route all three mapped types to their workspace configs", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
      hooks: uiConfig,
      lib: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/sidebar.tsx",
          type: "registry:ui",
          content: "export function Sidebar() {}",
        },
        {
          path: "registry/hooks/use-mobile.ts",
          type: "registry:hook",
          content: "export function useMobile() {}",
        },
        {
          path: "registry/lib/utils.ts",
          type: "registry:lib",
          content: "export function cn() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["sidebar"], appConfig, { silent: true })

    // Three calls: one per type.
    expect(updateFiles).toHaveBeenCalledTimes(3)

    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:ui" })],
      uiConfig,
      expect.any(Object)
    )
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [expect.objectContaining({ type: "registry:hook" })],
      uiConfig,
      expect.any(Object)
    )
    expect(updateFiles).toHaveBeenNthCalledWith(
      3,
      [expect.objectContaining({ type: "registry:lib" })],
      uiConfig,
      expect.any(Object)
    )
  })

  it("should fall back to app config for registry:file", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/button.tsx",
          type: "registry:ui",
          content: "export function Button() {}",
        },
        {
          path: ".env",
          type: "registry:file",
          target: "~/.env",
          content: "API_KEY=xxx",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["some-component"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(2)

    // registry:ui → workspace.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:ui" })],
      uiConfig,
      expect.any(Object)
    )

    // registry:file → app config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [expect.objectContaining({ type: "registry:file" })],
      appConfig,
      expect.any(Object)
    )
  })

  it("should default files with no type to registry:ui", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/button.tsx",
          // No type — should default to registry:ui.
          content: "export function Button() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    } as any)

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["button"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(1)

    // Should route to workspace UI config.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      expect.any(Array),
      uiConfig,
      expect.any(Object)
    )
  })

  it("should group multiple files of the same type into one updateFiles call", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/sidebar.tsx",
          type: "registry:ui",
          content: "export function Sidebar() {}",
        },
        {
          path: "registry/ui/sidebar-nav.tsx",
          type: "registry:ui",
          content: "export function SidebarNav() {}",
        },
        {
          path: "registry/ui/sidebar-menu.tsx",
          type: "registry:ui",
          content: "export function SidebarMenu() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["sidebar"], appConfig, { silent: true })

    // All three files are registry:ui — should be one call, not three.
    expect(updateFiles).toHaveBeenCalledTimes(1)
    expect(updateFiles).toHaveBeenCalledWith(
      expect.arrayContaining([
        expect.objectContaining({ path: "registry/ui/sidebar.tsx" }),
        expect.objectContaining({ path: "registry/ui/sidebar-nav.tsx" }),
        expect.objectContaining({ path: "registry/ui/sidebar-menu.tsx" }),
      ]),
      uiConfig,
      expect.any(Object)
    )
  })

  it("should route hooks to separate package when aliases differ", async () => {
    const appConfig = createMockConfig({
      aliases: {
        components: "~foo/ui/components",
        utils: "~foo/ui/lib/utils",
        hooks: "~foo/hooks/src",
        lib: "~foo/ui/lib",
        ui: "~foo/ui/components",
      },
    })

    const uiPackageConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    const hooksPackageConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/hooks",
        tailwindConfig: "/packages/hooks/tailwind.config.ts",
        tailwindCss: "/packages/hooks/src/globals.css",
        utils: "/packages/hooks/src/lib/utils",
        components: "/packages/hooks/src/components",
        lib: "/packages/hooks/src/lib",
        hooks: "/packages/hooks/src/hooks",
        ui: "/packages/hooks/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiPackageConfig,
      hooks: hooksPackageConfig,
      lib: uiPackageConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/sidebar.tsx",
          type: "registry:ui",
          content: "export function Sidebar() {}",
        },
        {
          path: "registry/hooks/use-mobile.ts",
          type: "registry:hook",
          content: "export function useMobile() {}",
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockImplementation(
      async (_cwd, resolvedPath) => {
        if (resolvedPath.startsWith("/packages/hooks")) {
          return "/packages/hooks"
        }
        return "/packages/ui"
      }
    )

    await addComponents(["sidebar"], appConfig, { silent: true })

    expect(updateFiles).toHaveBeenCalledTimes(2)

    // registry:ui → UI package.
    expect(updateFiles).toHaveBeenNthCalledWith(
      1,
      [expect.objectContaining({ type: "registry:ui" })],
      uiPackageConfig,
      expect.any(Object)
    )

    // registry:hook → hooks package (separate from UI).
    expect(updateFiles).toHaveBeenNthCalledWith(
      2,
      [expect.objectContaining({ type: "registry:hook" })],
      hooksPackageConfig,
      expect.any(Object)
    )
  })

  it("should rewrite cross-type imports when files target the same workspace package", async () => {
    const actualUpdateFiles = (await vi.importActual(
      "@/src/utils/updaters/update-files"
    )) as typeof import("@/src/utils/updaters/update-files")
    vi.mocked(updateFiles).mockImplementation(actualUpdateFiles.updateFiles)

    const root = await fs.mkdtemp(path.join(os.tmpdir(), "shadcn-cross-type-"))
    const appCwd = path.resolve(root, "apps/web")
    const uiCwd = path.resolve(root, "packages/ui")

    try {
      await writePackageImportProject(appCwd)
      await writePackageImportProject(uiCwd)

      const appConfig = createPackageImportConfig(appCwd)
      const uiConfig = createPackageImportConfig(uiCwd)

      vi.mocked(getWorkspaceConfig).mockResolvedValue({
        components: appConfig,
        ui: uiConfig,
        lib: appConfig,
        hooks: appConfig,
      })

      vi.mocked(resolveRegistryTree).mockResolvedValue({
        files: [
          {
            path: "registry/components/example-card.tsx",
            type: "registry:component",
            content: `import { useThing } from "@/hooks/use-thing"

export function ExampleCard() {
  useThing()
  return null
}
`,
          },
          {
            path: "registry/hooks/use-thing.ts",
            type: "registry:hook",
            content: `export function useThing() {
  return true
}
`,
          },
        ],
        dependencies: [],
        devDependencies: [],
      })

      await addComponents(["example-card"], appConfig, {
        overwrite: true,
        silent: true,
      })

      const componentContent = await fs.readFile(
        path.resolve(appCwd, "src/components/example-card.tsx"),
        "utf-8"
      )

      expect(componentContent).toContain(`from "../hooks/use-thing"`)
      expect(componentContent).not.toContain(`from "#hooks/use-thing"`)
    } finally {
      await fs.remove(root)
    }
  })

  it("should call updateFonts with app config, not workspace config", async () => {
    const appConfig = createMockConfig()
    const uiConfig = createMockConfig({
      resolvedPaths: {
        cwd: "/packages/ui",
        tailwindConfig: "/packages/ui/tailwind.config.ts",
        tailwindCss: "/packages/ui/src/globals.css",
        utils: "/packages/ui/src/lib/utils",
        components: "/packages/ui/src/components",
        lib: "/packages/ui/src/lib",
        hooks: "/packages/ui/src/hooks",
        ui: "/packages/ui/src/components/ui",
      },
    })

    vi.mocked(getWorkspaceConfig).mockResolvedValue({
      ui: uiConfig,
    })

    vi.mocked(resolveRegistryTree).mockResolvedValue({
      files: [
        {
          path: "registry/ui/button.tsx",
          type: "registry:ui",
          content: "export function Button() {}",
        },
      ],
      fonts: [
        {
          name: "font-inter",
          type: "registry:font",
          title: "Inter",
          font: {
            provider: "google",
            import: "Inter",
            family: "Inter, sans-serif",
            variable: "--font-sans",
            weight: ["400", "500", "600", "700"],
            subsets: ["latin"],
          },
        },
      ],
      dependencies: [],
      devDependencies: [],
    })

    vi.mocked(findPackageRoot).mockResolvedValue("/packages/ui")

    await addComponents(["button"], appConfig, { silent: true })

    // updateFonts should use the app config (layout lives in the app).
    expect(updateFonts).toHaveBeenCalledWith(
      expect.any(Array),
      appConfig,
      expect.any(Object)
    )

    // Verify it was NOT called with the workspace UI config.
    expect(updateFonts).not.toHaveBeenCalledWith(
      expect.anything(),
      uiConfig,
      expect.anything()
    )
  })
})
