import {CliConfig} from '@sanity/cli-core'
import {ClientConfig} from '@sanity/client'
import {CLITelemetryStore} from '@sanity/cli-core'
import {Command} from '@oclif/core'
import {Config} from '@oclif/core'
import {Errors} from '@oclif/core'
import {Hook} from '@oclif/core/hooks'
import {Hooks} from '@oclif/core/hooks'
import {Mock} from 'vitest'
import nock from 'nock'
import {ProjectRootResult} from '@sanity/cli-core'
import {RawRequestOptions} from '@sanity/client'
import {SanityClient} from 'sanity'
import {SanityCommand} from '@sanity/cli-core'
import {TestProject} from 'vitest/node'

declare interface CaptureOptions {
  /**
   * Whether to print the output to the console
   */
  print?: boolean
  /**
   * Whether to strip ANSI escape codes from the output
   */
  stripAnsi?: boolean
  testNodeEnv?: string
}

declare interface CaptureResult<T = unknown> {
  stderr: string
  stdout: string
  error?: Error & Partial<Errors.CLIError>
  result?: T
}

declare type CommandClass = (new (argv: string[], config: Config) => Command) & typeof Command

/**
 * Converts Unix-style paths to platform-appropriate paths.
 * On Windows:
 *   - Absolute paths starting with '/': adds drive letter and converts to backslashes
 *   - Relative/partial paths: converts forward slashes to backslashes
 * On Unix: keeps paths as-is.
 *
 * @param pathStr - Unix-style path (e.g., '/test/path' or '.config/file.json')
 * @returns Platform-appropriate path
 * @internal
 */
export declare function convertToSystemPath(pathStr: string): string

/**
 * Creates a real Sanity client instance for testing that makes actual HTTP requests.
 * Use with mockApi() to intercept and mock the HTTP calls.
 *
 * @public
 *
 * @example
 * ```typescript
 * // Mock getGlobalCliClient to return a test client
 * vi.mock('@sanity/cli-core', async (importOriginal) => {
 *   const actual = await importOriginal<typeof import('@sanity/cli-core')>()
 *   const {createTestClient} = await import('@sanity/cli-test')
 *
 *   return {
 *     ...actual,
 *     getGlobalCliClient: vi.fn().mockImplementation((opts) => {
 *       return Promise.resolve(createTestClient({
 *         apiVersion: opts.apiVersion,
 *       }))
 *     }),
 *   }
 * })
 *
 * // Then use mockApi to intercept requests
 * mockApi({
 *   apiVersion: 'v2025-02-19',
 *   method: 'get',
 *   uri: '/media-libraries',
 * }).reply(200, {data: [...]})
 * ```
 */
export declare function createTestClient(options: CreateTestClientOptions): {
  client: SanityClient
  request: Mock<(options: RawRequestOptions) => Promise<any>>
}

/**
 * Options for createTestClient
 *
 * @public
 */
export declare interface CreateTestClientOptions extends ClientConfig {
  /**
   * API version for the client
   */
  apiVersion: string
  /**
   * Authentication token
   */
  token: string
}

/**
 * Creates a test token for the Sanity CLI
 *
 * @public
 *
 * @param token - The token to create
 * @returns void
 */
export declare function createTestToken(token: string): void

/**
 * Default fixtures bundled with the package and their options.
 *
 * @public
 */
export declare const DEFAULT_FIXTURES: Record<FixtureName, FixtureOptions>

/**
 * @deprecated Use {@link FixtureName} instead. This type alias will be removed in a future release.
 * @public
 */
export declare type ExampleName = FixtureName

/**
 * Valid fixture name type.
 * @public
 */
export declare type FixtureName =
  | 'basic-app'
  | 'basic-studio'
  | 'multi-workspace-studio'
  | 'prebuilt-app'
  | 'prebuilt-studio'
  | 'worst-case-studio'

/**
 * Options for each fixture.
 * @public
 */
export declare interface FixtureOptions {
  includeDist?: boolean
}

/**
 * Gets the current Windows drive letter from process.cwd().
 * Falls back to 'C:\\' if detection fails.
 *
 * @returns Drive letter with backslash (e.g., 'C:\\', 'D:\\') or empty string on Unix
 * @internal
 */
export declare function getCurrentDrive(): string

/**
 * Gets the path to the fixtures directory bundled with this package.
 *
 * The fixtures are copied during build and bundled with the published package.
 * This function works the same whether the package is used in a monorepo
 * or installed from npm.
 *
 * @returns Absolute path to the fixtures directory
 * @internal
 */
export declare function getFixturesPath(): string

/**
 * Gets the path to the temporary directory for test fixtures.
 *
 * Uses the initial working directory captured when this module was first loaded,
 * not process.cwd() which may change during test execution.
 *
 * @param customTempDir - Optional custom temp directory path
 * @returns Absolute path to temp directory (default: initial cwd/tmp)
 * @internal
 */
export declare function getTempPath(customTempDir?: string): string

/**
 * Mocks the API calls, add some defaults so it doesn't cause too much friction
 *
 * @internal
 */
export declare function mockApi({
  apiHost,
  apiVersion,
  includeQueryTag,
  method,
  projectId,
  query,
  uri,
}: MockApiOptions): nock.Interceptor

/**
 * @internal
 */
export declare interface MockApiOptions {
  /**
   * Uri to mock
   */
  uri: string
  /**
   * Api host to mock, defaults to `https://api.sanity.io`
   */
  apiHost?: string
  /**
   * Api version to mock, defaults to `v2025-05-14`
   */
  apiVersion?: string
  /**
   * Whether to include `tag: 'sanity.cli'` in query parameters.
   * Defaults to `true`. Set to `false` for endpoints that don't use CLI tagging.
   */
  includeQueryTag?: boolean
  /**
   * HTTP method to mock
   *
   * Defaults to 'get'
   */
  method?: 'delete' | 'get' | 'patch' | 'post' | 'put'
  /**
   * Project ID to mock. When provided, constructs apiHost as `https://{projectId}.api.sanity.io`
   * Takes precedence over apiHost if both are provided.
   */
  projectId?: string
  /**
   * Query parameters to mock
   */
  query?: Record<string, string>
}

/**
 * Creates a testable subclass of a command with mocked SanityCommand dependencies.
 *
 * @public
 *
 * @example
 * ```ts
 * // Basic config mocking
 * const TestAdd = mockSanityCommand(Add, {
 *   cliConfig: { api: { projectId: 'test-project' } }
 * })
 *
 * // With mock API client
 * const mockClient = {
 *   getDocument: vi.fn().mockResolvedValue({ _id: 'doc1', title: 'Test' }),
 *   fetch: vi.fn().mockResolvedValue([]),
 * }
 * const TestGet = mockSanityCommand(GetDocumentCommand, {
 *   cliConfig: { api: { projectId: 'test-project', dataset: 'production' } },
 *   projectApiClient: mockClient,
 * })
 *
 * const {stdout} = await testCommand(TestGet, ['doc1'])
 * expect(mockClient.getDocument).toHaveBeenCalledWith('doc1')
 * ```
 */
export declare function mockSanityCommand<T extends typeof SanityCommand<typeof Command>>(
  CommandClass: T,
  options?: MockSanityCommandOptions,
): T

/**
 * @public
 */
export declare interface MockSanityCommandOptions {
  /**
   * Mock CLI config (required if command uses getCliConfig or getProjectId)
   */
  cliConfig?: CliConfig
  /**
   * Mock whether the terminal is interactive (used by isUnattended)
   */
  isInteractive?: boolean
  /**
   * Mock project root result (required if command uses getProjectRoot)
   */
  projectRoot?: ProjectRootResult
  /**
   * Mock authentication token (passed to API clients, bypasses getCliToken)
   */
  token?: string
}

/**
 * @public
 * @param options - Options for mocking the telemetry store.
 * @returns The mocked telemetry store.
 */
export declare const mockTelemetry: (options?: MockTelemetryOptions) => CLITelemetryStore

/**
 * @public
 */
export declare interface MockTelemetryOptions {
  trace?: () => void
  updateUserProperties?: () => void
}

declare interface Options {
  config: Config
  Command?: Command.Class
  context?: Hook.Context
}

/**
 * Global setup function for initializing test fixtures.
 *
 * Copies fixtures from the bundled location to a temp directory
 * and installs dependencies.
 *
 * Note: Fixtures are NOT built during setup. Tests that need built
 * fixtures should build them as part of the test.
 *
 * This function is designed to be used with vitest globalSetup.
 *
 * @public
 *
 * @param options - Configuration options
 * @example
 * ```typescript
 * // In vitest.config.ts
 * export default defineConfig({
 *   test: {
 *     globalSetup: ['@sanity/cli-test/vitest']
 *   }
 * })
 * ```
 */
export declare function setup(_: TestProject, options?: SetupTestFixturesOptions): Promise<void>

/**
 * Options for setupTestFixtures
 *
 * @public
 */
export declare interface SetupTestFixturesOptions {
  /**
   * Glob patterns for additional fixture directories to set up.
   *
   * Each pattern is matched against directories in the current working directory.
   * Only directories containing a `package.json` file are included.
   *
   * @example
   * ```typescript
   * ['fixtures/*', 'dev/*']
   * ```
   */
  additionalFixtures?: string[]
  /**
   * Custom temp directory path. Defaults to process.cwd()/tmp
   */
  tempDir?: string
}

/**
 * Teardown function to clean up test fixtures.
 *
 * Removes the temp directory created by setupTestFixtures.
 *
 * This function is designed to be used with vitest globalSetup.
 *
 * @public
 *
 * @param options - Configuration options
 */
export declare function teardown(options?: TeardownTestFixturesOptions): Promise<void>

/**
 * Options for teardownTestFixtures
 *
 * @public
 */
export declare interface TeardownTestFixturesOptions {
  /**
   * Custom temp directory path. Defaults to process.cwd()/tmp
   */
  tempDir?: string
}

/**
 * @public
 */
export declare function testCommand(
  command: CommandClass,
  args?: string[],
  options?: TestCommandOptions,
): Promise<CaptureResult<unknown>>

/**
 * @public
 */
export declare interface TestCommandOptions {
  /**
   * Options for capturing output
   */
  capture?: CaptureOptions
  /**
   * Partial oclif config overrides
   */
  config?: Partial<Config>
  /**
   * Mock options for SanityCommand dependencies (config, project root, API clients).
   * When provided, the command is automatically wrapped with mockSanityCommand.
   */
  mocks?: MockSanityCommandOptions & MockTelemetryOptions
}

/**
 * Recursively copy a directory, skipping specified folders.
 *
 * @param srcDir - Source directory to copy from
 * @param destDir - Destination directory to copy to
 * @param skip - Array of directory/file names to skip (e.g., ['node_modules', 'dist'])
 * @internal
 */
export declare function testCopyDirectory(
  srcDir: string,
  destDir: string,
  skip?: string[],
): Promise<void>

/**
 * @deprecated Use {@link testFixture} instead. This function will be removed in a future release.
 *
 * Clones an example (now called fixture) directory into a temporary directory with an isolated copy.
 *
 * @param exampleName - The name of the example/fixture to clone (e.g., 'basic-app', 'basic-studio')
 * @param options - Configuration options
 * @returns The absolute path to the temporary directory containing the example/fixture
 *
 * @public
 */
export declare function testExample(
  exampleName: FixtureName | (string & {}),
  options?: TestFixtureOptions,
): Promise<string>

/**
 * @deprecated Use {@link TestFixtureOptions} instead. This type alias will be removed in a future release.
 * @public
 */
export declare type TestExampleOptions = TestFixtureOptions

/**
 * Clones a fixture directory into a temporary directory with an isolated copy.
 *
 * The function creates a unique temporary copy of the specified fixture with:
 * - A random unique ID to avoid conflicts between parallel tests
 * - Symlinked node_modules for performance (from the global setup version)
 * - Modified package.json name to prevent conflicts
 *
 * The fixture is first looked up in the temp directory (if global setup ran),
 * otherwise it falls back to the bundled fixtures in the package.
 *
 * @param fixtureName - The name of the fixture to clone (e.g., 'basic-app', 'basic-studio')
 * @param options - Configuration options
 * @returns The absolute path to the temporary directory containing the fixture
 *
 * @public
 *
 * @example
 * ```typescript
 * import {testFixture} from '@sanity/cli-test'
 * import {describe, test} from 'vitest'
 *
 * describe('my test suite', () => {
 *   test('should work with basic-studio', async () => {
 *     const cwd = await testFixture('basic-studio')
 *     // ... run your tests in this directory
 *   })
 * })
 * ```
 */
export declare function testFixture(
  fixtureName: FixtureName | (string & {}),
  options?: TestFixtureOptions,
): Promise<string>

/**
 * @public
 */
export declare interface TestFixtureOptions {
  /**
   * Custom temp directory. Defaults to process.cwd()/tmp
   */
  tempDir?: string
}

/**
 * Test an oclif hook
 *
 * @public
 *
 * @example
 * ```ts
 * const result = await testHook(hook, {
 *   Command: Command.loadable,
 *   context: {
 *     config: Config.load({
 *      // CLI root is the directory of the package that contains the hook.
 *       root: path.resolve(fileURLToPath(import.meta.url), '../../../root'),
 *     }),
 *   },
 * })
 * ```
 *
 * @param hook - The hook to test
 * @param options - The options for the hook
 * @returns The result of the hook
 */
export declare function testHook<T extends keyof Hooks>(
  hook: Hook<T>,
  options: Options,
): Promise<CaptureResult<Hooks[T]['return']>>

export {}
