import {BrowserContext, Locator, Page, Frame} from 'playwright-core';
import {TestType, Download} from '@playwright/test';
import type {Credentials} from '@mablhq-internal/typescript-api-client';

declare module '@mablhq/mabl-cli' {
  export type SnippetLogLevel = 'info' | 'warn' | 'error';
  export interface SnippetLogLine {
    message: string;
    level: SnippetLogLevel;
  }

  export type AuthServerPolicy = 'off' | 'none' | 'all';
  export enum TestResultStatus {
    passed = 'passed',
    skipped = 'skipped',
    failed = 'failed',
  }
  export interface TestResult {
    status: TestResultStatus;
    errorMessage?: string;
    testName: string;
    startTime: number;
    endTime: number;
  }
  export interface TestResults {
    numFailedTests: number;
    numPassedTests: number;
    numSkippedTests: number;
    numTotalTests: number;
    testResults: TestResult[];
    startTime: number;
    endTime: number;
    totalTimeSeconds: number;
    success: boolean;
  }
  export enum RunnerType {
    Playwright = 'playwright',
  }
  export interface HttpHeadersType {
    [key: string]: string;
  }
  export interface HttpConfig {
    proxyHost?: URL;
    sslVerify: boolean;
    proxyType?: string;
  }
  export interface TestRunConfig {
    authToken?: string;
    authServerPolicy?: AuthServerPolicy;
    basicAuthCredentialsId?: string;
    branchName?: string;
    branchChangesOnly?: boolean;
    credentialsId?: string;
    dataTableVariables?: Scenario;
    environmentId?: string;
    /**
     * An object containing additional HTTP headers to be sent with every request.
     */
    extraHttpHeaders?: HttpHeadersType;
    filterHttpRequests: boolean;
    fromPlanId?: string;
    headless?: boolean;
    height?: number;
    highlights?: boolean;
    interactionSpeed?: string;
    keepBrowserOpen?: boolean;
    labelsExclude?: string[];
    labelsInclude?: string[];
    mobileConfig?: MobileConfig;
    proxy?: HttpConfig;
    testId?: string;
    runId?: string;
    url?: string;
    width?: number;
    workspaceId?: string;
    runnerType?: RunnerType;
  }

  export interface Scenario {
    variables?: Array<Variable>;
  }
  export interface Variable {
    name?: string;
    value?: string;
  }

  export interface MablApiClient {}
  export class BrowserTestsRunner {
    run(): Promise<TestResults>;
  }
  export function createBrowserTestsRunner(
    testRunConfig: TestRunConfig,
    apiClient?: MablApiClient,
  ): Promise<BrowserTestsRunner>;
  export class MobileTestsRunner {
    run(): Promise<TestResults>;
  }
  export function createMobileTestsRunner(
    testRunConfig: TestRunConfig,
    apiClient?: MablApiClient,
  ): Promise<MobileTestsRunner>;
  export function getRunnerVersion(): string;

  /**
   * Authentication provider type.
   */
  export enum AuthProviderType {
    Auth0 = 'auth0',
    Mabl = 'mabl',
    Okta = 'okta',
  }

  /**
   * Authentication type.
   */
  export enum AuthType {
    ApiKey = 'api-key',
    Bearer = 'bearer',
  }

  /**
   * Authentication configuration.
   */
  export interface AuthenticationConfig {
    authProvider: AuthProviderType;
    authType: AuthType;
    accessToken: string;
    refreshToken?: string;
    accessTokenExpirationTimeMilliseconds: number;
    refreshTokenExpirationTimeMilliseconds?: number;
  }

  /**
   * Provider for authentication operations.
   */
  export class AuthenticationProvider {
    constructor(shouldExitOnError?: boolean);

    /**
     * Get the current auth config, auto-renewing tokens if needed/possible.
     */
    getAuthConfigWithAutoRenew(): Promise<AuthenticationConfig>;

    /**
     * Get the current authentication type.
     */
    static getAuthType(): Promise<AuthType>;

    /**
     * Display authentication info to the console.
     */
    displayInfo(): Promise<void>;

    /**
     * Clear the current authentication.
     */
    clearAuthentication(): Promise<void>;

    /**
     * Activate an API key for authentication.
     */
    activateApiKey(apiKey: string): Promise<void>;

    /**
     * Authenticate via browser-based OAuth flow.
     */
    authenticate(): Promise<void>;

    /**
     * Activate a JWT token for authentication.
     */
    activateJwt(jwt: string): Promise<void>;
  }

  /**
   * Get the current CLI version.
   */
  export function getCliVersion(): string;
}

export interface TrainingSessionOptions {
  authToken?: string;
  applicationId?: string;
  autoBranch?: boolean;
  branchName?: string;
  credentialsId?: string;
  deploymentId?: string;
  environmentId?: string;
  height: number;
  url?: string;
  width: number;
}
export interface CreateTestTrainingSessionOptions
  extends TrainingSessionOptions {
  autoLogin?: boolean;
  dataTableIds?: string[];
  labels?: string[];
  newPlanName?: string;
  planId?: string;
  testDescription?: string;
  testName: string;
  url: string;
  workspaceId: string;
}
export interface EditTrainingSessionOptions extends TrainingSessionOptions {
  testId?: string;
  testRunId?: string;
}
export declare function trainNewTest(
  trainingSessionOptions: CreateTestTrainingSessionOptions,
): Promise<void>;
export declare function editTest(
  trainingSessionOptions: EditTrainingSessionOptions,
): Promise<void>;

/**
 * Result of the shouldContinueGenerationCheck callback.
 * - `{ continue: true }` - Continue generation as normal
 * - `{ continue: false, save: boolean }` - Stop generation early, optionally saving the generated steps
 */
export type ContinueGenerationCheckResult =
  | {continue: true}
  | {continue: false; save: boolean};

/**
 * Callback function to check if generation should continue.
 * Called at the start of each iteration of the generation loop.
 */
export type ShouldContinueGenerationCheck = () =>
  | ContinueGenerationCheckResult
  | Promise<ContinueGenerationCheckResult>;

export interface ProxySpec {
  server: string;
  username?: string;
  password?: string;
  bypass?: string;
}

/**
 * Base options shared by all agentic generation config variants.
 */
interface AgenticGenerationBaseOptions {
  headless?: boolean;
  verbose?: boolean;
  /** Whether this generation is running in the cloud (e.g. Cloud Run TAA).
   *  When true, cloud-only credentials are allowed. */
  isCloud?: boolean;
  apiClient: any;
  appVersion: string;
  autoSave?: boolean;
  labels?: string[];
  /** Data table IDs to associate with the saved test */
  datatables?: string[];
  /** Branch ID to save the test on */
  branchId?: string;
  /** Plan ID to add the test to after save */
  planId?: string;
  /** Device emulation preset (e.g. "iphone_15") */
  devicePreset?: string;
  /** Device orientation (e.g. "portrait_primary") */
  deviceOrientation?: string;
  /** Scenario (data table row) ID to associate with the saved test */
  scenarioId?: string;
  /**
   * Pre-fetched Credentials object to use instead of fetching by credentialsId.
   * Used by cloud TAA to pass credentials fetched with the workspace API key,
   * which can access cloud-only credentials that user OAuth tokens cannot.
   */
  credentials?: Credentials;
  /**
   * Optional callback to check if generation should continue.
   * Called at the start of each iteration of the generation loop.
   * Return `{ continue: true }` to continue, or `{ continue: false, save: boolean }`
   * to stop early, optionally saving the generated steps.
   */
  shouldContinueGenerationCheck?: ShouldContinueGenerationCheck;
  /**
   * Output base64-encoded screenshots to stdout during generation.
   * Format: [SCREENSHOT] {"name":"step_N","data":"base64...","mimeType":"image/png"}
   *
   * NOTE: This flag is intended for local CLI execution only and should not be used
   * for broad screenshot capture in cloud or automated environments.
   */
  outputScreenshots?: boolean;
  /** Optional proxy server for the browser to use (e.g. for Link agent support) */
  proxy?: ProxySpec;
}

/**
 * Options for creating a new authoring session with explicit parameters.
 */
export interface AgenticGenerationNewSessionOptions
  extends AgenticGenerationBaseOptions {
  type: 'new';
  url: string;
  testIntent: string;
  workspaceId: string;
  applicationId?: string;
  /** Test name (required for save) */
  testName?: string;
  /** Test description (defaults to testIntent) */
  testDescription?: string;
}

/**
 * Options for loading from an existing agent session.
 * Test name and description come from the session's test_information artifact.
 */
export interface AgenticGenerationExistingSessionOptions
  extends AgenticGenerationBaseOptions {
  type: 'existing';
  agentSessionId: string;
}

/**
 * Options for running agentic test generation.
 * Use type: 'new' to create a new session, or type: 'existing' to load from an existing session.
 */
export type AgenticGenerationOptions =
  | AgenticGenerationNewSessionOptions
  | AgenticGenerationExistingSessionOptions;

/**
 * Result of agentic test generation.
 */
export interface AgenticGenerationResult {
  success: boolean;
  error?: string;
  /**
   * The original error when an unexpected infrastructure failure occurred
   * (e.g., browser failed to launch, API returned 5xx during setup).
   * Callers can rethrow this to signal a fatal job failure for monitoring.
   */
  fatalError?: Error;
  steps: string[];
  trainingSessionId: string;
  rawSteps: any[];
  saveResult?: {
    success: boolean;
    testId?: string;
    flowId?: string;
    testUrl?: string;
    error?: string;
  };
}

export declare function runAgenticGeneration(
  options: AgenticGenerationOptions,
): Promise<AgenticGenerationResult>;

export interface MobileConfig {
  appiumAddress?: string;
  appiumHome?: string;
  appiumPort?: string;
  avdName?: string;
  isRealDevice?: boolean;
  mobileAppFileId?: string;
  path?: string;
  platformName: MobilePlatformEnum;
  udid?: string;
}

export declare enum MobilePlatformEnum {
  Android = 'android',
  Ios = 'ios',
}

/**
 * Options for the MablToolset
 */
export interface MablToolsetOptions {
  /** an API key to use to authenticate the MablApiClient */
  apiKey: string;
  /** A playwright browser context to use */
  browserContext?: BrowserContext;
  /** The workspace ID to use with the mabl tools, if not supplied an API request will be attempted to determine the correct ID */
  workspaceId?: string;
  /** The Playwright test */
  test: TestType<any, any>;
}

/**
 * The response from the gen AI assertion.
 */
export interface GenAiAssertionResponse {
  /**
   * Whether the assertion passed or failed
   */
  success: boolean;
  /**
   * The explanation for the assertion
   */
  explanation?: string;
  /**
   * The summary of the assertion
   */
  summary?: string;
  /**
   * The criteria for the assertion
   */
  assertionCriteria?: string[];
}

export interface DatabaseQueryParameter {
  id?: string;
  type: 'String' | 'Number' | 'Boolean';
  value: string;
}

export interface DatabaseQueryResultSet {
  workspace_id?: string;
  database_query_execution_id?: string;
  metadata?: {
    status?: 'success' | 'failed' | 'error';
    duration?: number;
    message?: string;
    error_message?: string;
    row_count?: number;
    column_count?: number;
    rows_affected?: number;
    additional_properties?: object;
  };
  columns?: any[];
  rows?: Array<{
    [key: string]: any;
  }>;
}

/**
 * The MablToolset is set of mabl features that can be used in Playwright tests
 */
export interface MablToolset {
  constructor(options: MablToolsetOptions);

  /**
   * Evaluates a gen AI prompt against the current state of the browser or specified element and returns the result of the evaluation.
   * @param page The page to evaluate the gen AI prompt against
   * @param prompt The gen AI prompt to evaluate
   * @param locator optional element to run the prompt against, the bounding box of the element will be used to produce the screenshot used in the assertion
   * @param filePaths Optional array of file paths to be evaluated along with the screenshot. Content type will be inferred from file extension.
   * @returns
   */
  evaluateGenAiAssertion(
    page: Page,
    prompt: string,
    locator?: Locator,
  ): Promise<GenAiAssertionResponse>;

  /**
   * Evaluates a gen AI prompt against a file (e.g., downloaded PDF, CSV, image, etc.).
   * This method is optimized for file-only assertions and doesn't require element selection.
   * @param page The page (used for test context)
   * @param prompt The gen AI prompt to evaluate
   * @param file The file to evaluate
   * @returns
   */
  evaluateGenAiAssertionInFile(
    page: Page,
    prompt: string,
    file: Download,
  ): Promise<GenAiAssertionResponse>;

  /**
   * Initializes the mabl tooling and execution context to work with the provided Playwright BrowserContext
   * @param browserContext the Playwright BrowserContext to use with the MablTools
   */
  initializeContext(browserContext: BrowserContext): Promise<void>;

  /**
   * Calls into the mabl API to create a new email address that can be used to receive emails during the external test run.
   * @returns a new email address that can be used to receive emails
   */
  createMailBoxAddress(): Promise<string>;

  /**
   * Run an email find action to locate an email that matches the provided email descriptor.
   * @param emailAddress the email address to wait for
   * @param page the page to wait for the email on
   * @param emailFindOptions optional options to use in locating the email
   * @returns a URL that can be used to open the email in a browser
   */
  waitForEmail(
    emailAddress: string,
    page: Page | Frame,
    emailFindOptions?: EmailFindOptions,
  ): Promise<MablMailboxEmail>;

  /**
   * Get a the username/password from a mabl credential to use in a playwright test
   * @param credentialId The ID of the credential to get
   * @returns The username/password credential
   */
  getCredentials(credentialsId: string): Promise<UsernamePasswordCredential>;

  /**
   * Get an MFA code for a credential
   * @param credentialId The ID of the credential to get the MFA code for
   * @returns The MFA code
   */
  getMfaCode(credentialsId: string): Promise<string>;

  /**
   * Executes a database query
   * @param dbConnectionId The ID of the database connection to use
   * @param query The query to execute
   * @param queryParameters Optional parameters to use in the query
   * @param environmentId Optional environment ID to use in the query
   * @param timeoutMillis Optional timeout in milliseconds for the query
   * @returns The result of the query
   */
  executeQuery(
    dbConnectionId: string,
    query: string,
    queryParameters?: DatabaseQueryParameter[],
    environmentId?: string,
    timeoutMillis?: number,
  ): Promise<DatabaseQueryResultSet>;

  /**
   * Opens a PDF file in the supplied Page for use in Playwright
   * @param filePath The path to the PDF file to open
   * @param page The page to open the PDF file in
   */
  openPdfFile(filePath: string, page: Page): Promise<void>;
}

export interface MablFixtures {
  mabl: MablToolset;
}

export declare function createMablFixtures(options?: MablToolsetOptions): {
  mabl: ({context}: any, use: (arg0: MablToolset) => any) => Promise<void>;
};

/**
 * Options for finding an email
 */
export interface EmailFindOptions {
  /**
   * The subject of the email to find
   */
  subject?: string;
  /**
   * The from address of the email to find
   */
  from?: string;
  /**
   * The body of the email to find
   */
  body?: string;
  /**
   * The amount of time to look back for an email in milliseconds.
   * E.g. if lookbackTimeMs is 10_000, it will ignore any emails that were received before 10 seconds ago.
   */
  lookbackTimeMs?: number;
  /**
   * The amount of time to wait for an email to be found in milliseconds.
   * E.g. if timeoutMillis is 10_000, it will wait for 10 seconds for an email to be found.
   */
  timeoutMillis?: number;
}

/**
 * A username and password credential
 */
export interface UsernamePasswordCredential {
  /**
   * The username for the credential
   */
  username: string;
  /**
   * The password for the credential
   */
  password: string;
}

/**
 * A mailbox email
 */
export interface MablMailboxEmail {
  /**
   * The URL to access to the email preview.
   */
  readonly emailUrl: string;
  /**
   * The page the email is on
   */
  readonly page: Page;
  /**
   * The locator in the email preview for the to address of the email.
   */
  readonly toAddress: Locator;
  /**
   * The locator for the subject of the email
   */
  readonly subject: Locator;
  /**
   * The locator in the email preview for the body of the email.
   */
  readonly body: Locator;
  /**
   * The locator in the email preview for the from address of the email.
   */
  readonly fromAddress: Locator;

  /**
   * Opens the email preview in the browser.
   */
  open(): Promise<void>;

  /**
   * Get the to address of the email.
   * @returns The to address of the email.
   */
  getToAddress(): Promise<string | null>;

  /**
   * Get the from address of the email.
   * @returns The from address of the email.
   */
  getFromAddress(): Promise<string | null>;

  /**
   * Get the subject of the email.
   * @returns The subject of the email.
   */
  getSubject(): Promise<string | null>;

  /**
   * Get the body of the email.
   * @returns The body of the email.
   */
  getBodyText(): Promise<string | null>;

  /**
   * Get the locator for the body of the email.
   * @returns The locator for the body of the email.
   */
  getBodyLocator(): Locator;
}
