import {
  getPackageManager,
  getPackageManagerFromUserAgent,
  getPackageRunnerCommand,
} from "@/src/utils/get-package-manager"
import { spinner } from "@/src/utils/spinner"
import { execa } from "execa"
import fs from "fs-extra"
import prompts from "prompts"
import {
  afterEach,
  beforeEach,
  describe,
  expect,
  it,
  vi,
  type MockInstance,
} from "vitest"

import { createProject } from "./create-project"

// Mock dependencies
vi.mock("fs-extra")
vi.mock("execa")
vi.mock("prompts")
vi.mock("@/src/utils/get-package-manager", () => ({
  getPackageManager: vi.fn().mockResolvedValue("npm"),
  getPackageManagerFromUserAgent: vi.fn(() => "npm"),
  getPackageRunnerCommand: vi.fn(() => "npx"),
}))
vi.mock("@/src/utils/spinner")
vi.mock("@/src/utils/logger", () => ({
  logger: {
    break: vi.fn(),
    error: vi.fn(),
    info: vi.fn(),
  },
}))

describe("createProject", () => {
  let mockExit: MockInstance

  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(getPackageManager).mockResolvedValue("npm")
    vi.mocked(getPackageManagerFromUserAgent).mockReturnValue("npm")
    vi.mocked(getPackageRunnerCommand).mockReturnValue("npx")

    // Reset all fs mocks
    vi.mocked(fs.access).mockResolvedValue(undefined)
    vi.mocked(fs.existsSync).mockReturnValue(false)
    vi.mocked(fs.ensureDir).mockResolvedValue(undefined)
    vi.mocked(fs.writeFile).mockResolvedValue(undefined)
    vi.mocked(fs.move).mockResolvedValue(undefined)
    vi.mocked(fs.remove).mockResolvedValue(undefined)

    // Mock execa for git clone and package manager install.
    vi.mocked(execa).mockResolvedValue({
      stdout: "",
      stderr: "",
      exitCode: 0,
      signal: undefined,
      signalDescription: undefined,
      command: "",
      escapedCommand: "",
      failed: false,
      timedOut: false,
      isCanceled: false,
      killed: false,
    } as any)

    // Reset prompts mock
    vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })

    // Mock spinner function
    const mockSpinner = {
      start: vi.fn().mockReturnThis(),
      succeed: vi.fn().mockReturnThis(),
      fail: vi.fn().mockReturnThis(),
      stop: vi.fn().mockReturnThis(),
      text: "",
      prefixText: "",
      suffixText: "",
      color: "cyan" as const,
      indent: 0,
      spinner: "dots" as const,
      isSpinning: false,
      interval: 100,
      stream: process.stderr,
      clear: vi.fn(),
      render: vi.fn(),
      frame: vi.fn(),
      stopAndPersist: vi.fn(),
      warn: vi.fn(),
      info: vi.fn(),
    }
    vi.mocked(spinner).mockReturnValue(mockSpinner as any)
  })

  afterEach(() => {
    vi.resetAllMocks()
    mockExit?.mockRestore()
  })

  it("should create a Next.js project with default options", async () => {
    vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })

    const result = await createProject({
      cwd: "/test",
      force: false,
    })

    expect(result).toEqual({
      projectPath: "/test/my-app",
      projectName: "my-app",
      template: "next",
    })
  })

  it("should create a monorepo project with --monorepo flag", async () => {
    vi.mocked(prompts).mockResolvedValue({
      type: "next",
      name: "my-monorepo",
    })

    const result = await createProject({
      cwd: "/test",
      force: false,
      monorepo: true,
    })

    expect(result).toEqual({
      projectPath: "/test/my-monorepo",
      projectName: "my-monorepo",
      template: "next",
    })
  })

  it("should force next template for remote components", async () => {
    const result = await createProject({
      cwd: "/test",
      force: true,
      components: ["/chat/b/some-component"],
    })

    expect(result.template).toBe("next")
  })

  it("should throw error if project path already exists", async () => {
    // Mock fs.existsSync to return true only for the specific package.json path
    vi.mocked(fs.existsSync).mockImplementation((path: any) => {
      return path.toString().includes("existing-app/package.json")
    })
    vi.mocked(prompts).mockResolvedValue({ type: "next", name: "existing-app" })

    mockExit = vi
      .spyOn(process, "exit")
      .mockImplementation(() => undefined as never)

    await createProject({
      cwd: "/test",
      force: false,
    })

    expect(mockExit).toHaveBeenCalledWith(1)
  })

  it("should throw error if path is not writable", async () => {
    vi.mocked(fs.access).mockRejectedValue(new Error("Permission denied"))
    vi.mocked(prompts).mockResolvedValue({ type: "next", name: "my-app" })

    mockExit = vi
      .spyOn(process, "exit")
      .mockImplementation(() => undefined as never)

    await createProject({
      cwd: "/test",
      force: false,
    })

    expect(mockExit).toHaveBeenCalledWith(1)
  })
})
