import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
import { AccessMode } from '@/models/access-control'
import * as exploreService from '@/service/explore'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
import { AppACLPermission } from '@/utils/permission'
import { AppCard } from '../app-card'
import { StarredAppCard } from '../starred-app-card'

let mockWebappAuthEnabled = false
let mockRbacEnabled = true
const mockUserCanAccessApp = vi.hoisted(() => ({
  result: true as boolean | undefined,
  isLoading: false,
}))
const mockAppDslExport = vi.hoisted(() => ({
  exportAppDsl: vi.fn(),
  isExporting: false,
}))
const mockWorkflowAppDslExport = vi.hoisted(() => ({
  exportWorkflowAppDsl: vi.fn(),
  isExporting: false,
}))
const mockCopyApp = vi.hoisted(() =>
  vi.fn((_variables: unknown): Promise<unknown> =>
    Promise.resolve({
      id: 'new-app-id',
      mode: 'chat',
      maintainer: 'user-1',
      permission_keys: [],
    }),
  ),
)
const mockUpdateAppMutation = vi.hoisted(() =>
  vi.fn((_variables: unknown): Promise<unknown> => Promise.resolve()),
)
const mockDeleteAppMutation = vi.hoisted(() =>
  vi.fn((_variables: unknown): Promise<unknown> => Promise.resolve()),
)
const mockStarAppMutation = vi.hoisted(() =>
  vi.fn((_variables: unknown): Promise<unknown> => Promise.resolve()),
)
const mockUnstarAppMutation = vi.hoisted(() =>
  vi.fn((_variables: unknown): Promise<unknown> => Promise.resolve()),
)

vi.mock('@/service/client', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@/service/client')>()
  const withMutation = (operation: object, mutationFn: typeof mockCopyApp) =>
    new Proxy(operation, {
      get(target, property, receiver) {
        if (property === 'mutationOptions')
          return () => ({ mutationFn: (variables: unknown) => mutationFn(variables) })
        return Reflect.get(target, property, receiver)
      },
    })
  const copy = new Proxy(actual.consoleQuery.apps.byAppId.copy, {
    get(target, property, receiver) {
      if (property === 'post') return withMutation(target.post, mockCopyApp)
      return Reflect.get(target, property, receiver)
    },
  })
  const star = new Proxy(actual.consoleQuery.apps.byAppId.star, {
    get(target, property, receiver) {
      if (property === 'post') return withMutation(target.post, mockStarAppMutation)
      if (property === 'delete') return withMutation(target.delete, mockUnstarAppMutation)
      return Reflect.get(target, property, receiver)
    },
  })
  const byAppId = new Proxy(actual.consoleQuery.apps.byAppId, {
    get(target, property, receiver) {
      if (property === 'copy') return copy
      if (property === 'put') return withMutation(target.put, mockUpdateAppMutation)
      if (property === 'delete') return withMutation(target.delete, mockDeleteAppMutation)
      if (property === 'star') return star
      return Reflect.get(target, property, receiver)
    },
  })
  const apps = new Proxy(actual.consoleQuery.apps, {
    get(target, property, receiver) {
      if (property === 'byAppId') return byAppId
      return Reflect.get(target, property, receiver)
    },
  })

  return {
    ...actual,
    consoleQuery: new Proxy(actual.consoleQuery, {
      get(target, property, receiver) {
        if (property === 'apps') return apps
        return Reflect.get(target, property, receiver)
      },
    }),
  }
})

vi.mock('@/app/components/app/use-export-app-dsl', () => ({
  useExportAppDsl: () => mockAppDslExport,
  useExportWorkflowAppDsl: () => mockWorkflowAppDslExport,
}))

const getOperationsTrigger = () =>
  screen.getByRole('button', { name: /common\.operation\.moreActionsFor/ })

// Mock next/navigation
const mockPush = vi.fn()
vi.mock('@/next/navigation', () => ({
  useRouter: () => ({
    push: mockPush,
  }),
}))

const toastMocks = vi.hoisted(() => {
  const record = vi.fn()
  const api = vi.fn((message: unknown, options?: Record<string, unknown>) =>
    record({ message, ...options }),
  )
  return {
    record,
    api: Object.assign(api, {
      success: vi.fn((message: unknown, options?: Record<string, unknown>) =>
        record({ type: 'success', message, ...options }),
      ),
      error: vi.fn((message: unknown, options?: Record<string, unknown>) =>
        record({ type: 'error', message, ...options }),
      ),
      warning: vi.fn((message: unknown, options?: Record<string, unknown>) =>
        record({ type: 'warning', message, ...options }),
      ),
      info: vi.fn((message: unknown, options?: Record<string, unknown>) =>
        record({ type: 'info', message, ...options }),
      ),
      dismiss: vi.fn(),
      update: vi.fn(),
      promise: vi.fn(),
    }),
  }
})

vi.mock('@langgenius/dify-ui/toast', () => ({
  toast: toastMocks.api,
}))

// Mock use-context-selector with stable toast reference for tracking calls
// Include createContext for components that use it (like Toast)
vi.mock('use-context-selector', () => ({
  createContext: <T,>(defaultValue: T) => React.createContext(defaultValue),
  useContext: () => ({
    notify: toastMocks.api,
  }),
  useContextSelector: (_context: unknown, selector: (state: Record<string, unknown>) => unknown) =>
    selector({
      notify: toastMocks.api,
    }),
}))

const mockConsoleState = vi.hoisted(() => ({
  isCurrentWorkspaceEditor: true,
  userProfile: { id: 'user-1' },
  workspacePermissionKeys: ['app.create_and_management'] as string[],
}))

const render = (ui: React.ReactElement) =>
  renderWithConsoleQuery(ui, {
    accountProfile: mockConsoleState.userProfile,
    systemFeatures: {
      webapp_auth: { enabled: mockWebappAuthEnabled },
      branding: { enabled: false },
      rbac_enabled: mockRbacEnabled,
    },
  })

// Mock app context

vi.mock('@/context/workspace-state', async () => {
  const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
  return createWorkspaceStateModuleMock(() => mockConsoleState)
})
vi.mock('@/context/permission-state', async () => {
  const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
  return createPermissionStateModuleMock(() => mockConsoleState)
})

// Mock provider context
const mockOnPlanInfoChanged = vi.fn()
vi.mock('@/context/provider-context', () => ({
  useProviderContext: () => ({
    onPlanInfoChanged: mockOnPlanInfoChanged,
  }),
}))

// systemFeatures is seeded into the QueryClient via the local render helper.

vi.mock('@/service/apps', () => ({
  deleteApp: vi.fn(() => Promise.resolve()),
}))

vi.mock('@/service/explore', () => ({
  fetchInstalledAppList: vi.fn(() => Promise.resolve({ installed_apps: [{ id: 'installed-1' }] })),
}))

vi.mock('@/service/access-control', () => ({
  useGetUserCanAccessApp: () => ({
    data:
      mockUserCanAccessApp.result === undefined
        ? undefined
        : { result: mockUserCanAccessApp.result },
    isLoading: mockUserCanAccessApp.isLoading,
  }),
}))

vi.mock('@/service/access-control/use-app-access-control', () => ({
  useGetUserCanAccessApp: () => ({
    data:
      mockUserCanAccessApp.result === undefined
        ? undefined
        : { result: mockUserCanAccessApp.result },
    isLoading: mockUserCanAccessApp.isLoading,
  }),
}))

// Mock hooks
const mockOpenAsyncWindow = vi.fn()
vi.mock('@/hooks/use-async-window-open', () => ({
  useAsyncWindowOpen: () => mockOpenAsyncWindow,
}))

// Mock utils
const { mockGetRedirection } = vi.hoisted(() => ({
  mockGetRedirection: vi.fn(),
}))

vi.mock('@/utils/app-redirection', () => ({
  getRedirectionPath: (app: { id: string }) => `/app/${app.id}/configuration`,
  getRedirection: mockGetRedirection,
}))

vi.mock('@/utils/var', () => ({
  basePath: '',
}))

vi.mock('@/utils/time', () => ({
  formatTime: () => 'Jan 1, 2024',
}))

// Mock dynamic imports
vi.mock('@/next/dynamic', () => ({
  default: (importFn: () => Promise<unknown>) => {
    void importFn().catch(() => {})
    const fnString = importFn.toString()

    if (fnString.includes('create-app-modal') || fnString.includes('explore/create-app-modal')) {
      return function MockEditAppModal({
        show,
        onHide,
        onConfirm,
      }: {
        show: boolean
        onHide: () => void
        onConfirm?: (data: Record<string, unknown>) => void
      }) {
        if (!show) return null
        return React.createElement(
          'div',
          { 'data-testid': 'edit-app-modal' },
          React.createElement(
            'button',
            { onClick: onHide, 'data-testid': 'close-edit-modal' },
            'Close',
          ),
          React.createElement(
            'button',
            {
              onClick: () =>
                onConfirm?.({
                  name: 'Updated App',
                  icon_type: 'emoji',
                  icon: '🎯',
                  icon_background: '#FFEAD5',
                  description: 'Updated description',
                  use_icon_as_answer_icon: false,
                  max_active_requests: null,
                }),
              'data-testid': 'confirm-edit-modal',
            },
            'Confirm',
          ),
        )
      }
    }
    if (fnString.includes('duplicate-modal')) {
      return function MockDuplicateAppModal({
        show,
        onHide,
        onConfirm,
      }: {
        show: boolean
        onHide: () => void
        onConfirm?: (data: Record<string, unknown>) => void
      }) {
        if (!show) return null
        return React.createElement(
          'div',
          { 'data-testid': 'duplicate-modal' },
          React.createElement(
            'button',
            { onClick: onHide, 'data-testid': 'close-duplicate-modal' },
            'Close',
          ),
          React.createElement(
            'button',
            {
              onClick: () =>
                onConfirm?.({
                  name: 'Copied App',
                  icon_type: 'emoji',
                  icon: '📋',
                  icon_background: '#E4FBCC',
                }),
              'data-testid': 'confirm-duplicate-modal',
            },
            'Confirm',
          ),
        )
      }
    }
    if (fnString.includes('switch-app-modal')) {
      return function MockSwitchAppModal({
        show,
        onClose,
      }: {
        show: boolean
        onClose: () => void
      }) {
        if (!show) return null
        return React.createElement(
          'div',
          { 'data-testid': 'switch-modal' },
          React.createElement(
            'button',
            { onClick: onClose, 'data-testid': 'close-switch-modal' },
            'Close',
          ),
        )
      }
    }
    if (fnString.includes('dsl-export-confirm-modal')) {
      return function MockDSLExportModal({
        onClose,
        onConfirm,
      }: {
        onClose?: () => void
        onConfirm?: (withSecrets: boolean) => void
      }) {
        return React.createElement(
          'div',
          { 'data-testid': 'dsl-export-modal' },
          React.createElement(
            'button',
            { onClick: () => onClose?.(), 'data-testid': 'close-dsl-export' },
            'Close',
          ),
          React.createElement(
            'button',
            { onClick: () => onConfirm?.(true), 'data-testid': 'confirm-dsl-export' },
            'Export with secrets',
          ),
          React.createElement(
            'button',
            { onClick: () => onConfirm?.(false), 'data-testid': 'confirm-dsl-export-no-secrets' },
            'Export without secrets',
          ),
        )
      }
    }
    return () => null
  },
}))

// AppCardTags has tag API dependencies - mock for isolated testing
vi.mock('@/features/tag-management/components/app-card-tags', () => ({
  AppCardTags: ({
    tags,
    canBindOrUnbindTags,
    appName,
  }: {
    tags?: { id: string; name: string }[]
    canBindOrUnbindTags?: boolean
    appName: string
  }) => {
    return React.createElement(
      'div',
      {
        'aria-label': 'tag-selector',
        'data-can-bind-or-unbind-tags': String(Boolean(canBindOrUnbindTags)),
        'data-app-name': appName,
      },
      tags?.map((tag: { id: string; name: string }) =>
        React.createElement('span', { key: tag.id }, tag.name),
      ),
    )
  },
}))

// AppTypeIcon has complex icon mapping - mock for focused component testing
vi.mock('@/app/components/app/type-selector', () => ({
  AppTypeIcon: () => React.createElement('div', { 'data-testid': 'app-type-icon' }),
}))

const createMockApp = (overrides: Partial<AppPartial> = {}): AppPartial => ({
  id: 'test-app-id',
  name: 'Test App',
  description: 'Test app description',
  mode: AppModeEnum.CHAT,
  icon: '🤖',
  icon_type: 'emoji' as const,
  icon_background: '#FFEAD5',
  icon_url: null,
  author_name: 'Test Author',
  created_by: 'user-1',
  maintainer: 'user-1',
  created_at: 1704067200,
  updated_at: 1704153600,
  tags: [],
  use_icon_as_answer_icon: false,
  max_active_requests: null,
  access_mode: AccessMode.PUBLIC,
  has_draft_trigger: false,
  permission_keys: [],
  ...overrides,
})

describe('AppCard', () => {
  const mockApp = createMockApp()

  beforeEach(() => {
    vi.clearAllMocks()
    mockOpenAsyncWindow.mockReset()
    mockWebappAuthEnabled = false
    mockRbacEnabled = true
    mockUserCanAccessApp.result = true
    mockUserCanAccessApp.isLoading = false
    mockCopyApp.mockResolvedValue({
      id: 'new-app-id',
      mode: 'chat',
      maintainer: 'user-1',
      permission_keys: [],
    })
    mockAppDslExport.isExporting = false
    mockAppDslExport.exportAppDsl.mockResolvedValue({ status: 'downloaded' })
    mockWorkflowAppDslExport.isExporting = false
    mockWorkflowAppDslExport.exportWorkflowAppDsl.mockResolvedValue({ status: 'downloaded' })
    mockConsoleState.isCurrentWorkspaceEditor = true
    mockConsoleState.userProfile = { id: 'user-1' }
    mockConsoleState.workspacePermissionKeys = ['app.create_and_management']
  })

  describe('Rendering', () => {
    it('should render preview-only app card as a dimmed information-only card', () => {
      const previewOnlyApp = createMockApp({
        name: 'Preview Only App',
        description: 'Only visible metadata',
        author_name: 'Readonly Author',
        created_by: 'another-user',
        maintainer: 'another-user',
        tags: [{ id: 'tag-preview', name: 'Readonly Tag', type: 'app' as const }],
        permission_keys: [AppACLPermission.Preview],
      })

      render(<AppCard app={previewOnlyApp} />)

      const card = screen.getByRole('button', { name: 'Preview Only App' })
      expect(card).toHaveClass('opacity-60')
      expect(screen.getByRole('listitem')).toContainElement(card)
      expect(card).toHaveAccessibleDescription('Only visible metadata')
      expect(card).not.toHaveAttribute('aria-disabled')
      expect(screen.getByText('Only visible metadata')).toBeInTheDocument()
      expect(screen.getByText('Readonly Author')).toBeInTheDocument()
      const tagSelector = screen.getByLabelText('tag-selector')
      expect(tagSelector).toBeInTheDocument()
      expect(tagSelector).toHaveAttribute('data-can-bind-or-unbind-tags', 'false')
      expect(screen.queryByRole('link', { name: 'Preview Only App' })).not.toBeInTheDocument()
      expect(screen.queryByRole('button', { name: 'app.studio.starApp' })).not.toBeInTheDocument()
      expect(
        screen.queryByRole('button', {
          name: /common\.operation\.moreActionsFor/,
        }),
      ).not.toBeInTheDocument()

      fireEvent.click(tagSelector)

      expect(toastMocks.record).not.toHaveBeenCalled()

      fireEvent.click(card)

      expect(toastMocks.record).toHaveBeenCalledWith({
        type: 'warning',
        message: 'app.noAccessResourcePermission',
      })
    })

    it('should render preview-only starred app card as a dimmed information-only card', () => {
      const previewOnlyApp = createMockApp({
        name: 'Preview Only Starred App',
        author_name: 'Readonly Author',
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.Preview],
      })

      render(<StarredAppCard app={previewOnlyApp} />)

      const card = screen.getByRole('button', { name: 'Preview Only Starred App' })
      expect(card).toHaveClass('opacity-60')
      expect(card).not.toHaveAttribute('aria-disabled')
      expect(screen.getByText('Readonly Author')).toBeInTheDocument()
      expect(
        screen.queryByRole('link', { name: 'Preview Only Starred App' }),
      ).not.toBeInTheDocument()
      expect(screen.queryByRole('button', { name: 'app.studio.starApp' })).not.toBeInTheDocument()
      expect(
        screen.queryByRole('button', {
          name: /common\.operation\.moreActionsFor/,
        }),
      ).not.toBeInTheDocument()

      fireEvent.click(card)

      expect(toastMocks.record).toHaveBeenCalledWith({
        type: 'warning',
        message: 'app.noAccessResourcePermission',
      })
    })

    it('should display app name', () => {
      render(<AppCard app={mockApp} />)
      expect(screen.getByText('Test App')).toBeInTheDocument()
    })

    it('should display app description', () => {
      render(<AppCard app={mockApp} />)
      expect(screen.getByText('Test app description')).toBeInTheDocument()
    })

    it('should display author name', () => {
      render(<AppCard app={mockApp} />)
      expect(screen.getByText('Test Author')).toBeInTheDocument()
    })

    it('should render app icon', () => {
      // AppIcon component renders the emoji icon from app data
      const { container } = render(<AppCard app={mockApp} />)
      const emojiIcon = container.querySelector(`em-emoji[id="${mockApp.icon}"]`)
      const imageIcon = container.querySelector('img')
      expect(emojiIcon || imageIcon).toBeTruthy()
      expect(emojiIcon?.parentElement).toHaveAttribute('aria-hidden', 'true')
    })

    it('should treat a redundant image icon as decorative', () => {
      const imageApp = createMockApp({
        icon_type: 'image',
        icon_url: 'https://example.com/app-icon.png',
      })
      const { container } = render(<AppCard app={imageApp} />)

      expect(container.querySelector('img')).toHaveAttribute('alt', '')
    })

    it('should render app type icon', () => {
      render(<AppCard app={mockApp} />)
      expect(screen.getByTestId('app-type-icon')).toBeInTheDocument()
    })

    it('should display formatted edit time', () => {
      render(<AppCard app={mockApp} />)
      expect(screen.getByText(/edited/i)).toBeInTheDocument()
    })
  })

  describe('Props', () => {
    it('should handle different app modes', () => {
      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)
      expect(screen.getByRole('link', { name: 'Test App' })).toBeInTheDocument()
    })

    it('should handle app with tags', () => {
      const appWithTags = {
        ...mockApp,
        tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const, binding_count: '' }],
      }
      render(<AppCard app={appWithTags} />)
      // Verify the tag selector component renders
      expect(screen.getByLabelText('tag-selector')).toHaveAttribute('data-app-name', 'Test App')
    })

    it('should display refreshed tag names from app props when tag ids stay the same', () => {
      const firstApp = createMockApp({
        tags: [{ id: 'tag1', name: 'Old Tag', type: 'app' as const }],
      })
      const refreshedApp = createMockApp({
        tags: [{ id: 'tag1', name: 'New Tag', type: 'app' as const }],
      })

      const { rerender } = render(<AppCard app={firstApp} />)
      expect(screen.getByText('Old Tag')).toBeInTheDocument()

      rerender(<AppCard app={refreshedApp} />)

      expect(screen.getByText('New Tag')).toBeInTheDocument()
      expect(screen.queryByText('Old Tag')).not.toBeInTheDocument()
    })

    it('should allow app edit permission to bind tags without workspace tag management permission', () => {
      mockConsoleState.isCurrentWorkspaceEditor = false
      mockConsoleState.workspacePermissionKeys = []
      mockConsoleState.userProfile = { id: 'user-2' }
      const editableApp = createMockApp({
        maintainer: 'user-1',
        tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const }],
        permission_keys: [AppACLPermission.Edit],
      })

      render(<AppCard app={editableApp} />)

      expect(screen.getByLabelText('tag-selector')).toHaveAttribute(
        'data-can-bind-or-unbind-tags',
        'true',
      )
    })

    it('should allow workspace app tag management permission to bind tags without app edit permission', () => {
      mockConsoleState.isCurrentWorkspaceEditor = false
      mockConsoleState.workspacePermissionKeys = ['app.tag.manage']
      mockConsoleState.userProfile = { id: 'user-2' }
      const tagManageApp = createMockApp({
        maintainer: 'user-1',
        tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const }],
        permission_keys: [AppACLPermission.ViewLayout],
      })

      render(<AppCard app={tagManageApp} />)

      expect(screen.getByLabelText('tag-selector')).toHaveAttribute(
        'data-can-bind-or-unbind-tags',
        'true',
      )
    })

    it('should render existing app tags as readonly without app edit or workspace tag management permission', () => {
      mockConsoleState.isCurrentWorkspaceEditor = false
      mockConsoleState.workspacePermissionKeys = []
      mockConsoleState.userProfile = { id: 'user-2' }
      const readonlyApp = createMockApp({
        maintainer: 'user-1',
        tags: [{ id: 'tag1', name: 'Tag 1', type: 'app' as const }],
        permission_keys: [AppACLPermission.ViewLayout],
      })

      render(<AppCard app={readonlyApp} />)

      expect(screen.getByLabelText('tag-selector')).toHaveAttribute(
        'data-can-bind-or-unbind-tags',
        'false',
      )
    })
  })

  describe('Web app access control entry points', () => {
    it('should not render the access mode icon or tooltip trigger', () => {
      render(<AppCard app={mockApp} />)

      expect(
        screen.queryByRole('img', { name: 'app.accessItemsDescription.anyone' }),
      ).not.toBeInTheDocument()
    })
  })

  describe('Card Interaction', () => {
    it('should render card navigation as a link', () => {
      render(<AppCard app={mockApp} />)
      const cardLink = screen.getByRole('link', { name: 'Test App' })

      expect(cardLink).toHaveAttribute('href', '/app/test-app-id/configuration')
      expect(cardLink).toHaveAccessibleName('Test App')
      expect(cardLink).toHaveAccessibleDescription('Test app description')
      expect(cardLink).toHaveAttribute('aria-describedby')
      expect(screen.getByRole('listitem')).toContainElement(cardLink)
    })

    it('should keep card navigation and actions as sibling focus targets', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      const cardLink = screen.getByRole('link', { name: 'Test App' })
      const starToggle = screen.getByRole('button', { name: 'app.studio.starApp: Test App' })
      const operationsTrigger = getOperationsTrigger()

      expect(cardLink).not.toContainElement(starToggle)
      expect(cardLink).not.toContainElement(operationsTrigger)

      await user.tab()
      expect(cardLink).toHaveFocus()
      await user.tab()
      expect(starToggle).toHaveFocus()
      await user.tab()
      expect(operationsTrigger).toHaveFocus()
    })

    it('should star the app from the card action without navigating', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      const starToggle = screen.getByRole('button', { name: 'app.studio.starApp: Test App' })
      expect(starToggle).toHaveAttribute('aria-pressed', 'false')

      await user.click(starToggle)

      await waitFor(() => {
        expect(mockStarAppMutation).toHaveBeenCalledWith({
          params: { app_id: mockApp.id },
        })
      })
      expect(mockPush).not.toHaveBeenCalled()
    })

    it('should unstar the app from the filled star action', async () => {
      const user = userEvent.setup()
      const starredApp = createMockApp({ is_starred: true })
      render(<AppCard app={starredApp} />)

      const starToggle = screen.getByRole('button', { name: 'app.studio.starApp: Test App' })
      expect(starToggle).toHaveAttribute('aria-pressed', 'true')

      await user.hover(starToggle)

      expect(await screen.findByText('app.studio.starApp')).toBeInTheDocument()

      await user.click(starToggle)

      await waitFor(() => {
        expect(mockUnstarAppMutation).toHaveBeenCalledWith({
          params: { app_id: starredApp.id },
        })
      })
    })
  })

  describe('Operations Menu', () => {
    it('should show edit option when dropdown menu is opened', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      await user.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.editApp')).toBeInTheDocument()
      })
      expect(mockPush).not.toHaveBeenCalled()
    })

    it('should expose the same operations from the card context menu', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      await user.pointer({
        target: screen.getByRole('link', { name: 'Test App' }),
        keys: '[MouseRight]',
      })

      expect(await screen.findByRole('menuitem', { name: 'app.editApp' })).toBeInTheDocument()
      expect(screen.getByRole('menuitem', { name: 'app.duplicate' })).toBeInTheDocument()
      expect(screen.getByRole('menuitem', { name: 'app.export' })).toBeInTheDocument()
    })

    it('should keep card actions outside the card context menu trigger', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      await user.pointer({
        target: screen.getByRole('button', { name: 'app.studio.starApp: Test App' }),
        keys: '[MouseRight]',
      })

      expect(screen.queryByRole('menuitem', { name: 'app.editApp' })).not.toBeInTheDocument()
    })

    it('should show duplicate option when dropdown menu is opened', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.duplicate')).toBeInTheDocument()
      })
    })

    it('should show duplicate option when user can create apps without app import export permission', async () => {
      const appWithoutImportExportPermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.ViewLayout],
      })
      render(<AppCard app={appWithoutImportExportPermission} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.duplicate')).toBeInTheDocument()
      })
      expect(screen.queryByText('app.export')).not.toBeInTheDocument()
    })

    it('should show duplicate option on starred cards when user can create apps without app import export permission', async () => {
      const appWithoutImportExportPermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.ViewLayout],
      })
      render(<StarredAppCard app={appWithoutImportExportPermission} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.duplicate')).toBeInTheDocument()
      })
      expect(screen.queryByText('app.export')).not.toBeInTheDocument()
    })

    it('should show export option when dropdown menu is opened', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.export')).toBeInTheDocument()
      })
    })

    it('should show delete option when dropdown menu is opened', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
      })
    })

    it('should show switch option for chat mode apps', async () => {
      const chatApp = { ...mockApp, mode: AppModeEnum.CHAT }
      render(<AppCard app={chatApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText(/switch/i)).toBeInTheDocument()
      })
    })

    it('should hide duplicate but keep app-authorized switch without app creation permission', async () => {
      mockConsoleState.workspacePermissionKeys = []
      const editableChatApp = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        mode: AppModeEnum.CHAT,
        permission_keys: [AppACLPermission.Edit],
      })
      render(<AppCard app={editableChatApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText(/switch/i)).toBeInTheDocument()
      })
      expect(screen.queryByText('app.duplicate')).not.toBeInTheDocument()
    })

    it('should show switch option for completion mode apps', async () => {
      const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION }
      render(<AppCard app={completionApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText(/switch/i)).toBeInTheDocument()
      })
    })

    it('should not show switch option for workflow mode apps', async () => {
      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.queryByText(/switch/i)).not.toBeInTheDocument()
      })
    })
  })

  describe('Modal Interactions', () => {
    it('should open edit modal when edit button is clicked', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        const editButton = screen.getByText('app.editApp')
        fireEvent.click(editButton)
      })

      await waitFor(() => {
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })
    })

    it('should open duplicate modal when duplicate button is clicked', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        const duplicateButton = screen.getByText('app.duplicate')
        fireEvent.click(duplicateButton)
      })

      await waitFor(() => {
        expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
      })
    })

    it('should open confirm dialog when delete button is clicked', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))
      expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
    })

    it('should autofill the app name for delete confirmation', async () => {
      const user = userEvent.setup()
      render(<AppCard app={mockApp} />)

      await user.click(getOperationsTrigger())
      await user.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))

      const deleteInput = await screen.findByRole('textbox')
      const confirmButton = screen.getByRole('button', { name: 'common.operation.confirm' })

      expect(confirmButton).toBeDisabled()

      await user.click(screen.getByRole('button', { name: 'common.operation.fill' }))

      expect(deleteInput).toHaveValue(mockApp.name)
      expect(confirmButton).toBeEnabled()
    })

    it('should close confirm dialog when cancel is clicked', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))
      expect(await screen.findByRole('alertdialog')).toBeInTheDocument()
      fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
      await waitFor(() => {
        expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
      })
    })

    it('should not submit delete when confirmation text does not match', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))

      const form = (await screen.findByRole('alertdialog')).querySelector('form')
      expect(form).toBeTruthy()
      fireEvent.submit(form!)

      expect(mockDeleteAppMutation).not.toHaveBeenCalled()
    })

    it('should close edit modal when onHide is called', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.editApp'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })

      // Click close button to trigger onHide
      fireEvent.click(screen.getByTestId('close-edit-modal'))

      await waitFor(() => {
        expect(screen.queryByTestId('edit-app-modal')).not.toBeInTheDocument()
      })
    })

    it('should close duplicate modal when onHide is called', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.duplicate'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
      })

      // Click close button to trigger onHide
      fireEvent.click(screen.getByTestId('close-duplicate-modal'))

      await waitFor(() => {
        expect(screen.queryByTestId('duplicate-modal')).not.toBeInTheDocument()
      })
    })

    it('should clear delete confirmation input after closing the dialog', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))

      const deleteInput = await screen.findByRole('textbox')
      fireEvent.change(deleteInput, { target: { value: 'partial name' } })
      fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))

      await waitFor(() => {
        expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
      })

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))

      await waitFor(() => {
        expect(screen.getByRole('textbox')).toHaveValue('')
      })
    })
  })

  describe('API Callbacks', () => {
    it('should call deleteApp API when confirming delete', async () => {
      render(<AppCard app={mockApp} />)

      // Open dropdown menu and click delete
      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))
      expect(await screen.findByRole('alertdialog')).toBeInTheDocument()

      // Fill in the confirmation input with app name
      const deleteInput = screen.getByRole('textbox')
      fireEvent.change(deleteInput, { target: { value: mockApp.name } })

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))

      await waitFor(() => {
        expect(mockDeleteAppMutation).toHaveBeenCalled()
      })
    })

    it('should handle delete failure', async () => {
      mockDeleteAppMutation.mockRejectedValueOnce(new Error('Delete failed'))

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))
      expect(await screen.findByRole('alertdialog')).toBeInTheDocument()

      // Fill in the confirmation input with app name
      const deleteInput = screen.getByRole('textbox')
      fireEvent.change(deleteInput, { target: { value: mockApp.name } })

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))

      await waitFor(() => {
        expect(mockDeleteAppMutation).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({
          type: 'error',
          message: expect.stringContaining('Delete failed'),
        })
      })
    })

    it('should handle delete failure without an error message', async () => {
      mockDeleteAppMutation.mockRejectedValueOnce({})

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'common.operation.delete' }))
      expect(await screen.findByRole('alertdialog')).toBeInTheDocument()

      fireEvent.change(screen.getByRole('textbox'), { target: { value: mockApp.name } })
      fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))

      await waitFor(() => {
        expect(mockDeleteAppMutation).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({
          type: 'error',
          message: 'app.appDeleteFailed',
        })
      })
    })

    it('should update the app and close the edit modal', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.editApp'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-edit-modal'))

      await waitFor(() => {
        expect(mockUpdateAppMutation).toHaveBeenCalledWith({
          params: { app_id: mockApp.id },
          body: {
            name: 'Updated App',
            icon_type: 'emoji',
            icon: '🎯',
            icon_background: '#FFEAD5',
            description: 'Updated description',
            use_icon_as_answer_icon: false,
            max_active_requests: null,
          },
        })
        expect(screen.queryByTestId('edit-app-modal')).not.toBeInTheDocument()
      })
    })

    it('should call copyApp API when duplicating app', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.duplicate'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-duplicate-modal'))

      await waitFor(() => {
        expect(mockCopyApp).toHaveBeenCalled()
      })
    })

    it('should call onPlanInfoChanged after successful duplication', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.duplicate'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-duplicate-modal'))

      await waitFor(() => {
        expect(mockOnPlanInfoChanged).toHaveBeenCalled()
      })
    })

    it('should handle copy failure', async () => {
      mockCopyApp.mockRejectedValueOnce(new Error('Copy failed'))

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.duplicate'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('duplicate-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-duplicate-modal'))

      await waitFor(() => {
        expect(mockCopyApp).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({
          type: 'error',
          message: 'app.newApp.appCreateFailed',
        })
      })
    })

    it('should export the app DSL when exporting', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      expect(mockAppDslExport.exportAppDsl).toHaveBeenCalledWith({
        appId: mockApp.id,
        appName: mockApp.name,
      })
    })

    it('should prevent duplicate exports while an app DSL export is pending', () => {
      mockAppDslExport.isExporting = true
      render(<AppCard app={mockApp} />)

      const trigger = screen.getByRole('button', { name: 'common.operation.exporting' })

      expect(trigger).toBeDisabled()
    })
  })

  describe('Switch Modal', () => {
    it('should open switch modal when switch button is clicked', async () => {
      const chatApp = { ...mockApp, mode: AppModeEnum.CHAT }
      render(<AppCard app={chatApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.switch'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('switch-modal')).toBeInTheDocument()
      })
    })

    it('should close switch modal when close button is clicked', async () => {
      const chatApp = { ...mockApp, mode: AppModeEnum.CHAT }
      render(<AppCard app={chatApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.switch'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('switch-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('close-switch-modal'))

      await waitFor(() => {
        expect(screen.queryByTestId('switch-modal')).not.toBeInTheDocument()
      })
    })

    it('should open switch modal for completion mode apps', async () => {
      const completionApp = { ...mockApp, mode: AppModeEnum.COMPLETION }
      render(<AppCard app={completionApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.switch'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('switch-modal')).toBeInTheDocument()
      })
    })
  })

  describe('Open in Explore', () => {
    it('should show open in explore option when dropdown menu is opened', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
      })
    })
  })

  describe('Workflow Export with Environment Variables', () => {
    it('should use the workflow export command for workflow apps', async () => {
      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      await waitFor(() => {
        expect(mockWorkflowAppDslExport.exportWorkflowAppDsl).toHaveBeenCalledWith({
          appId: workflowApp.id,
          appName: workflowApp.name,
        })
      })
      expect(mockAppDslExport.exportAppDsl).not.toHaveBeenCalled()
    })

    it('should show DSL export modal when workflow has secret variables', async () => {
      mockWorkflowAppDslExport.exportWorkflowAppDsl.mockResolvedValueOnce({
        status: 'confirmation-required',
        secretEnvList: [{ value_type: 'secret', name: 'API_KEY', value: 'secret' }],
      })

      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('dsl-export-modal')).toBeInTheDocument()
      })
    })

    it('should not open a modal when the workflow command downloads directly', async () => {
      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      await waitFor(() => {
        expect(mockWorkflowAppDslExport.exportWorkflowAppDsl).toHaveBeenCalledWith({
          appId: workflowApp.id,
          appName: workflowApp.name,
        })
      })

      expect(screen.queryByTestId('dsl-export-modal')).not.toBeInTheDocument()
    })

    it('should use the workflow export command for advanced chat apps', async () => {
      const advancedChatApp = { ...mockApp, mode: AppModeEnum.ADVANCED_CHAT }
      render(<AppCard app={advancedChatApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      await waitFor(() => {
        expect(mockWorkflowAppDslExport.exportWorkflowAppDsl).toHaveBeenCalledWith({
          appId: advancedChatApp.id,
          appName: advancedChatApp.name,
        })
      })
    })

    it('should close DSL export modal when onClose is called', async () => {
      mockWorkflowAppDslExport.exportWorkflowAppDsl.mockResolvedValueOnce({
        status: 'confirmation-required',
        secretEnvList: [{ value_type: 'secret', name: 'API_KEY', value: 'secret' }],
      })

      const workflowApp = { ...mockApp, mode: AppModeEnum.WORKFLOW }
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.export'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('dsl-export-modal')).toBeInTheDocument()
      })

      // Click close button to trigger onClose
      fireEvent.click(screen.getByTestId('close-dsl-export'))

      await waitFor(() => {
        expect(screen.queryByTestId('dsl-export-modal')).not.toBeInTheDocument()
      })
    })
  })

  describe('Edit mutation', () => {
    it('should handle edit failure', async () => {
      mockUpdateAppMutation.mockRejectedValueOnce(new Error('Edit failed'))

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.editApp'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-edit-modal'))

      await waitFor(() => {
        expect(mockUpdateAppMutation).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({
          type: 'error',
          message: expect.stringContaining('Edit failed'),
        })
      })
    })

    it('should fall back to the default edit failure message', async () => {
      mockUpdateAppMutation.mockRejectedValueOnce({ message: '' })

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.editApp'))
      })

      await waitFor(() => {
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByTestId('confirm-edit-modal'))

      await waitFor(() => {
        expect(mockUpdateAppMutation).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: 'app.editFailed' })
      })
    })
  })

  describe('Operations behavior', () => {
    it('should close operations menu after selecting an item', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      fireEvent.click(await screen.findByRole('menuitem', { name: 'app.editApp' }))

      await waitFor(() => {
        expect(getOperationsTrigger()).toHaveAttribute('aria-expanded', 'false')
        expect(screen.getByTestId('edit-app-modal')).toBeInTheDocument()
      })
    })

    it('should handle open in explore via async window', async () => {
      let openedUrl = ''
      // Configure mockOpenAsyncWindow to actually call the callback
      mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>) => {
        openedUrl = await callback()
      })

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        const openInExploreBtn = screen.getByText('app.openInExplore')
        fireEvent.click(openInExploreBtn)
      })

      await waitFor(() => {
        expect(exploreService.fetchInstalledAppList).toHaveBeenCalledWith(mockApp.id)
        expect(openedUrl).toBe('/installed/installed-1')
      })
    })

    it('should show string errors from open in explore onError callback', async () => {
      mockOpenAsyncWindow.mockImplementationOnce(
        async (
          _callback: () => Promise<string>,
          options?: { onError?: (err: unknown) => void },
        ) => {
          options?.onError?.('Window failed')
        },
      )

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('app.openInExplore'))
      })

      await waitFor(() => {
        expect(toastMocks.record).toHaveBeenCalledWith({ type: 'error', message: 'Window failed' })
      })
    })
  })

  describe('Access Control', () => {
    it('should render the tour-controlled operations menu as presentation only', async () => {
      render(
        <AppCard
          app={mockApp}
          stepByStepTourActionMenuHighlightPart={
            STEP_BY_STEP_TOUR_TARGETS.studioWithAppsFirstAppCardActionsMenu
          }
          stepByStepTourActionMenuOpen
        />,
      )

      expect(await screen.findByText('app.editApp')).toBeInTheDocument()
      expect(
        screen.getByRole('menuitem', { name: 'app.editApp', hidden: true }),
      ).toBeInTheDocument()
      expect(
        document.querySelector(
          `[data-step-by-step-tour-highlight-part="${STEP_BY_STEP_TOUR_TARGETS.studioWithAppsFirstAppCardActionsMenu}"]`,
        ),
      ).toHaveAttribute(
        'data-step-by-step-tour-highlight-part',
        STEP_BY_STEP_TOUR_TARGETS.studioWithAppsFirstAppCardActionsMenu,
      )
      expect(screen.getByRole('menu', { hidden: true })).toHaveAttribute('aria-hidden', 'true')
      expect(screen.getByRole('menu', { hidden: true })).toHaveClass('pointer-events-none')
    })
  })

  describe('Open in Explore - No App Found', () => {
    it('should tell workflow users to publish before opening in explore', async () => {
      const workflowApp = createMockApp({
        mode: AppModeEnum.WORKFLOW,
        workflow: undefined,
      })
      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByText('app.openInExplore'))

      expect(mockOpenAsyncWindow).not.toHaveBeenCalled()
      expect(exploreService.fetchInstalledAppList).not.toHaveBeenCalled()
      expect(toastMocks.record).toHaveBeenCalledWith({
        type: 'error',
        message: 'app.notPublishedYet',
      })
    })

    it('should handle case when installed_apps is empty array', async () => {
      vi.mocked(exploreService.fetchInstalledAppList).mockResolvedValueOnce({
        has_more: false,
        installed_apps: [],
        next_cursor: null,
      })

      // Configure mockOpenAsyncWindow to call the callback and trigger error
      mockOpenAsyncWindow.mockImplementationOnce(
        async (callback: () => Promise<string>, options?: { onError?: (err: unknown) => void }) => {
          try {
            await callback()
          } catch (err) {
            options?.onError?.(err)
          }
        },
      )

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        const openInExploreBtn = screen.getByText('app.openInExplore')
        fireEvent.click(openInExploreBtn)
      })

      await waitFor(() => {
        expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
        expect(toastMocks.record).toHaveBeenCalledWith({
          type: 'error',
          message: 'app.notPublishedYet',
        })
      })
    })

    it('should handle case when API throws in callback', async () => {
      vi.mocked(exploreService.fetchInstalledAppList).mockRejectedValueOnce(
        new Error('Network error'),
      )

      // Configure mockOpenAsyncWindow to call the callback without catching
      mockOpenAsyncWindow.mockImplementationOnce(async (callback: () => Promise<string>) => {
        return await callback()
      })

      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        const openInExploreBtn = screen.getByText('app.openInExplore')
        fireEvent.click(openInExploreBtn)
      })

      await waitFor(() => {
        expect(exploreService.fetchInstalledAppList).toHaveBeenCalled()
      })
    })
  })

  describe('Draft Trigger Apps', () => {
    it('should not show open in explore option for apps with has_draft_trigger', async () => {
      const draftTriggerApp = createMockApp({ has_draft_trigger: true })
      render(<AppCard app={draftTriggerApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        expect(screen.getByText('app.editApp')).toBeInTheDocument()
        // openInExplore should not be shown for draft trigger apps
        expect(screen.queryByText('app.openInExplore')).not.toBeInTheDocument()
      })
    })
  })

  describe('Non-editor User', () => {
    it('should handle non-editor workspace users', () => {
      // This tests the isCurrentWorkspaceEditor=true branch (default mock)
      render(<AppCard app={mockApp} />)
      expect(screen.getByRole('link', { name: 'Test App' })).toBeInTheDocument()
    })
  })

  describe('WebApp Auth Enabled', () => {
    beforeEach(() => {
      mockWebappAuthEnabled = true
    })

    it('should omit web app access control when webapp_auth is enabled', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        expect(screen.getByText('app.editApp')).toBeInTheDocument()
      })
      expect(screen.queryByText('app.accessControl')).not.toBeInTheDocument()
    })

    it('should omit web app access control for release-and-version permission', async () => {
      const appWithReleasePermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.ReleaseAndVersion, AppACLPermission.Delete],
      })
      render(<AppCard app={appWithReleasePermission} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
      })
      expect(screen.queryByText('app.accessControl')).not.toBeInTheDocument()
    })

    it('should show resource access option when user only has app access config permission', async () => {
      const appWithAccessConfigPermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.AccessConfig, AppACLPermission.Delete],
      })
      render(<AppCard app={appWithAccessConfigPermission} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
      })
      expect(screen.queryByText('app.accessControl')).not.toBeInTheDocument()
      expect(screen.getByText('common.settings.resourceAccess')).toBeInTheDocument()
    })

    it('should hide resource access option when RBAC is disabled', async () => {
      mockRbacEnabled = false
      const appWithAccessConfigPermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.AccessConfig, AppACLPermission.Delete],
      })
      render(<AppCard app={appWithAccessConfigPermission} />)

      fireEvent.click(getOperationsTrigger())

      await waitFor(() => {
        expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
      })
      expect(screen.queryByText('common.settings.resourceAccess')).not.toBeInTheDocument()
    })

    it('should navigate to app access config when resource access is clicked', async () => {
      const appWithAccessConfigPermission = createMockApp({
        created_by: 'another-user',
        maintainer: 'another-user',
        permission_keys: [AppACLPermission.AccessConfig],
      })
      render(<AppCard app={appWithAccessConfigPermission} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        fireEvent.click(screen.getByText('common.settings.resourceAccess'))
      })

      expect(mockPush).toHaveBeenCalledWith('/app/test-app-id/access-config')
    })

    it('should show open in explore when userCanAccessApp is true', async () => {
      render(<AppCard app={mockApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
      })
    })

    it('should keep open in explore visible for unpublished workflow apps while access check is pending', async () => {
      mockUserCanAccessApp.result = false
      mockUserCanAccessApp.isLoading = true
      const workflowApp = createMockApp({
        mode: AppModeEnum.WORKFLOW,
        workflow: undefined,
      })

      render(<AppCard app={workflowApp} />)

      fireEvent.click(getOperationsTrigger())
      await waitFor(() => {
        expect(screen.getByText('app.openInExplore')).toBeInTheDocument()
      })

      fireEvent.click(screen.getByText('app.openInExplore'))

      expect(mockOpenAsyncWindow).not.toHaveBeenCalled()
      expect(exploreService.fetchInstalledAppList).not.toHaveBeenCalled()
      expect(toastMocks.record).toHaveBeenCalledWith({
        type: 'error',
        message: 'app.notPublishedYet',
      })
    })
  })
})
