/**
 * biome-ignore-all lint/correctness/noEmptyPattern: oddly enough in extend below this is required
 * see https://vitest.dev/guide/test-context.html#extend-test-context
 */
import { type APIRequestContext, test as base } from "@playwright/test";
import type { SupportedProvider } from "@shared";
import {
  ADMIN_EMAIL,
  ADMIN_PASSWORD,
  adminAuthFile,
  editorAuthFile,
  getE2eRequestUrl,
  KEYCLOAK_OIDC,
  LLM_MODELS_ROUTE,
  LLM_PROVIDER_API_KEYS_AVAILABLE_ROUTE,
  LLM_PROVIDER_API_KEYS_ROUTE,
  memberAuthFile,
  SYNC_LLM_MODELS_ROUTE,
  UI_BASE_URL,
  WIREMOCK_BASE_URL,
} from "../consts";

export {
  LLM_MODELS_ROUTE,
  LLM_PROVIDER_API_KEYS_AVAILABLE_ROUTE,
  LLM_PROVIDER_API_KEYS_ROUTE,
  SYNC_LLM_MODELS_ROUTE,
};

/**
 * Playwright test extension with fixtures
 * https://playwright.dev/docs/test-fixtures#creating-a-fixture
 */
export interface TestFixtures {
  makeApiRequest: typeof makeApiRequest;
  createAgent: typeof createAgent;
  createLlmProxy: typeof createLlmProxy;
  createMcpGateway: typeof createMcpGateway;
  deleteAgent: typeof deleteAgent;
  createApiKey: typeof createApiKey;
  deleteApiKey: typeof deleteApiKey;
  createIdentityProvider: typeof createIdentityProvider;
  deleteIdentityProvider: typeof deleteIdentityProvider;
  createToolInvocationPolicy: typeof createToolInvocationPolicy;
  deleteToolInvocationPolicy: typeof deleteToolInvocationPolicy;
  createTrustedDataPolicy: typeof createTrustedDataPolicy;
  deleteTrustedDataPolicy: typeof deleteTrustedDataPolicy;
  createMcpCatalogItem: typeof createMcpCatalogItem;
  deleteMcpCatalogItem: typeof deleteMcpCatalogItem;
  installMcpServer: typeof installMcpServer;
  uninstallMcpServer: typeof uninstallMcpServer;
  createRole: typeof createRole;
  deleteRole: typeof deleteRole;
  createTeam: typeof createTeam;
  deleteTeam: typeof deleteTeam;
  waitForAgentTool: typeof waitForAgentTool;
  waitForProxyTool: typeof waitForProxyTool;
  getTeamByName: typeof getTeamByName;
  addTeamMember: typeof addTeamMember;
  removeTeamMember: typeof removeTeamMember;
  getActiveOrganizationId: typeof getActiveOrganizationId;
  createOptimizationRule: typeof createOptimizationRule;
  deleteOptimizationRule: typeof deleteOptimizationRule;
  updateOptimizationRule: typeof updateOptimizationRule;
  createLimit: typeof createLimit;
  deleteLimit: typeof deleteLimit;
  getLimits: typeof getLimits;
  getModels: typeof getModels;
  syncModels: typeof syncModels;
  updateModelPricing: typeof updateModelPricing;
  getOrganization: typeof getOrganization;
  updateLlmSettings: typeof updateLlmSettings;
  updateSecuritySettings: typeof updateSecuritySettings;
  updateKnowledgeSettings: typeof updateKnowledgeSettings;
  getInteractions: typeof getInteractions;
  getWiremockRequests: typeof getWiremockRequests;
  clearWiremockRequests: typeof clearWiremockRequests;
  createKnowledgeBase: typeof createKnowledgeBase;
  deleteKnowledgeBase: typeof deleteKnowledgeBase;
  createConnector: typeof createConnector;
  deleteConnector: typeof deleteConnector;
  /** API request context authenticated as admin (same as default `request`) */
  adminRequest: APIRequestContext;
  /** API request context authenticated as editor */
  editorRequest: APIRequestContext;
  /** API request context authenticated as member */
  memberRequest: APIRequestContext;
}

const makeApiRequest = async ({
  request,
  method,
  urlSuffix,
  data = null,
  headers = {
    "Content-Type": "application/json",
    Origin: UI_BASE_URL,
  },
  ignoreStatusCheck = false,
}: {
  request: APIRequestContext;
  method: "get" | "post" | "put" | "patch" | "delete";
  urlSuffix: string;
  data?: unknown;
  headers?: Record<string, string>;
  ignoreStatusCheck?: boolean;
}) => {
  const makeRequest = () =>
    request[method](getE2eRequestUrl(urlSuffix), {
      headers,
      data,
    });

  let response = await makeRequest();

  if (!ignoreStatusCheck && response.status() === 403) {
    await refreshAdminSession(request);
    response = await makeRequest();
  }

  if (!ignoreStatusCheck && response.status() === 401) {
    await refreshAdminSession(request);
    response = await makeRequest();
  }

  if (!ignoreStatusCheck && !response.ok()) {
    throw new Error(
      `Failed to ${method} ${urlSuffix} with data ${JSON.stringify(
        data,
      )}: ${response.status()} ${await response.text()}`,
    );
  }

  return response;
};

async function refreshAdminSession(request: APIRequestContext): Promise<void> {
  const response = await request.post(
    getE2eRequestUrl("/api/auth/sign-in/email"),
    {
      data: {
        email: ADMIN_EMAIL,
        password: ADMIN_PASSWORD,
      },
      headers: {
        "Content-Type": "application/json",
        Cookie: "",
        Origin: UI_BASE_URL,
      },
    },
  );

  if (!response.ok()) {
    throw new Error(
      `Failed to refresh admin session: ${response.status()} ${await response.text()}`,
    );
  }

  const permissionsResponse = await request.get(
    getE2eRequestUrl("/api/user/permissions"),
    {
      headers: { Origin: UI_BASE_URL },
    },
  );

  if (!permissionsResponse.ok()) {
    throw new Error(
      `Failed to verify refreshed admin session: ${permissionsResponse.status()} ${await permissionsResponse.text()}`,
    );
  }

  const permissions = await permissionsResponse.json();
  if (
    !permissions?.identityProvider?.includes("create") ||
    !permissions?.mcpRegistry?.includes("create") ||
    !permissions?.toolPolicy?.includes("create")
  ) {
    throw new Error(
      `Refreshed session does not have admin permissions: ${JSON.stringify(permissions)}`,
    );
  }
}

function extractPaginatedArray<T>(data: unknown): T[] {
  if (Array.isArray(data)) {
    return data as T[];
  }

  if (
    data &&
    typeof data === "object" &&
    "data" in data &&
    Array.isArray(data.data)
  ) {
    return data.data as T[];
  }

  return [];
}

/**
 * Create an agent
 * (authnz is handled by the authenticated session)
 */
const createAgent = async (
  request: APIRequestContext,
  name: string,
  scope: "personal" | "team" | "org",
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/agents",
    data: {
      name,
      teams: [],
      scope,
    },
  });

/**
 * Create an LLM Proxy
 * (authnz is handled by the authenticated session)
 */
const createLlmProxy = async (
  request: APIRequestContext,
  name: string,
  scope: "personal" | "team" | "org",
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/agents",
    data: {
      name,
      teams: [],
      agentType: "llm_proxy",
      scope,
    },
  });

/**
 * Create an MCP Gateway
 * (authnz is handled by the authenticated session)
 */
const createMcpGateway = async (
  request: APIRequestContext,
  name: string,
  scope: "personal" | "team" | "org",
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/agents",
    data: {
      name,
      teams: [],
      agentType: "mcp_gateway",
      scope,
    },
  });

/**
 * Delete an agent
 * (authnz is handled by the authenticated session)
 */
const deleteAgent = async (request: APIRequestContext, agentId: string) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/agents/${agentId}`,
  });

/**
 * Create an API key
 * (authnz is handled by the authenticated session)
 */
const createApiKey = async (
  request: APIRequestContext,
  name: string = "Test API Key",
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/auth/api-key/create",
    data: {
      name,
      expiresIn: 60 * 60 * 24 * 7, // 1 week
    },
  });

/**
 * Delete an API key by ID
 * (authnz is handled by the authenticated session)
 */
const deleteApiKey = async (request: APIRequestContext, keyId: string) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/auth/api-key/delete",
    data: {
      keyId,
    },
  });

/**
 * Create an identity provider (SSO provider) via the API with OIDC config pointing to Keycloak.
 * Returns the created provider's ID.
 */
const createIdentityProvider = async (
  request: APIRequestContext,
  providerId: string,
  options?: {
    domain?: string;
    oidcConfig?: {
      issuer?: string;
      skipDiscovery?: boolean;
      pkce?: boolean;
      clientId?: string;
      clientSecret?: string;
      authorizationEndpoint?: string;
      discoveryEndpoint?: string;
      userInfoEndpoint?: string;
      tokenEndpoint?: string;
      tokenEndpointAuthentication?:
        | "client_secret_post"
        | "client_secret_basic"
        | "private_key_jwt";
      jwksEndpoint?: string;
    };
    enterpriseManagedCredentials?: {
      clientId?: string;
      clientSecret?: string;
      tokenEndpoint?: string;
      tokenEndpointAuthentication?:
        | "client_secret_post"
        | "client_secret_basic"
        | "private_key_jwt";
      subjectTokenType?:
        | "urn:ietf:params:oauth:token-type:access_token"
        | "urn:ietf:params:oauth:token-type:id_token"
        | "urn:ietf:params:oauth:token-type:jwt";
    };
  },
): Promise<string> => {
  const oidcConfig = {
    issuer: options?.oidcConfig?.issuer ?? KEYCLOAK_OIDC.issuer,
    skipDiscovery: options?.oidcConfig?.skipDiscovery,
    pkce: options?.oidcConfig?.pkce ?? true,
    clientId: options?.oidcConfig?.clientId ?? KEYCLOAK_OIDC.clientId,
    clientSecret:
      options?.oidcConfig?.clientSecret ?? KEYCLOAK_OIDC.clientSecret,
    authorizationEndpoint: options?.oidcConfig?.authorizationEndpoint,
    discoveryEndpoint:
      options?.oidcConfig?.discoveryEndpoint ?? KEYCLOAK_OIDC.discoveryEndpoint,
    userInfoEndpoint: options?.oidcConfig?.userInfoEndpoint,
    tokenEndpoint: options?.oidcConfig?.tokenEndpoint,
    tokenEndpointAuthentication:
      options?.oidcConfig?.tokenEndpointAuthentication,
    jwksEndpoint:
      options?.oidcConfig?.jwksEndpoint ?? KEYCLOAK_OIDC.jwksEndpoint,
  };

  const response = await makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/identity-providers",
    data: {
      providerId,
      issuer: oidcConfig.issuer,
      domain: options?.domain ?? "jwks-test.example.com",
      oidcConfig: {
        ...oidcConfig,
        ...(options?.enterpriseManagedCredentials
          ? {
              enterpriseManagedCredentials:
                options.enterpriseManagedCredentials,
            }
          : {}),
      },
    },
  });

  const provider = await response.json();
  return provider.id;
};

/**
 * Delete an identity provider (SSO provider) via the API.
 */
const deleteIdentityProvider = async (
  request: APIRequestContext,
  id: string,
): Promise<void> => {
  await makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/identity-providers/${id}`,
    ignoreStatusCheck: true,
  });
};

/**
 * Create a tool invocation policy
 * (authnz is handled by the authenticated session)
 */
const createToolInvocationPolicy = async (
  request: APIRequestContext,
  policy: {
    toolId: string;
    conditions: Array<{ key: string; operator: string; value: string }>;
    action:
      | "allow_when_context_is_untrusted"
      | "block_when_context_is_untrusted"
      | "block_always";
    reason?: string;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/autonomy-policies/tool-invocation",
    data: {
      toolId: policy.toolId,
      conditions: policy.conditions,
      action: policy.action,
      reason: policy.reason,
    },
  });

/**
 * Delete a tool invocation policy
 * (authnz is handled by the authenticated session)
 */
const deleteToolInvocationPolicy = async (
  request: APIRequestContext,
  policyId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/autonomy-policies/tool-invocation/${policyId}`,
  });

/**
 * Create a trusted data policy
 * (authnz is handled by the authenticated session)
 */
const createTrustedDataPolicy = async (
  request: APIRequestContext,
  policy: {
    toolId: string;
    conditions: Array<{ key: string; operator: string; value: string }>;
    action:
      | "block_always"
      | "mark_as_trusted"
      | "mark_as_untrusted"
      | "sanitize_with_dual_llm";
    description?: string;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/trusted-data-policies",
    data: {
      toolId: policy.toolId,
      conditions: policy.conditions,
      action: policy.action,
      description: policy.description,
    },
  });

/**
 * Delete a trusted data policy
 * (authnz is handled by the authenticated session)
 */
const deleteTrustedDataPolicy = async (
  request: APIRequestContext,
  policyId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/trusted-data-policies/${policyId}`,
  });

/**
 * Create an MCP catalog item
 * (authnz is handled by the authenticated session)
 */
const createMcpCatalogItem = async (
  request: APIRequestContext,
  catalogItem: {
    name: string;
    description: string;
    serverType: "local" | "remote";
    localConfig?: unknown;
    serverUrl?: string;
    authFields?: unknown;
    labels?: Array<{ key: string; value: string }>;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/internal_mcp_catalog",
    data: catalogItem,
  });

/**
 * Delete an MCP catalog item
 * (authnz is handled by the authenticated session)
 */
const deleteMcpCatalogItem = async (
  request: APIRequestContext,
  catalogId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/internal_mcp_catalog/${catalogId}`,
  });

/**
 * Install an MCP server
 * (authnz is handled by the authenticated session)
 */
const installMcpServer = async (
  request: APIRequestContext,
  serverData: {
    name: string;
    catalogId?: string;
    scope?: "personal" | "team" | "org";
    teamId?: string;
    userConfigValues?: Record<string, string>;
    environmentValues?: Record<string, string>;
    accessToken?: string;
    agentIds?: string[];
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/mcp_server",
    data: serverData,
  });

/**
 * Uninstall an MCP server
 * (authnz is handled by the authenticated session)
 */
const uninstallMcpServer = async (
  request: APIRequestContext,
  serverId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/mcp_server/${serverId}`,
  });

/**
 * Create a custom role
 * (authnz is handled by the authenticated session)
 */
const createRole = async (
  request: APIRequestContext,
  roleData: {
    name: string;
    permission: Record<string, string[]>;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/roles",
    data: roleData,
  });

/**
 * Delete a role by ID
 * (authnz is handled by the authenticated session)
 */
const deleteRole = async (request: APIRequestContext, roleId: string) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/roles/${roleId}`,
  });

/**
 * Create a team
 * (authnz is handled by the authenticated session)
 */
const createTeam = async (
  request: APIRequestContext,
  name: string,
  description?: string,
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/teams",
    data: { name, ...(description != null && { description }) },
  });

/**
 * Delete a team by ID
 * (authnz is handled by the authenticated session)
 */
const deleteTeam = async (request: APIRequestContext, teamId: string) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/teams/${teamId}`,
  });

/**
 * Wait for an agent-tool to be registered with retry/polling logic.
 * This helps avoid race conditions when a tool is registered asynchronously.
 * In CI with parallel workers, tool registration can take longer due to resource contention.
 *
 * IMPORTANT: Uses server-side filtering by agentId to avoid pagination issues.
 * The default API limit is 20 items, so without filtering, the tool might not
 * appear in results if there are many agent-tools in the database.
 */
const waitForAgentTool = async (
  request: APIRequestContext,
  agentId: string,
  toolName: string,
  options?: {
    maxAttempts?: number;
    delayMs?: number;
  },
): Promise<{
  id: string;
  agent: { id: string };
  tool: { id: string; name: string };
}> => {
  // Increased defaults for CI stability: 20 attempts × 1000ms = 20 seconds total wait
  const maxAttempts = options?.maxAttempts ?? 20;
  const delayMs = options?.delayMs ?? 1000;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    // Use server-side filtering by agentId and increase limit to avoid pagination issues
    const agentToolsResponse = await makeApiRequest({
      request,
      method: "get",
      urlSuffix: `/api/agent-tools?agentId=${agentId}&limit=100`,
      ignoreStatusCheck: true,
    });

    if (agentToolsResponse.ok()) {
      const agentTools = await agentToolsResponse.json();
      // Defense-in-depth: validate both agentId AND toolName client-side
      // in case the API silently ignores unknown query params
      const foundTool = agentTools.data.find(
        (at: { agent: { id: string }; tool: { id: string; name: string } }) =>
          at.agent.id === agentId && at.tool.name === toolName,
      );

      if (foundTool) {
        return foundTool;
      }
    }

    if (attempt < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }

  throw new Error(
    `Agent-tool '${toolName}' for agent '${agentId}' not found after ${maxAttempts} attempts`,
  );
};

/**
 * Wait for a proxy-discovered tool to appear in the tools list.
 * Queries GET /api/tools/with-assignments filtered by name and llm-proxy origin.
 */
const waitForProxyTool = async (
  request: APIRequestContext,
  toolName: string,
  options?: {
    maxAttempts?: number;
    delayMs?: number;
  },
): Promise<{
  id: string;
  name: string;
  description: string | null;
  catalogId: string | null;
}> => {
  const maxAttempts = options?.maxAttempts ?? 20;
  const delayMs = options?.delayMs ?? 1000;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await makeApiRequest({
      request,
      method: "get",
      urlSuffix: `/api/tools/with-assignments?search=${encodeURIComponent(toolName)}&origin=llm-proxy`,
      ignoreStatusCheck: true,
    });

    if (response.ok()) {
      const result = await response.json();
      const found = result.data.find(
        (t: { name: string }) => t.name === toolName,
      );
      if (found) return found;
    }

    if (attempt < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }

  throw new Error(
    `Proxy tool '${toolName}' not found after ${maxAttempts} attempts`,
  );
};

/**
 * Get a team by name (includes members)
 */
export const getTeamByName = async (
  request: APIRequestContext,
  teamName: string,
): Promise<{
  id: string;
  name: string;
  members: Array<{ userId: string; email: string }>;
}> => {
  const teamsResponse = await makeApiRequest({
    request,
    method: "get",
    urlSuffix: "/api/teams",
  });
  const teams = extractPaginatedArray<{ id: string; name: string }>(
    await teamsResponse.json(),
  );
  const team = teams.find((t: { name: string }) => t.name === teamName);
  if (!team) {
    throw new Error(`Team '${teamName}' not found`);
  }

  // Get team members
  const membersResponse = await makeApiRequest({
    request,
    method: "get",
    urlSuffix: `/api/teams/${team.id}/members`,
  });
  const members = await membersResponse.json();

  return { ...team, members };
};

/**
 * Add a member to a team
 */
const addTeamMember = async (
  request: APIRequestContext,
  teamId: string,
  userId: string,
  role: "member" | "owner" = "member",
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: `/api/teams/${teamId}/members`,
    data: { userId, role },
  });

/**
 * Remove a member from a team
 */
export const removeTeamMember = async (
  request: APIRequestContext,
  teamId: string,
  userId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/teams/${teamId}/members/${userId}`,
  });

/**
 * Get the active organization ID from the current session
 */
const getActiveOrganizationId = async (
  request: APIRequestContext,
): Promise<string> => {
  const response = await makeApiRequest({
    request,
    method: "get",
    urlSuffix: "/api/auth/get-session",
  });
  const data = await response.json();
  const organizationId = data?.session?.activeOrganizationId;
  if (!organizationId) {
    throw new Error("Failed to get organization ID from session");
  }
  return organizationId;
};

/**
 * Optimization rule condition types
 */
type OptimizationRuleCondition = { maxLength: number } | { hasTools: boolean };

/**
 * Create an optimization rule
 * (authnz is handled by the authenticated session)
 */
const createOptimizationRule = async (
  request: APIRequestContext,
  rule: {
    entityType: "organization" | "team" | "agent";
    entityId: string;
    provider: SupportedProvider;
    conditions: OptimizationRuleCondition[];
    targetModel: string;
    enabled?: boolean;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/optimization-rules",
    data: {
      ...rule,
      enabled: rule.enabled ?? true,
    },
  });

/**
 * Update an optimization rule
 * (authnz is handled by the authenticated session)
 */
const updateOptimizationRule = async (
  request: APIRequestContext,
  ruleId: string,
  updates: {
    conditions?: OptimizationRuleCondition[];
    targetModel?: string;
    enabled?: boolean;
  },
) =>
  makeApiRequest({
    request,
    method: "put",
    urlSuffix: `/api/optimization-rules/${ruleId}`,
    data: updates,
  });

/**
 * Delete an optimization rule
 * (authnz is handled by the authenticated session)
 */
const deleteOptimizationRule = async (
  request: APIRequestContext,
  ruleId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/optimization-rules/${ruleId}`,
  });

/**
 * Create a limit (token cost, mcp_server_calls, or tool_calls)
 * (authnz is handled by the authenticated session)
 */
const createLimit = async (
  request: APIRequestContext,
  limit: {
    entityType: "organization" | "team" | "agent";
    entityId: string;
    limitType: "token_cost" | "mcp_server_calls" | "tool_calls";
    limitValue: number;
    model?: string[];
    mcpServerName?: string;
    toolName?: string;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/limits",
    data: limit,
  });

/**
 * Delete a limit by ID
 * (authnz is handled by the authenticated session)
 */
const deleteLimit = async (request: APIRequestContext, limitId: string) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/limits/${limitId}`,
  });

/**
 * Get limits with optional filtering
 * (authnz is handled by the authenticated session)
 */
const getLimits = async (
  request: APIRequestContext,
  entityType?: "organization" | "team" | "agent",
  entityId?: string,
) => {
  const params = new URLSearchParams();
  if (entityType) params.append("entityType", entityType);
  if (entityId) params.append("entityId", entityId);
  const queryString = params.toString();
  return makeApiRequest({
    request,
    method: "get",
    urlSuffix: `/api/limits${queryString ? `?${queryString}` : ""}`,
  });
};

/**
 * Get all models with their API keys and capabilities
 * (authnz is handled by the authenticated session)
 */
const getModels = async (request: APIRequestContext) =>
  makeApiRequest({
    request,
    method: "get",
    urlSuffix: LLM_MODELS_ROUTE,
  });

/**
 * Trigger a model sync from all providers.
 * Useful in tests to ensure models are synced when WireMock may not have been
 * ready during backend startup seed.
 * (authnz is handled by the authenticated session)
 */
const syncModels = async (request: APIRequestContext) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: SYNC_LLM_MODELS_ROUTE,
  });

/**
 * Update custom pricing for a model by its internal UUID.
 * Set prices to null to reset to default pricing.
 * (authnz is handled by the authenticated session)
 */
const updateModelPricing = async (
  request: APIRequestContext,
  modelId: string,
  pricing: {
    customPricePerMillionInput: string | null;
    customPricePerMillionOutput: string | null;
  },
) =>
  makeApiRequest({
    request,
    method: "patch",
    urlSuffix: `${LLM_MODELS_ROUTE}/${modelId}`,
    data: pricing,
  });

/**
 * Get organization details
 * (authnz is handled by the authenticated session)
 */
const getOrganization = async (request: APIRequestContext) =>
  makeApiRequest({
    request,
    method: "get",
    urlSuffix: "/api/organization",
  });

/**
 * Update LLM settings (compression, cleanup interval)
 * (authnz is handled by the authenticated session)
 */
const updateLlmSettings = async (
  request: APIRequestContext,
  updates: {
    convertToolResultsToToon?: boolean;
    compressionScope?: "organization" | "team";
    limitCleanupInterval?: "1h" | "12h" | "24h" | "1w" | "1m";
  },
) =>
  makeApiRequest({
    request,
    method: "patch",
    urlSuffix: "/api/organization/llm-settings",
    data: updates,
  });

/**
 * Update security settings (global tool policy, chat file uploads)
 * (authnz is handled by the authenticated session)
 */
const updateSecuritySettings = async (
  request: APIRequestContext,
  updates: {
    globalToolPolicy?: "permissive" | "restrictive";
    allowChatFileUploads?: boolean;
  },
) =>
  makeApiRequest({
    request,
    method: "patch",
    urlSuffix: "/api/organization/security-settings",
    data: updates,
  });

/**
 * Update knowledge settings (embedding model, API keys, reranker)
 * (authnz is handled by the authenticated session)
 */
const updateKnowledgeSettings = async (
  request: APIRequestContext,
  updates: {
    embeddingModel?: string | null;
    embeddingChatApiKeyId?: string | null;
    rerankerChatApiKeyId?: string | null;
    rerankerModel?: string | null;
  },
) =>
  makeApiRequest({
    request,
    method: "patch",
    urlSuffix: "/api/organization/knowledge-settings",
    data: updates,
  });

/**
 * Get interactions with optional filtering by profileId
 * (authnz is handled by the authenticated session)
 */
const getInteractions = async (
  request: APIRequestContext,
  options?: {
    profileId?: string;
    limit?: number;
    offset?: number;
    sortBy?: string;
    sortDirection?: "asc" | "desc";
  },
) => {
  const params = new URLSearchParams();
  if (options?.profileId) params.append("profileId", options.profileId);
  if (options?.limit) params.append("limit", String(options.limit));
  if (options?.offset) params.append("offset", String(options.offset));
  if (options?.sortBy) params.append("sortBy", options.sortBy);
  if (options?.sortDirection)
    params.append("sortDirection", options.sortDirection);
  const queryString = params.toString();
  return makeApiRequest({
    request,
    method: "get",
    urlSuffix: `/api/interactions${queryString ? `?${queryString}` : ""}`,
  });
};

/**
 * WireMock request journal entry structure
 */
export interface WiremockRequest {
  id: string;
  request: {
    url: string;
    absoluteUrl: string;
    method: string;
    headers: Record<string, string>;
    body: string;
    loggedDate: number;
    loggedDateString: string;
  };
  responseDefinition: {
    status: number;
  };
}

/**
 * Get requests from WireMock's request journal
 * Useful for verifying what was actually sent to mock LLM providers
 */
const getWiremockRequests = async (
  request: APIRequestContext,
  options?: {
    limit?: number;
    method?: string;
    urlPattern?: string;
  },
): Promise<WiremockRequest[]> => {
  const params = new URLSearchParams();
  if (options?.limit) params.append("limit", String(options.limit));

  const queryString = params.toString();
  const response = await request.get(
    `${WIREMOCK_BASE_URL}/__admin/requests${queryString ? `?${queryString}` : ""}`,
  );
  const data = await response.json();

  let requests: WiremockRequest[] = data.requests || [];

  // Filter by method if specified
  if (options?.method) {
    requests = requests.filter(
      (r) => r.request.method.toUpperCase() === options.method?.toUpperCase(),
    );
  }

  // Filter by URL pattern if specified
  if (options?.urlPattern) {
    const pattern = new RegExp(options.urlPattern);
    requests = requests.filter((r) => pattern.test(r.request.url));
  }

  return requests;
};

/**
 * Clear WireMock's request journal
 * Useful for test isolation - call in beforeEach to ensure clean state
 */
const clearWiremockRequests = async (request: APIRequestContext) => {
  let lastError: unknown;

  for (let attempt = 0; attempt < 8; attempt++) {
    try {
      const response = await request.delete(
        `${WIREMOCK_BASE_URL}/__admin/requests`,
        { timeout: 5000 },
      );

      if (response.ok()) return;

      lastError = new Error(`${response.status()} ${await response.text()}`);
    } catch (error) {
      lastError = error;
    }

    await new Promise((resolve) =>
      setTimeout(resolve, Math.min(500 * 2 ** attempt, 5000)),
    );
  }

  throw new Error(
    `Failed to clear WireMock requests at ${WIREMOCK_BASE_URL}: ${String(lastError)}`,
  );
};

/**
 * Create a knowledge base
 * (authnz is handled by the authenticated session)
 */
const createKnowledgeBase = async (request: APIRequestContext, name?: string) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/knowledge-bases",
    data: {
      name: name ?? `Test Knowledge Base ${crypto.randomUUID().slice(0, 8)}`,
    },
  });

/**
 * Delete a knowledge base by ID
 * (authnz is handled by the authenticated session)
 */
const deleteKnowledgeBase = async (request: APIRequestContext, id: string) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/knowledge-bases/${id}`,
    ignoreStatusCheck: true,
  });

/**
 * Create a connector for a knowledge base
 * (authnz is handled by the authenticated session)
 */
const createConnector = async (
  request: APIRequestContext,
  kgId: string,
  name?: string,
  overrides?: {
    connectorType?: string;
    config?: Record<string, unknown>;
    credentials?: { email: string; apiToken: string };
    schedule?: string;
    enabled?: boolean;
  },
) =>
  makeApiRequest({
    request,
    method: "post",
    urlSuffix: "/api/connectors",
    data: {
      name: name ?? `Test Connector ${crypto.randomUUID().slice(0, 8)}`,
      knowledgeBaseIds: [kgId],
      connectorType: overrides?.connectorType ?? "jira",
      config: overrides?.config ?? {
        type: "jira",
        jiraBaseUrl: "https://test.atlassian.net",
        isCloud: true,
        projectKey: "TEST",
      },
      credentials: overrides?.credentials ?? {
        email: "test@example.com",
        apiToken: "test-token-123",
      },
      schedule: overrides?.schedule ?? "0 */6 * * *",
      enabled: overrides?.enabled ?? true,
    },
  });

/**
 * Delete a connector by ID
 * (authnz is handled by the authenticated session)
 */
const deleteConnector = async (
  request: APIRequestContext,
  _kgId: string,
  connectorId: string,
) =>
  makeApiRequest({
    request,
    method: "delete",
    urlSuffix: `/api/connectors/${connectorId}`,
    ignoreStatusCheck: true,
  });

export * from "@playwright/test";
export const test = base.extend<TestFixtures>({
  makeApiRequest: async ({}, use) => {
    await use(makeApiRequest);
  },
  createAgent: async ({}, use) => {
    await use(createAgent);
  },
  createLlmProxy: async ({}, use) => {
    await use(createLlmProxy);
  },
  createMcpGateway: async ({}, use) => {
    await use(createMcpGateway);
  },
  deleteAgent: async ({}, use) => {
    await use(deleteAgent);
  },
  createApiKey: async ({}, use) => {
    await use(createApiKey);
  },
  deleteApiKey: async ({}, use) => {
    await use(deleteApiKey);
  },
  createIdentityProvider: async ({}, use) => {
    await use(createIdentityProvider);
  },
  deleteIdentityProvider: async ({}, use) => {
    await use(deleteIdentityProvider);
  },
  createToolInvocationPolicy: async ({}, use) => {
    await use(createToolInvocationPolicy);
  },
  deleteToolInvocationPolicy: async ({}, use) => {
    await use(deleteToolInvocationPolicy);
  },
  createTrustedDataPolicy: async ({}, use) => {
    await use(createTrustedDataPolicy);
  },
  deleteTrustedDataPolicy: async ({}, use) => {
    await use(deleteTrustedDataPolicy);
  },
  createMcpCatalogItem: async ({}, use) => {
    await use(createMcpCatalogItem);
  },
  deleteMcpCatalogItem: async ({}, use) => {
    await use(deleteMcpCatalogItem);
  },
  installMcpServer: async ({}, use) => {
    await use(installMcpServer);
  },
  uninstallMcpServer: async ({}, use) => {
    await use(uninstallMcpServer);
  },
  createRole: async ({}, use) => {
    await use(createRole);
  },
  deleteRole: async ({}, use) => {
    await use(deleteRole);
  },
  createTeam: async ({}, use) => {
    await use(createTeam);
  },
  deleteTeam: async ({}, use) => {
    await use(deleteTeam);
  },
  waitForAgentTool: async ({}, use) => {
    await use(waitForAgentTool);
  },
  waitForProxyTool: async ({}, use) => {
    await use(waitForProxyTool);
  },
  getTeamByName: async ({}, use) => {
    await use(getTeamByName);
  },
  addTeamMember: async ({}, use) => {
    await use(addTeamMember);
  },
  removeTeamMember: async ({}, use) => {
    await use(removeTeamMember);
  },
  getActiveOrganizationId: async ({}, use) => {
    await use(getActiveOrganizationId);
  },
  createOptimizationRule: async ({}, use) => {
    await use(createOptimizationRule);
  },
  deleteOptimizationRule: async ({}, use) => {
    await use(deleteOptimizationRule);
  },
  updateOptimizationRule: async ({}, use) => {
    await use(updateOptimizationRule);
  },
  createLimit: async ({}, use) => {
    await use(createLimit);
  },
  deleteLimit: async ({}, use) => {
    await use(deleteLimit);
  },
  getLimits: async ({}, use) => {
    await use(getLimits);
  },
  getModels: async ({}, use) => {
    await use(getModels);
  },
  syncModels: async ({}, use) => {
    await use(syncModels);
  },
  updateModelPricing: async ({}, use) => {
    await use(updateModelPricing);
  },
  getOrganization: async ({}, use) => {
    await use(getOrganization);
  },
  updateLlmSettings: async ({}, use) => {
    await use(updateLlmSettings);
  },
  updateSecuritySettings: async ({}, use) => {
    await use(updateSecuritySettings);
  },
  updateKnowledgeSettings: async ({}, use) => {
    await use(updateKnowledgeSettings);
  },
  getInteractions: async ({}, use) => {
    await use(getInteractions);
  },
  getWiremockRequests: async ({}, use) => {
    await use(getWiremockRequests);
  },
  clearWiremockRequests: async ({}, use) => {
    await use(clearWiremockRequests);
  },
  createKnowledgeBase: async ({}, use) => {
    await use(createKnowledgeBase);
  },
  deleteKnowledgeBase: async ({}, use) => {
    await use(deleteKnowledgeBase);
  },
  createConnector: async ({}, use) => {
    await use(createConnector);
  },
  deleteConnector: async ({}, use) => {
    await use(deleteConnector);
  },
  /**
   * Admin request - creates a new request context with admin auth
   */
  adminRequest: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      storageState: adminAuthFile,
    });
    await use(context);
    await context.dispose();
  },
  /**
   * Editor request - creates a new request context with editor auth
   */
  editorRequest: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      storageState: editorAuthFile,
    });
    await use(context);
    await context.dispose();
  },
  /**
   * Member request - creates a new request context with member auth
   */
  memberRequest: async ({ playwright }, use) => {
    const context = await playwright.request.newContext({
      storageState: memberAuthFile,
    });
    await use(context);
    await context.dispose();
  },
});
