import type {
  PluginCategoryInstalledPluginResponse,
  PluginEntity,
  PluginInstallationItemResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import type { Permissions, PluginTaskStart } from '@/app/components/plugins/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { FormTypeEnum } from '@/app/components/base/form/types'
import {
  AUTO_UPDATE_MODE,
  AUTO_UPDATE_STRATEGY,
} from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
import {
  PermissionType,
  PluginCategoryEnum,
  PluginSource,
  TaskStatus,
} from '@/app/components/plugins/types'
import { consoleQuery } from '@/service/client'
import { renderHook } from '@/test/console/render'
import {
  normalizeInstalledPluginDetail,
  useInstalledPluginList,
  useInvalidateInstalledPluginList,
  useMutationPluginAutoUpgradeSettings,
  useMutationPluginPermissionSettings,
  usePluginAutoUpgradeSettings,
  usePluginTaskList,
  useRemoveFilteredInstalledPluginPageOnUnmount,
  useVersionListOfPlugin,
} from '../use-plugins'

const { mockGet, mockGetMarketplace, mockPost, mockRequest } = vi.hoisted(() => ({
  mockGet: vi.fn(),
  mockGetMarketplace: vi.fn(),
  mockPost: vi.fn(),
  mockRequest: vi.fn(),
}))

vi.mock('@/service/base', () => ({
  get: mockGet,
  getMarketplace: mockGetMarketplace,
  post: mockPost,
  postMarketplace: vi.fn(),
  request: mockRequest,
}))

mockRequest.mockImplementation(async (url: string) => {
  const data = await mockGet(url)
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' },
  })
})

vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({
  default: () => ({
    refreshPluginList: vi.fn(),
  }),
}))

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

  return createPermissionStateModuleMock(() => ({
    workspacePermissionKeys: ['plugin.install'],
  }))
})

const { mockInvalidateAllBuiltInTools } = vi.hoisted(() => ({
  mockInvalidateAllBuiltInTools: vi.fn(),
}))

vi.mock('../use-tools', () => ({
  useInvalidateAllBuiltInTools: () => mockInvalidateAllBuiltInTools,
}))

const createQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
      mutations: {
        retry: false,
      },
    },
  })

const createWrapper = (queryClient: QueryClient) => {
  return function Wrapper({ children }: { children: ReactNode }) {
    return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  }
}

const createPluginInstallation = (): PluginInstallationItemResponse => ({
  id: 'installation-row-id',
  created_at: '2026-06-01T00:00:00Z',
  updated_at: '2026-06-02T00:00:00Z',
  plugin_id: 'langgenius/full-declaration',
  plugin_unique_identifier: 'langgenius/full-declaration:1.0.0@test',
  tenant_id: 'tenant-id',
  endpoints_setups: 1,
  endpoints_active: 1,
  version: '1.0.0',
  source: PluginSource.marketplace,
  runtime_type: 'local',
  checksum: 'checksum',
  meta: {
    repo: 'langgenius/full-declaration',
    version: '1.0.0',
    package: 'full-declaration.difypkg',
  },
  declaration: {
    version: '1.0.0',
    author: 'Dify',
    icon: 'icon.svg',
    icon_dark: 'icon-dark.svg',
    name: 'full-declaration',
    category: PluginCategoryEnum.trigger,
    label: { en_US: 'Full declaration', zh_Hans: 'Full declaration' },
    description: { en_US: 'Full declaration plugin', zh_Hans: 'Full declaration plugin' },
    created_at: '2026-06-01T00:00:00Z',
    resource: {},
    plugins: {},
    verified: true,
    tags: ['automation'],
    meta: {
      version: '1.0.0',
      minimum_dify_version: '1.4.0',
    },
    tool: {
      identity: {
        author: 'Dify',
        name: 'tool-provider',
        description: { en_US: 'Tool provider' },
        icon: 'tool.svg',
        label: { en_US: 'Tool provider' },
        tags: ['tool'],
      },
      credentials_schema: [
        {
          name: 'api_key',
          label: { en_US: 'API key' },
          type: 'secret-input',
          required: true,
          default: 'token',
        },
      ],
    },
    datasource: {
      identity: {
        author: 'Dify',
        name: 'datasource-provider',
        description: { en_US: 'Datasource provider' },
        icon: 'datasource.svg',
        label: { en_US: 'Datasource provider' },
        tags: ['datasource'],
      },
      credentials_schema: [],
    },
    endpoint: {
      settings: [
        {
          name: 'endpoint_secret',
          label: { en_US: 'Endpoint secret' },
          type: 'secret-input',
          required: true,
          default: '',
        },
      ],
      endpoints: [
        {
          path: '/webhook',
          method: 'POST',
          hidden: false,
        },
      ],
    },
    trigger: {
      events: [
        {
          name: 'issue_created',
          identity: {
            author: 'Dify',
            name: 'issue_created',
            label: { en_US: 'Issue created' },
            provider: 'github',
          },
          description: { en_US: 'Issue created event' },
          parameters: [
            {
              name: 'retry_count',
              label: { en_US: 'Retry count' },
              type: 'number',
              default: 0,
              required: false,
              multiple: false,
            },
          ],
          output_schema: {
            type: 'object',
          },
        },
      ],
      identity: {
        author: 'Dify',
        name: 'github-trigger',
        label: { en_US: 'GitHub trigger' },
        description: { en_US: 'GitHub trigger provider' },
        icon: 'trigger.svg',
        tags: ['trigger'],
      },
      subscription_constructor: {
        credentials_schema: [],
        oauth_schema: {
          client_schema: [],
          credentials_schema: [],
        },
        parameters: [
          {
            name: 'max_retries',
            label: { en_US: 'Max retries' },
            type: 'number',
            default: 3,
            required: false,
            multiple: false,
          },
        ],
      },
      subscription_schema: [
        {
          name: 'repository',
          label: { en_US: 'Repository' },
          type: 'text-input',
          required: true,
        },
      ],
    },
  },
})

const createInstalledPlugin = (
  pluginId: string,
  category: PluginCategoryEnum,
): PluginCategoryInstalledPluginResponse => {
  const installation = createPluginInstallation()

  return {
    ...installation,
    installation_id: installation.id,
    name: installation.declaration.name,
    plugin_id: pluginId,
    declaration: {
      ...installation.declaration,
      category,
    },
  }
}

const createPluginEntity = (): PluginEntity => ({
  checksum: 'checksum',
  created_at: '2026-06-01T00:00:00Z',
  declaration: {
    author: 'Dify',
    category: PluginCategoryEnum.tool,
    created_at: '2026-06-01T00:00:00Z',
    description: { en_US: 'Plugin description' },
    icon: 'icon.svg',
    label: { en_US: 'Plugin label' },
    meta: { version: '1.0.0' },
    name: 'plugin-entity',
    plugins: {},
    resource: { memory: 0 },
    version: '1.0.0',
  },
  endpoints_active: 1,
  endpoints_setups: 1,
  id: 'plugin-entity-id',
  installation_id: 'plugin-entity-id',
  meta: {},
  name: 'plugin-entity',
  plugin_id: 'langgenius/plugin-entity',
  plugin_unique_identifier: 'langgenius/plugin-entity:1.0.0@test',
  runtime_type: 'local',
  source: PluginSource.marketplace,
  tenant_id: 'tenant-id',
  updated_at: '2026-06-02T00:00:00Z',
  version: '1.0.0',
})

describe('normalizeInstalledPluginDetail', () => {
  it('adapts generic and category list items to the legacy detail model', () => {
    const genericDetail = normalizeInstalledPluginDetail(createPluginEntity())
    const categoryDetail = normalizeInstalledPluginDetail(
      createInstalledPlugin('langgenius/category-plugin', PluginCategoryEnum.tool),
    )

    expect(genericDetail.plugin_id).toBe('langgenius/plugin-entity')
    expect(categoryDetail.plugin_id).toBe('langgenius/category-plugin')
  })

  it('should preserve generated plugin declaration capabilities', () => {
    const detail = normalizeInstalledPluginDetail(createPluginInstallation())

    expect(detail.name).toBe('full-declaration')
    expect(detail.installation_id).toBe('installation-row-id')
    expect(detail.latest_version).toBe('1.0.0')
    expect(detail.latest_unique_identifier).toBe('langgenius/full-declaration:1.0.0@test')
    expect(detail.status).toBe('active')
    expect(detail.declaration.tool?.identity.name).toBe('tool-provider')
    expect(detail.declaration.tool?.credentials_schema[0]?.name).toBe('api_key')
    expect(detail.declaration.datasource?.identity.name).toBe('datasource-provider')
    expect(detail.declaration.endpoint?.endpoints?.[0]).toEqual({
      path: '/webhook',
      method: 'POST',
      hidden: false,
    })
    expect(detail.declaration.trigger.identity.name).toBe('github-trigger')
    expect(detail.declaration.trigger.events[0]?.parameters[0]?.default).toBe(0)
    expect(detail.declaration.trigger.subscription_constructor.parameters[0]?.type).toBe(
      FormTypeEnum.textNumber,
    )
    expect(detail.declaration.trigger.subscription_schema[0]?.name).toBe('repository')
  })
})

describe('use-plugins mutations', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('optimistically updates category auto-upgrade cache before the request finishes', async () => {
    const queryClient = createQueryClient()
    const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
    const previousAutoUpgrade = {
      strategy_setting: AUTO_UPDATE_STRATEGY.latest,
      upgrade_time_of_day: 0,
      upgrade_mode: AUTO_UPDATE_MODE.exclude,
      exclude_plugins: [],
      include_plugins: [],
    }
    const nextAutoUpgrade = {
      ...previousAutoUpgrade,
      upgrade_time_of_day: 3600,
    }
    let resolvePost: (value: unknown) => void = () => {}
    mockPost.mockReturnValue(
      new Promise((resolve) => {
        resolvePost = resolve
      }),
    )
    queryClient.setQueryData(queryKey, {
      category: PluginCategoryEnum.model,
      auto_upgrade: previousAutoUpgrade,
    })

    const { result } = renderHook(
      () => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
      { wrapper: createWrapper(queryClient) },
    )

    act(() => {
      result.current.mutate(nextAutoUpgrade)
    })

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/change', {
        body: {
          category: PluginCategoryEnum.model,
          auto_upgrade: nextAutoUpgrade,
        },
      })
    })
    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual({
        category: PluginCategoryEnum.model,
        auto_upgrade: nextAutoUpgrade,
      })
    })

    resolvePost({})
  })

  it('optimistically updates plugin permission cache before the request finishes', async () => {
    const queryClient = createQueryClient()
    const queryKey = ['plugins', 'referenceSettings', 'permission']
    const previousPermission: Permissions = {
      install_permission: PermissionType.admin,
      debug_permission: PermissionType.admin,
    }
    const nextPermission: Permissions = {
      install_permission: PermissionType.everyone,
      debug_permission: PermissionType.admin,
    }
    let resolvePost: (value: unknown) => void = () => {}
    mockPost.mockReturnValue(
      new Promise((resolve) => {
        resolvePost = resolve
      }),
    )
    queryClient.setQueryData(queryKey, previousPermission)

    const { result } = renderHook(() => useMutationPluginPermissionSettings(), {
      wrapper: createWrapper(queryClient),
    })

    act(() => {
      result.current.mutate(nextPermission)
    })

    await waitFor(() => {
      expect(mockPost).toHaveBeenCalledWith('/workspaces/current/plugin/permission/change', {
        body: nextPermission,
      })
    })
    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
    })

    resolvePost({})
  })

  it('rolls back category auto-upgrade cache when the request fails', async () => {
    const queryClient = createQueryClient()
    const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
    const previousAutoUpgrade = {
      strategy_setting: AUTO_UPDATE_STRATEGY.latest,
      upgrade_time_of_day: 0,
      upgrade_mode: AUTO_UPDATE_MODE.exclude,
      exclude_plugins: [],
      include_plugins: [],
    }
    const nextAutoUpgrade = {
      ...previousAutoUpgrade,
      upgrade_time_of_day: 3600,
    }
    let rejectPost: (reason?: unknown) => void = () => {}
    mockPost.mockReturnValue(
      new Promise((_resolve, reject) => {
        rejectPost = reject
      }),
    )
    queryClient.setQueryData(queryKey, {
      category: PluginCategoryEnum.model,
      auto_upgrade: previousAutoUpgrade,
    })

    const { result } = renderHook(
      () => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
      { wrapper: createWrapper(queryClient) },
    )

    const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual({
        category: PluginCategoryEnum.model,
        auto_upgrade: nextAutoUpgrade,
      })
    })

    rejectPost(new Error('auto-upgrade update failed'))

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual({
        category: PluginCategoryEnum.model,
        auto_upgrade: previousAutoUpgrade,
      })
    })
    await mutation
  })

  it('clears optimistic category auto-upgrade cache when the request fails without previous cache', async () => {
    const queryClient = createQueryClient()
    const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
    const nextAutoUpgrade = {
      strategy_setting: AUTO_UPDATE_STRATEGY.latest,
      upgrade_time_of_day: 3600,
      upgrade_mode: AUTO_UPDATE_MODE.exclude,
      exclude_plugins: [],
      include_plugins: [],
    }
    let rejectPost: (reason?: unknown) => void = () => {}
    mockPost.mockReturnValue(
      new Promise((_resolve, reject) => {
        rejectPost = reject
      }),
    )

    const { result } = renderHook(
      () => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
      { wrapper: createWrapper(queryClient) },
    )

    const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual({
        category: PluginCategoryEnum.model,
        auto_upgrade: nextAutoUpgrade,
      })
    })

    rejectPost(new Error('auto-upgrade update failed'))

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toBeUndefined()
    })
    await mutation
  })

  it('rolls back plugin permission cache when the request fails', async () => {
    const queryClient = createQueryClient()
    const queryKey = ['plugins', 'referenceSettings', 'permission']
    const previousPermission: Permissions = {
      install_permission: PermissionType.admin,
      debug_permission: PermissionType.admin,
    }
    const nextPermission: Permissions = {
      install_permission: PermissionType.everyone,
      debug_permission: PermissionType.admin,
    }
    let rejectPost: (reason?: unknown) => void = () => {}
    mockPost.mockReturnValue(
      new Promise((_resolve, reject) => {
        rejectPost = reject
      }),
    )
    queryClient.setQueryData(queryKey, previousPermission)

    const { result } = renderHook(() => useMutationPluginPermissionSettings(), {
      wrapper: createWrapper(queryClient),
    })

    const mutation = result.current.mutateAsync(nextPermission).catch(() => undefined)

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
    })

    rejectPost(new Error('permission update failed'))

    await waitFor(() => {
      expect(queryClient.getQueryData(queryKey)).toEqual(previousPermission)
    })
    await mutation
  })
})

describe('useVersionListOfPlugin', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('does not fetch versions while disabled', () => {
    const queryClient = createQueryClient()

    renderHook(() => useVersionListOfPlugin('plugin-1', false), {
      wrapper: createWrapper(queryClient),
    })

    expect(mockGetMarketplace).not.toHaveBeenCalled()
  })

  it('fetches versions when enabled', async () => {
    const queryClient = createQueryClient()
    mockGetMarketplace.mockResolvedValue({ data: { versions: [] } })

    renderHook(() => useVersionListOfPlugin('plugin-1'), {
      wrapper: createWrapper(queryClient),
    })

    await waitFor(() => {
      expect(mockGetMarketplace).toHaveBeenCalledWith('/plugins/plugin-1/versions', {
        params: { page: 1, page_size: 100 },
      })
    })
  })
})

describe('useInstalledPluginList', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('fetches the default installed plugin list when no category is provided', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], total: 0 })

    renderHook(() => useInstalledPluginList(), { wrapper: createWrapper(queryClient) })

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/list?page=1&page_size=100',
      )
    })
  })

  it('does not fetch or expose data when disabled', () => {
    const queryClient = createQueryClient()

    const { result } = renderHook(() => useInstalledPluginList({ enabled: false }), {
      wrapper: createWrapper(queryClient),
    })

    expect(mockGet).not.toHaveBeenCalled()
    expect(result.current.data).toBeUndefined()
  })

  it('fetches the scoped installed plugin category list when category is provided', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], has_more: false })

    renderHook(() => useInstalledPluginList({ category: PluginCategoryEnum.trigger }), {
      wrapper: createWrapper(queryClient),
    })

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/trigger/list?page=1&page_size=100',
      )
    })
  })

  it('filters every installed tool page on the server', async () => {
    const queryClient = createQueryClient()
    mockGet
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('tool-plugin-1', PluginCategoryEnum.tool)],
        has_more: true,
      })
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('tool-plugin-2', PluginCategoryEnum.tool)],
        has_more: false,
      })

    const { result } = renderHook(
      () =>
        useInstalledPluginList({
          category: PluginCategoryEnum.tool,
          filters: {
            language: 'zh_Hans',
            query: 'slack',
            tags: ['communication', 'api'],
          },
          pageSize: 30,
        }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=1&page_size=30&query=slack&language=zh_Hans&tags=communication&tags=api',
      )
    })

    act(() => {
      result.current.loadNextPage()
    })

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=2&page_size=30&query=slack&language=zh_Hans&tags=communication&tags=api',
      )
    })
  })

  it('keeps category lists with different page sizes in separate caches', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], has_more: false })
    const wrapper = createWrapper(queryClient)

    renderHook(() => useInstalledPluginList({ category: PluginCategoryEnum.tool, pageSize: 30 }), {
      wrapper,
    })
    renderHook(() => useInstalledPluginList({ category: PluginCategoryEnum.tool }), {
      wrapper,
    })

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=1&page_size=30',
      )
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=1&page_size=100',
      )
    })
  })

  it('keeps category lists with different filters in separate caches', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], has_more: false })
    const wrapper = createWrapper(queryClient)

    renderHook(
      () =>
        useInstalledPluginList({ category: PluginCategoryEnum.tool, filters: { query: 'slack' } }),
      { wrapper },
    )
    renderHook(
      () =>
        useInstalledPluginList({ category: PluginCategoryEnum.tool, filters: { query: 'notion' } }),
      { wrapper },
    )

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=1&page_size=100&query=slack',
      )
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/tool/list?page=1&page_size=100&query=notion',
      )
    })
  })

  it('keeps builtin tools from the scoped tool plugin category response', async () => {
    const queryClient = createQueryClient()
    const builtinTools = [
      {
        id: 'builtin-tool',
        name: 'builtin-tool',
        label: { en_US: 'Builtin Tool', zh_Hans: 'Builtin Tool' },
        description: { en_US: 'Builtin Tool description', zh_Hans: 'Builtin Tool description' },
        author: 'Dify',
        icon: '',
        type: 'builtin',
        team_credentials: {},
        is_team_authorization: false,
        allow_delete: false,
        labels: [],
      },
    ]
    mockGet.mockResolvedValue({ plugins: [], builtin_tools: builtinTools, has_more: false })

    const { result } = renderHook(
      () => useInstalledPluginList({ category: PluginCategoryEnum.tool }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.data?.builtin_tools).toEqual(builtinTools)
    })
  })

  it('uses has_more to load the next scoped plugin category page', async () => {
    const queryClient = createQueryClient()
    mockGet
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('trigger-plugin-1', PluginCategoryEnum.trigger)],
        has_more: true,
      })
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('trigger-plugin-2', PluginCategoryEnum.trigger)],
        has_more: false,
      })

    const { result } = renderHook(
      () => useInstalledPluginList({ category: PluginCategoryEnum.trigger }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.isLastPage).toBe(false)
    })

    act(() => {
      result.current.loadNextPage()
    })

    await waitFor(() => {
      expect(mockGet).toHaveBeenCalledWith(
        'http://localhost:5001/console/api/workspaces/current/plugin/trigger/list?page=2&page_size=100',
      )
    })
    await waitFor(() => {
      expect(result.current.isLastPage).toBe(true)
    })
  })

  it('keeps builtin tools from the first scoped tool plugin page when loading more pages', async () => {
    const queryClient = createQueryClient()
    const builtinTools = [
      {
        id: 'builtin-tool',
        name: 'builtin-tool',
        label: { en_US: 'Builtin Tool', zh_Hans: 'Builtin Tool' },
        description: { en_US: 'Builtin Tool description', zh_Hans: 'Builtin Tool description' },
        author: 'Dify',
        icon: '',
        type: 'builtin',
        team_credentials: {},
        is_team_authorization: false,
        allow_delete: false,
        labels: [],
      },
    ]
    mockGet
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('tool-plugin-1', PluginCategoryEnum.tool)],
        builtin_tools: builtinTools,
        has_more: true,
      })
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('tool-plugin-2', PluginCategoryEnum.tool)],
        builtin_tools: builtinTools,
        has_more: false,
      })

    const { result } = renderHook(
      () => useInstalledPluginList({ category: PluginCategoryEnum.tool }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.isLastPage).toBe(false)
    })

    act(() => {
      result.current.loadNextPage()
    })

    await waitFor(() => {
      expect(result.current.data?.plugins.map((plugin) => plugin.plugin_id)).toEqual([
        'tool-plugin-1',
        'tool-plugin-2',
      ])
    })
    expect(result.current.data?.builtin_tools).toEqual(builtinTools)
  })
})

describe('useRemoveFilteredInstalledPluginPageOnUnmount', () => {
  it('retains every unfiltered category page when leaving the page', async () => {
    const queryClient = createQueryClient()
    mockGet
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('first-page-plugin', PluginCategoryEnum.tool)],
        has_more: true,
      })
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('second-page-plugin', PluginCategoryEnum.tool)],
        has_more: false,
      })
    const wrapper = createWrapper(queryClient)
    const useToolPluginList = () => {
      const pluginList = useInstalledPluginList({
        category: PluginCategoryEnum.tool,
        gcTime: 10 * 60 * 1000,
        pageSize: 30,
        staleTime: 5 * 60 * 1000,
      })
      useRemoveFilteredInstalledPluginPageOnUnmount(PluginCategoryEnum.tool, 30)
      return pluginList
    }
    const { result, unmount } = renderHook(useToolPluginList, {
      wrapper,
    })

    await waitFor(() => expect(result.current.isLastPage).toBe(false))
    act(() => result.current.loadNextPage())
    await waitFor(() => expect(result.current.data?.plugins).toHaveLength(2))
    const cachedQuery = queryClient
      .getQueryCache()
      .getAll()
      .find((query) => (query.state.data as { pages?: unknown[] } | undefined)?.pages?.length === 2)
    if (!cachedQuery) throw new Error('Expected cached second page')
    expect(cachedQuery.options.gcTime).toBe(10 * 60 * 1000)
    expect(cachedQuery.observers[0]?.options.staleTime).toBe(5 * 60 * 1000)

    unmount()

    expect((cachedQuery.state.data as { pages: unknown[] }).pages).toHaveLength(2)
    const requestCount = mockGet.mock.calls.length
    const { result: remountedResult } = renderHook(useToolPluginList, { wrapper })
    expect(remountedResult.current.data?.plugins).toHaveLength(2)
    expect(mockGet).toHaveBeenCalledTimes(requestCount)
  })

  it('removes filtered category pages when leaving the page', async () => {
    const queryClient = createQueryClient()
    const filters = { query: 'slack', tags: ['communication'], language: 'en_US' as const }
    mockGet
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('slack-1', PluginCategoryEnum.tool)],
        has_more: true,
      })
      .mockResolvedValueOnce({
        plugins: [createInstalledPlugin('slack-2', PluginCategoryEnum.tool)],
        has_more: false,
      })
    const wrapper = createWrapper(queryClient)
    const { result, unmount } = renderHook(
      () => {
        const pluginList = useInstalledPluginList({
          category: PluginCategoryEnum.tool,
          filters,
          pageSize: 30,
        })
        useRemoveFilteredInstalledPluginPageOnUnmount(PluginCategoryEnum.tool, 30, filters)
        return pluginList
      },
      { wrapper },
    )
    await waitFor(() => expect(result.current.isLastPage).toBe(false))
    act(() => result.current.loadNextPage())
    await waitFor(() => expect(result.current.data?.plugins).toHaveLength(2))
    const cachedQuery = queryClient
      .getQueryCache()
      .getAll()
      .find((query) => (query.state.data as { pages?: unknown[] } | undefined)?.pages?.length === 2)
    if (!cachedQuery) throw new Error('Expected cached filtered pages')

    unmount()

    expect(
      queryClient.getQueryCache().find({ queryKey: cachedQuery.queryKey, exact: true }),
    ).toBeUndefined()
  })
})

describe('useInvalidateInstalledPluginList', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('only invalidates every cached query for the requested category', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], has_more: false })
    const modelInstalledIdsKey = consoleQuery.workspaces.current.plugin.installedIds.get.queryKey({
      input: { query: { category: PluginCategoryEnum.model } },
    })
    const toolInstalledIdsKey = consoleQuery.workspaces.current.plugin.installedIds.get.queryKey({
      input: { query: { category: PluginCategoryEnum.tool } },
    })
    const modelCategoryQueryKey = consoleQuery.workspaces.current.plugin.byCategory.list.get.key({
      type: 'infinite',
      input: { params: { category: PluginCategoryEnum.model } },
    })
    const toolCategoryQueryKey = consoleQuery.workspaces.current.plugin.byCategory.list.get.key({
      type: 'infinite',
      input: { params: { category: PluginCategoryEnum.tool } },
    })

    queryClient.setQueryData(modelInstalledIdsKey, { plugin_ids: ['langgenius/openai'] })
    queryClient.setQueryData(toolInstalledIdsKey, { plugin_ids: ['langgenius/google'] })

    const { result: listResult, unmount } = renderHook(
      () => ({
        model: useInstalledPluginList({ category: PluginCategoryEnum.model }),
        filteredModel: useInstalledPluginList({
          category: PluginCategoryEnum.model,
          filters: { query: 'openai', language: 'en_US' },
          pageSize: 30,
        }),
        tool: useInstalledPluginList({ category: PluginCategoryEnum.tool }),
      }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(listResult.current.model.isSuccess).toBe(true)
      expect(listResult.current.filteredModel.isSuccess).toBe(true)
      expect(listResult.current.tool.isSuccess).toBe(true)
    })

    unmount()
    const { result } = renderHook(() => useInvalidateInstalledPluginList(), {
      wrapper: createWrapper(queryClient),
    })

    await act(async () => {
      await result.current(PluginCategoryEnum.model)
    })

    const modelCategoryQueries = queryClient
      .getQueryCache()
      .findAll({ queryKey: modelCategoryQueryKey })
    const toolCategoryQueries = queryClient
      .getQueryCache()
      .findAll({ queryKey: toolCategoryQueryKey })

    expect(modelCategoryQueries).toHaveLength(2)
    expect(modelCategoryQueries.every((query) => query.state.isInvalidated)).toBe(true)
    expect(toolCategoryQueries).toHaveLength(1)
    expect(toolCategoryQueries[0]?.state.isInvalidated).toBe(false)
    expect(queryClient.getQueryState(modelInstalledIdsKey)?.isInvalidated).toBe(true)
    expect(queryClient.getQueryState(toolInstalledIdsKey)?.isInvalidated).toBe(false)
    expect(mockInvalidateAllBuiltInTools).not.toHaveBeenCalled()
  })

  it('also invalidates built-in tools for the tool category', async () => {
    const queryClient = createQueryClient()
    mockGet.mockResolvedValue({ plugins: [], has_more: false })

    const { result } = renderHook(
      () => ({
        invalidate: useInvalidateInstalledPluginList(),
        model: useInstalledPluginList({ category: PluginCategoryEnum.model }),
        tool: useInstalledPluginList({ category: PluginCategoryEnum.tool }),
      }),
      { wrapper: createWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.model.isSuccess).toBe(true)
      expect(result.current.tool.isSuccess).toBe(true)
    })

    await act(async () => {
      await result.current.invalidate(PluginCategoryEnum.tool)
    })

    expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledOnce()
  })
})

describe('usePluginTaskList', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('adds the task from an install start response to the task list cache', async () => {
    const queryClient = createQueryClient()
    queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
      install_permission: PermissionType.admin,
      debug_permission: PermissionType.admin,
    })
    mockGet.mockResolvedValue({ tasks: [] })

    const startedTask: PluginTaskStart = {
      id: 'task-new',
      created_at: '2026-06-05T03:34:59.578653Z',
      updated_at: '2026-06-05T03:34:59.578653Z',
      status: 'running',
      total_plugins: 1,
      completed_plugins: 0,
      plugins: [
        {
          plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
          plugin_id: 'langgenius/gitlab_datasource',
          status: TaskStatus.running,
          message: '',
          icon: 'gitlab.png',
          labels: {
            en_US: 'GitLab',
          } as PluginTaskStart['plugins'][number]['labels'],
          source: PluginSource.marketplace,
        },
      ],
    }

    const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
      wrapper: createWrapper(queryClient),
    })

    act(() => {
      result.current.handleInstallTaskStart({
        all_installed: false,
        task_id: 'task-new',
        plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
        task: startedTask,
      })
    })

    expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
      tasks: [
        {
          ...startedTask,
          plugins: [
            {
              ...startedTask.plugins[0],
              taskId: 'task-new',
            },
          ],
        },
      ],
    })
  })

  it('replaces an existing task with the latest start response task data', () => {
    const queryClient = createQueryClient()
    queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
      install_permission: PermissionType.admin,
      debug_permission: PermissionType.admin,
    })
    queryClient.setQueryData(['plugins', 'pluginTaskList'], {
      tasks: [
        {
          id: 'task-new',
          created_at: '2026-06-05T03:34:59.578653Z',
          updated_at: '2026-06-05T03:34:59.578653Z',
          status: 'running',
          total_plugins: 1,
          completed_plugins: 0,
          plugins: [],
        },
        {
          id: 'task-old',
          created_at: '2026-06-04T03:34:59.578653Z',
          updated_at: '2026-06-04T03:34:59.578653Z',
          status: 'success',
          total_plugins: 1,
          completed_plugins: 1,
          plugins: [],
        },
      ],
    })
    mockGet.mockResolvedValue({ tasks: [] })

    const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
      wrapper: createWrapper(queryClient),
    })

    act(() => {
      result.current.handleInstallTaskStart({
        all_installed: false,
        task_id: 'task-new',
        plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
        task: {
          id: 'task-new',
          created_at: '2026-06-05T03:34:59.578653Z',
          updated_at: '2026-06-05T03:35:59.578653Z',
          status: 'success',
          total_plugins: 1,
          completed_plugins: 1,
          plugins: [],
        },
      })
    })

    expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
      tasks: [
        {
          id: 'task-new',
          created_at: '2026-06-05T03:34:59.578653Z',
          updated_at: '2026-06-05T03:35:59.578653Z',
          status: 'success',
          total_plugins: 1,
          completed_plugins: 1,
          plugins: [],
        },
        {
          id: 'task-old',
          created_at: '2026-06-04T03:34:59.578653Z',
          updated_at: '2026-06-04T03:34:59.578653Z',
          status: 'success',
          total_plugins: 1,
          completed_plugins: 1,
          plugins: [],
        },
      ],
    })
  })

  it('keeps a locally started unfinished task when an immediate refetch returns a stale task list', async () => {
    const queryClient = createQueryClient()
    queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
      install_permission: PermissionType.admin,
      debug_permission: PermissionType.admin,
    })
    const staleTask = {
      id: 'task-old',
      created_at: '2026-06-04T03:34:59.578653Z',
      updated_at: '2026-06-04T03:34:59.578653Z',
      status: TaskStatus.success,
      total_plugins: 1,
      completed_plugins: 1,
      plugins: [],
    }
    mockGet.mockResolvedValue({ tasks: [staleTask] })
    const startedTask: PluginTaskStart = {
      id: 'task-new',
      created_at: '2026-06-05T03:34:59.578653Z',
      updated_at: '2026-06-05T03:34:59.578653Z',
      status: TaskStatus.running,
      total_plugins: 1,
      completed_plugins: 0,
      plugins: [
        {
          plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
          plugin_id: 'langgenius/gitlab_datasource',
          status: TaskStatus.pending,
          message: '',
          icon: 'gitlab.png',
          labels: {
            en_US: 'GitLab',
          } as PluginTaskStart['plugins'][number]['labels'],
          source: PluginSource.marketplace,
        },
      ],
    }

    const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
      wrapper: createWrapper(queryClient),
    })

    await waitFor(() => {
      expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
        tasks: [staleTask],
      })
    })

    act(() => {
      result.current.handleInstallTaskStart({
        all_installed: false,
        task_id: 'task-new',
        task: startedTask,
      })
    })

    await waitFor(() => {
      expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
        tasks: [
          {
            ...startedTask,
            plugins: [
              {
                ...startedTask.plugins[0],
                taskId: 'task-new',
              },
            ],
          },
          staleTask,
        ],
      })
    })
  })
})

describe('usePluginAutoUpgradeSettings', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('does not expose frontend default settings before backend data resolves', () => {
    const queryClient = createQueryClient()
    mockGet.mockReturnValue(new Promise(() => {}))

    const { result } = renderHook(() => usePluginAutoUpgradeSettings(PluginCategoryEnum.model), {
      wrapper: createWrapper(queryClient),
    })

    expect(result.current.data).toBeUndefined()
    expect(mockGet).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/fetch', {
      params: {
        category: PluginCategoryEnum.model,
      },
    })
  })

  it('returns backend auto-upgrade settings when the request resolves', async () => {
    const queryClient = createQueryClient()
    const backendAutoUpgrade = {
      strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
      upgrade_time_of_day: 0,
      upgrade_mode: AUTO_UPDATE_MODE.exclude,
      exclude_plugins: [],
      include_plugins: [],
    }
    mockGet.mockResolvedValue({
      category: PluginCategoryEnum.tool,
      auto_upgrade: backendAutoUpgrade,
    })

    const { result } = renderHook(() => usePluginAutoUpgradeSettings(PluginCategoryEnum.tool), {
      wrapper: createWrapper(queryClient),
    })

    await waitFor(() => {
      expect(result.current.data).toEqual({
        category: PluginCategoryEnum.tool,
        auto_upgrade: backendAutoUpgrade,
      })
    })
  })
})
