import type { TagResponse as Tag } from '@dify/contracts/api/console/tags/types.gen'
import type {
  SkillReferenceResponse,
  SkillResponse,
  SkillTagResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import { toast } from '@langgenius/dify-ui/toast'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import SkillsPage from '../page'

type SkillsInfiniteOptions = {
  getNextPageParam: (lastPage: { has_more: boolean; page: number }) => number | undefined
  initialPageParam: number
  input: (pageParam: unknown) => {
    query: Record<string, unknown>
  }
}

const mocks = vi.hoisted(() => ({
  createSkillMutationFn: vi.fn(),
  deleteSkillMutationFn: vi.fn(),
  downloadBlob: vi.fn(),
  duplicateSkillMutationFn: vi.fn(),
  exportSkillArchiveBlob: vi.fn(),
  importSkillMutationFn: vi.fn(),
  push: vi.fn(),
  genericTags: [] as Tag[],
  genericTagsQueryOptions: vi.fn((_options: unknown) => ({})),
  queryState: {
    keyword: '',
    tag: [] as string[],
  },
  skills: [] as SkillResponse[],
  skillPages: [] as SkillResponse[][],
  skillsKey: vi.fn((_options: unknown): unknown[] => ['skills']),
  skillsQueryOptions: vi.fn((_options: SkillsInfiniteOptions) => ({})),
  skillReferences: [] as SkillReferenceResponse[],
  skillReferencesQueryOptions: vi.fn((_options: unknown) => ({})),
  tags: [] as SkillTagResponse[],
  tagsKey: vi.fn((_options: unknown): unknown[] => ['skill-tags']),
  tagsQueryOptions: vi.fn((_options: unknown) => ({})),
}))

vi.mock('@langgenius/dify-ui/toast', () => ({
  toast: {
    error: vi.fn(),
    success: vi.fn(),
  },
}))

vi.mock('ahooks', () => ({
  useDebounce: (value: unknown) => value,
}))

vi.mock('nuqs', async () => {
  const React = await import('react')
  const listeners = new Map<'keyword' | 'tag', Set<() => void>>()
  const createParser = () => ({
    withDefault: () => ({
      withOptions: () => ({}),
    }),
  })

  return {
    debounce: () => undefined,
    parseAsArrayOf: () => ({
      withDefault: () => ({}),
    }),
    parseAsString: createParser(),
    useQueryState: (name: 'keyword' | 'tag') => {
      const [value, setValue] = React.useState(mocks.queryState[name])
      React.useEffect(() => {
        const nameListeners = listeners.get(name) ?? new Set<() => void>()
        listeners.set(name, nameListeners)
        const listener = () => setValue(mocks.queryState[name])
        nameListeners.add(listener)

        return () => {
          nameListeners.delete(listener)
        }
      }, [name])
      const setQueryValue = (nextValue: string | string[]) => {
        mocks.queryState[name] = nextValue as never
        setValue(nextValue as never)
        listeners.get(name)?.forEach((listener) => listener())
        return Promise.resolve(new URLSearchParams())
      }

      return [value, setQueryValue] as const
    },
  }
})

vi.mock('@/hooks/use-document-title', () => ({
  default: vi.fn(),
}))

vi.mock('@/hooks/use-format-time-from-now', () => ({
  useFormatTimeFromNow: () => ({
    formatTimeFromNow: () => '2 hours ago',
  }),
}))

vi.mock('@/hooks/use-timestamp', () => ({
  default: () => ({
    formatTime: () => '2026-07-22 10:00',
  }),
}))

vi.mock('@/app/components/base/app-icon', () => ({
  default: ({ icon }: { icon?: string }) => <span>{icon}</span>,
}))

vi.mock('@/features/tag-management/components/skill-card-tags', () => ({
  SkillCardTags: ({ tags }: { tags: string[] }) => (
    <button type="button" aria-label={tags.join(', ')}>
      {tags.join(', ')}
    </button>
  ),
}))

vi.mock('../skill-list-tag-management-modal', () => ({
  SkillListTagManagementModal: () => null,
}))

vi.mock('@/next/link', () => ({
  default: ({ children, href, ...props }: { children: ReactNode; href: string }) => (
    <a href={href} {...props}>
      {children}
    </a>
  ),
}))

vi.mock('@/next/navigation', () => ({
  useRouter: () => ({
    push: mocks.push,
  }),
}))

vi.mock('@/utils/download', () => ({
  downloadBlob: mocks.downloadBlob,
}))

vi.mock('../client', () => ({
  fetchSkillArchiveBlob: mocks.exportSkillArchiveBlob,
  uploadSkillFile: vi.fn(),
}))

vi.mock('@/service/console', () => ({
  consoleQuery: {
    tags: {
      get: {
        queryOptions: mocks.genericTagsQueryOptions,
      },
    },
    workspaces: {
      current: {
        skills: {
          get: {
            key: mocks.skillsKey,
            infiniteOptions: mocks.skillsQueryOptions,
          },
          post: {
            mutationOptions: () => ({ mutationFn: mocks.createSkillMutationFn }),
          },
          import: {
            post: {
              mutationOptions: () => ({ mutationFn: mocks.importSkillMutationFn }),
            },
          },
          tags: {
            get: {
              key: mocks.tagsKey,
              queryOptions: mocks.tagsQueryOptions,
            },
          },
          bySkillId: {
            delete: {
              mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }),
            },
            references: {
              get: {
                queryOptions: mocks.skillReferencesQueryOptions,
              },
            },
            duplicate: {
              post: {
                mutationOptions: () => ({ mutationFn: mocks.duplicateSkillMutationFn }),
              },
            },
          },
        },
      },
    },
  },
}))

vi.mock('../permissions', () => ({
  useSkillPermissions: () => ({ canDelete: true, canEdit: true, canPublish: true }),
}))

function createSkill(overrides: Partial<SkillResponse> = {}): SkillResponse {
  return {
    id: 'skill-1',
    name: 'refund-approval',
    display_name: 'Refund approval',
    icon: '💳',
    description: 'Handle refund requests.',
    tags: ['support'],
    visibility: 'workspace',
    latest_published_version_id: 'version-1',
    latest_published_at: 1784638400,
    reference_count: 2,
    created_at: 1784631405,
    updated_at: 1784638487,
    ...overrides,
  }
}

function createAgentReference(
  overrides: Partial<SkillReferenceResponse> = {},
): SkillReferenceResponse {
  return {
    agent_id: 'agent-1',
    agent_icon: '🤖',
    agent_icon_background: '#EFF6FF',
    agent_icon_type: 'emoji',
    app_id: 'app-1',
    display_name: 'Support Agent',
    name: 'support-agent',
    type: 'agent',
    ...overrides,
  }
}

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: { mutations: { retry: false }, queries: { retry: false } },
  })
}

function renderSkillsPage(queryClient = createTestQueryClient()) {
  return render(
    <QueryClientProvider client={queryClient}>
      <SkillsPage />
    </QueryClientProvider>,
  )
}

describe('SkillsPage', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mocks.queryState.keyword = ''
    mocks.queryState.tag = []
    mocks.skills = [createSkill()]
    mocks.skillPages = [mocks.skills]
    mocks.skillReferences = [createAgentReference()]
    mocks.genericTags = [
      { binding_count: '2', id: 'tag-support', name: 'support', type: 'skill' },
      { binding_count: '1', id: 'tag-sales', name: 'sales', type: 'skill' },
    ]
    mocks.tags = [
      { count: 2, tag: 'support' },
      { count: 1, tag: 'sales' },
    ]
    mocks.skillsKey.mockImplementation((options) => ['skills', options])
    mocks.tagsKey.mockImplementation((options) => ['skill-tags', options])
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skills', options],
      queryFn: async ({ pageParam }: { pageParam: unknown }) => {
        const page = Number(pageParam)
        return {
          data: mocks.skillPages[page - 1] ?? [],
          has_more: page < mocks.skillPages.length,
          page,
          total: mocks.skillPages.flat().length,
        }
      },
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))
    mocks.tagsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skill-tags', options],
      queryFn: async () => ({
        data: mocks.tags,
      }),
    }))
    mocks.genericTagsQueryOptions.mockImplementation((options) => ({
      queryKey: ['tags', options],
      queryFn: async () => mocks.genericTags,
    }))
    mocks.skillReferencesQueryOptions.mockImplementation((options) => ({
      queryKey: ['skill-references', options],
      queryFn: async () => ({
        data: mocks.skillReferences,
      }),
    }))
    mocks.createSkillMutationFn.mockResolvedValue(createSkill({ id: 'created-skill' }))
    mocks.importSkillMutationFn.mockResolvedValue(createSkill({ id: 'imported-skill' }))
    mocks.duplicateSkillMutationFn.mockResolvedValue(createSkill({ id: 'duplicated-skill' }))
    mocks.exportSkillArchiveBlob.mockResolvedValue(new Blob(['skill archive']))
    mocks.deleteSkillMutationFn.mockResolvedValue({
      deleted: true,
      id: 'skill-1',
    })
  })

  it('exposes loading as a status without presenting skeletons as skill items', () => {
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skills-pending', options],
      queryFn: () => new Promise(() => {}),
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))

    renderSkillsPage()

    const skillRegion = screen.getByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    expect(skillRegion).toHaveAttribute('aria-busy', 'true')
    expect(within(skillRegion).getByRole('status')).toHaveTextContent('common.loading')
    expect(within(skillRegion).queryByRole('list')).not.toBeInTheDocument()
    expect(within(skillRegion).queryByRole('listitem')).not.toBeInTheDocument()
  })

  it('exposes a failed skill request as an alert without a stale list', async () => {
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skills-error', options],
      queryFn: () => Promise.reject(new Error('skills request failed')),
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))

    renderSkillsPage()

    const skillRegion = screen.getByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    expect(await within(skillRegion).findByRole('alert')).toHaveTextContent(
      'skill.skillManagement.loadingError',
    )
    expect(
      within(skillRegion).getByRole('button', { name: 'common.operation.retry' }),
    ).toBeInTheDocument()
    expect(within(skillRegion).queryByRole('list')).not.toBeInTheDocument()
  })

  it('replaces a cached empty state with a retryable error when refetch fails', async () => {
    const user = userEvent.setup()
    const queryKey = ['skills-cached-empty-refetch-error']
    let requestCount = 0
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey,
      queryFn: () => {
        requestCount += 1
        return Promise.reject(new Error('skills refetch failed'))
      },
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))
    const queryClient = createTestQueryClient()
    queryClient.setQueryData(queryKey, {
      pages: [{ data: [], has_more: false, page: 1, total: 0 }],
      pageParams: [1],
    })

    renderSkillsPage(queryClient)

    const skillRegion = screen.getByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    const error = await within(skillRegion).findByRole('alert')
    expect(error).toHaveTextContent('skill.skillManagement.loadingError')
    expect(within(skillRegion).queryByText('skill.skillManagement.empty')).not.toBeInTheDocument()
    expect(
      within(skillRegion).queryByRole('button', {
        name: 'skill.skillManagement.emptyAction.createTitle',
      }),
    ).not.toBeInTheDocument()
    expect(within(skillRegion).queryByRole('list')).not.toBeInTheDocument()

    await user.click(within(error).getByRole('button', { name: 'common.operation.retry' }))
    await waitFor(() => {
      expect(requestCount).toBe(2)
    })
  })

  it('renders skills with tags, reference count, and detail links', async () => {
    renderSkillsPage()

    const skillLink = await screen.findByRole('link', { name: /Refund approval/ })
    const skillList = screen.getByRole('list')
    expect(screen.getByRole('listitem', { name: 'Refund approval' })).toBeInTheDocument()
    expect(skillList).toContainElement(skillLink)
    expect(skillLink).toHaveAttribute('href', '/skills/skill-1')
    expect(skillLink).toHaveAccessibleDescription('Handle refund requests.')
    expect(within(skillLink).queryByRole('button')).not.toBeInTheDocument()
    expect(screen.getByText('refund-approval')).toBeInTheDocument()
    expect(screen.getByText('Handle refund requests.')).toBeInTheDocument()
    expect(screen.getByText('support')).toBeInTheDocument()
    expect(
      screen.getByText('skill.skillManagement.referenceCount_other:{"count":2}'),
    ).toBeInTheDocument()
    expect(
      screen.getByText('skill.skillManagement.publishedAt:{"time":"2 hours ago"}'),
    ).toBeInTheDocument()
  })

  it('tabs from the card More action to its tag trigger', async () => {
    const user = userEvent.setup()
    renderSkillsPage()

    const skillLink = await screen.findByRole('link', { name: /Refund approval/ })
    const moreButton = screen.getByRole('button', {
      name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
    })
    const tagTrigger = screen.getByRole('button', { name: 'support' })

    skillLink.focus()
    await user.tab()
    expect(moreButton).toHaveFocus()
    await user.tab()
    expect(tagTrigger).toHaveFocus()
  })

  it('renders draft update time as relative time', async () => {
    mocks.skills = [createSkill({ latest_published_version_id: null, latest_published_at: null })]
    mocks.skillPages = [mocks.skills]

    renderSkillsPage()

    const skillLink = await screen.findByRole('link', { name: 'Refund approval' })
    expect(skillLink).toHaveAccessibleDescription(
      'skill.skillManagement.draft Handle refund requests.',
    )
    expect(
      screen.getByText('skill.skillManagement.editedAt:{"time":"2 hours ago"}'),
    ).toBeInTheDocument()
  })

  it('shows guidance instead of a persisted editor placeholder when description is empty', async () => {
    mocks.skills = [createSkill({ description: '' })]
    mocks.skillPages = [mocks.skills]

    renderSkillsPage()

    expect(await screen.findByText('skill.skillManagement.noDescription')).toBeInTheDocument()
  })

  it('passes keyword and selected tags to the list query', async () => {
    mocks.queryState.keyword = 'refund'
    mocks.queryState.tag = ['support']
    renderSkillsPage()

    await waitFor(() => {
      const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
      expect(queryOptions?.input(1)).toEqual({
        query: {
          keyword: 'refund',
          limit: 20,
          page: 1,
          tag: ['support'],
        },
      })
    })
  })

  it('uses the localized Skills search hint as the search placeholder', async () => {
    renderSkillsPage()

    expect(
      await screen.findByRole('searchbox', {
        name: 'skill.skillManagement.searchLabel',
      }),
    ).toHaveAttribute('placeholder', 'skill.skillManagement.searchPlaceholder')
  })

  it('clears stale tag names from the URL-backed filter state', async () => {
    mocks.queryState.tag = ['renamed-tag']

    renderSkillsPage()

    await waitFor(() => {
      expect(mocks.queryState.tag).toEqual([])
    })
    await waitFor(() => {
      const queryOptions = mocks.skillsQueryOptions.mock.lastCall?.[0]
      expect(queryOptions?.input(1)).toEqual({
        query: {
          limit: 20,
          page: 1,
        },
      })
    })
  })

  it('loads the next skill page when the list scrolls near the bottom', async () => {
    const firstPageSkills = Array.from({ length: 20 }, (_, index) =>
      createSkill({
        id: `skill-${index + 1}`,
        name: `skill-${index + 1}`,
        display_name: `Skill ${index + 1}`,
      }),
    )
    const nextPageSkill = createSkill({
      id: 'skill-21',
      name: 'skill-21',
      display_name: 'Skill 21',
    })
    let resolveNextPage:
      | ((page: { data: SkillResponse[]; has_more: boolean; page: number; total: number }) => void)
      | undefined
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skills-deferred-next-page', options],
      queryFn: async ({ pageParam }: { pageParam: unknown }) => {
        if (Number(pageParam) === 1) {
          return {
            data: firstPageSkills,
            has_more: true,
            page: 1,
            total: 21,
          }
        }

        return new Promise((resolve) => {
          resolveNextPage = resolve
        })
      },
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))

    renderSkillsPage()

    const skillList = await screen.findByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    await screen.findByRole('heading', { name: 'Skill 1' })
    expect(within(skillList).getAllByRole('listitem')).toHaveLength(20)

    const scrollViewport = skillList.parentElement?.parentElement
    expect(scrollViewport).not.toBeNull()
    Object.defineProperties(scrollViewport!, {
      clientHeight: { configurable: true, value: 600 },
      scrollHeight: { configurable: true, value: 1200 },
      scrollTop: { configurable: true, value: 560 },
    })
    fireEvent.scroll(scrollViewport!)

    const paginationLoading = await within(skillList).findByRole('status')
    expect(paginationLoading).toHaveTextContent('common.loading')
    expect(paginationLoading).toBeVisible()
    expect(within(skillList).getAllByRole('listitem')).toHaveLength(20)

    resolveNextPage?.({
      data: [nextPageSkill],
      has_more: false,
      page: 2,
      total: 21,
    })
    expect(await screen.findByRole('heading', { name: 'Skill 21' })).toBeInTheDocument()
    expect(within(skillList).getAllByRole('listitem')).toHaveLength(21)
    expect(mocks.skillsQueryOptions.mock.lastCall?.[0].input(2)).toEqual({
      query: {
        limit: 20,
        page: 2,
      },
    })
  })

  it('waits for a cached first page to refetch before auto-loading the next page', async () => {
    const queryKey = ['skills-cached-refetch']
    const staleFirstPage = Array.from({ length: 20 }, (_, index) =>
      createSkill({
        id: `stale-skill-${index + 1}`,
        name: `stale-skill-${index + 1}`,
        display_name: `Stale Skill ${index + 1}`,
      }),
    )
    const freshFirstPage = staleFirstPage.map((skill, index) =>
      createSkill({
        ...skill,
        display_name: `Fresh Skill ${index + 1}`,
      }),
    )
    let resolveFirstPageRefetch:
      | ((page: { data: SkillResponse[]; has_more: boolean; page: number; total: number }) => void)
      | undefined
    let nextPageRequestCount = 0
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey,
      queryFn: ({ pageParam }: { pageParam: unknown }) => {
        if (Number(pageParam) === 1) {
          return new Promise((resolve) => {
            resolveFirstPageRefetch = resolve
          })
        }

        nextPageRequestCount += 1
        return Promise.resolve({
          data: [createSkill({ id: 'skill-21', name: 'skill-21', display_name: 'Fresh Skill 21' })],
          has_more: false,
          page: 2,
          total: 21,
        })
      },
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))
    const queryClient = createTestQueryClient()
    queryClient.setQueryData(queryKey, {
      pages: [
        {
          data: staleFirstPage,
          has_more: true,
          page: 1,
          total: 21,
        },
      ],
      pageParams: [1],
    })

    renderSkillsPage(queryClient)

    const skillRegion = screen.getByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    expect(await screen.findByRole('heading', { name: 'Stale Skill 1' })).toBeInTheDocument()
    const scrollViewport = skillRegion.parentElement?.parentElement
    expect(scrollViewport).not.toBeNull()
    Object.defineProperties(scrollViewport!, {
      clientHeight: { configurable: true, value: 600 },
      scrollHeight: { configurable: true, value: 1200 },
      scrollTop: { configurable: true, value: 560 },
    })
    fireEvent.scroll(scrollViewport!)

    expect(nextPageRequestCount).toBe(0)
    expect(screen.getByRole('heading', { name: 'Stale Skill 1' })).toBeInTheDocument()

    resolveFirstPageRefetch?.({
      data: freshFirstPage,
      has_more: true,
      page: 1,
      total: 21,
    })
    expect(await screen.findByRole('heading', { name: 'Fresh Skill 1' })).toBeInTheDocument()
    expect(await screen.findByRole('heading', { name: 'Fresh Skill 21' })).toBeInTheDocument()
    expect(nextPageRequestCount).toBe(1)
  })

  it('keeps loaded skills visible when the next page fails and exposes retry', async () => {
    const user = userEvent.setup()
    const firstPageSkills = Array.from({ length: 20 }, (_, index) =>
      createSkill({
        id: `skill-${index + 1}`,
        name: `skill-${index + 1}`,
        display_name: `Skill ${index + 1}`,
      }),
    )
    let nextPageRequestCount = 0
    let resolveRetry:
      | ((page: { data: SkillResponse[]; has_more: boolean; page: number; total: number }) => void)
      | undefined
    mocks.skillsQueryOptions.mockImplementation((options) => ({
      queryKey: ['skills-next-page-error', options],
      queryFn: async ({ pageParam }: { pageParam: unknown }) => {
        if (Number(pageParam) === 1) {
          return {
            data: firstPageSkills,
            has_more: true,
            page: 1,
            total: 21,
          }
        }

        nextPageRequestCount += 1
        if (nextPageRequestCount === 1) throw new Error('next skill page failed')

        return new Promise((resolve) => {
          resolveRetry = resolve
        })
      },
      getNextPageParam: options.getNextPageParam,
      initialPageParam: options.initialPageParam,
    }))

    renderSkillsPage()

    const skillRegion = await screen.findByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    await screen.findByRole('heading', { name: 'Skill 1' })
    const scrollViewport = skillRegion.parentElement?.parentElement
    expect(scrollViewport).not.toBeNull()
    Object.defineProperties(scrollViewport!, {
      clientHeight: { configurable: true, value: 600 },
      scrollHeight: { configurable: true, value: 600 },
      scrollTop: { configurable: true, value: 0 },
    })
    fireEvent.scroll(scrollViewport!)

    const paginationError = await within(skillRegion).findByRole('alert')
    expect(within(skillRegion).getAllByRole('listitem')).toHaveLength(20)
    expect(paginationError).toHaveTextContent('skill.skillManagement.loadingError')
    expect(nextPageRequestCount).toBe(1)

    const retryButton = within(paginationError).getByRole('button', {
      name: 'common.operation.retry',
    })
    await user.click(retryButton)
    await waitFor(() => {
      expect(nextPageRequestCount).toBe(2)
    })
    expect(retryButton).toHaveAttribute('aria-disabled', 'true')
    expect(retryButton).toHaveFocus()
    expect(within(skillRegion).queryByRole('status')).not.toBeInTheDocument()
    expect(within(skillRegion).getAllByRole('listitem')).toHaveLength(20)

    resolveRetry?.({
      data: [createSkill({ id: 'skill-21', name: 'skill-21', display_name: 'Recovered Skill 21' })],
      has_more: false,
      page: 2,
      total: 21,
    })
    expect(await screen.findByRole('heading', { name: 'Recovered Skill 21' })).toBeInTheDocument()
    expect(within(skillRegion).getAllByRole('listitem')).toHaveLength(21)
  })

  it('creates a placeholder skill and navigates to its detail page', async () => {
    const user = userEvent.setup()
    const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries')
    renderSkillsPage()

    await user.click(await screen.findByRole('button', { name: 'skill.skillManagement.create' }))

    await waitFor(() => {
      expect(mocks.createSkillMutationFn).toHaveBeenCalledWith(
        {
          body: {},
        },
        expect.anything(),
      )
    })
    expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.createSuccess')
    expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'query' }] })
    expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'infinite' }] })
    expect(invalidateQueries).toHaveBeenCalledWith({
      queryKey: ['skill-tags', { type: 'query' }],
    })
    expect(mocks.push).toHaveBeenCalledWith('/skills/created-skill')
    invalidateQueries.mockRestore()
  })

  it('explains when the workspace skill limit blocks draft creation', async () => {
    const user = userEvent.setup()
    mocks.createSkillMutationFn.mockRejectedValueOnce({ code: 'skill_limit_exceeded' })
    renderSkillsPage()

    await user.click(await screen.findByRole('button', { name: 'skill.skillManagement.create' }))

    await waitFor(() => {
      expect(toast.error).toHaveBeenCalledWith('skill.skillManagement.errors.workspaceLimit')
    })
  })

  it('imports a package file and navigates to the imported skill', async () => {
    const user = userEvent.setup()
    const invalidateQueries = vi.spyOn(QueryClient.prototype, 'invalidateQueries')
    const { container } = renderSkillsPage()

    const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')
    expect(fileInput).not.toBeNull()
    const file = new File(['skill'], 'refund.skill', { type: 'application/zip' })

    await user.upload(fileInput!, file)

    await waitFor(() => {
      expect(mocks.importSkillMutationFn).toHaveBeenCalledWith(
        {
          body: {
            file,
          },
        },
        expect.anything(),
      )
    })
    expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.importSuccess')
    expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'query' }] })
    expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['skills', { type: 'infinite' }] })
    expect(invalidateQueries).toHaveBeenCalledWith({
      queryKey: ['skill-tags', { type: 'query' }],
    })
    expect(mocks.push).toHaveBeenCalledWith('/skills/imported-skill')
    invalidateQueries.mockRestore()
  })

  it.each([
    {
      error: {
        data: {
          body: {
            code: 'skill_name_conflict',
            details: { name: 'refund-approval' },
          },
        },
      },
      message: 'skill.skillManagement.errors.nameConflict:{"name":"refund-approval"}',
    },
    {
      error: { code: 'skill_limit_exceeded' },
      message: 'skill.skillManagement.errors.workspaceLimit',
    },
    {
      error: { code: 'missing_skill_md' },
      message: 'skill.skillManagement.errors.missingSkillMd',
    },
    {
      error: { message: 'Skill package must contain SKILL.md' },
      message: 'skill.skillManagement.errors.missingSkillMd',
    },
    {
      error: { message: 'Skill name "refund-approval" already exists' },
      message: 'skill.skillManagement.errors.nameConflict:{"name":"refund-approval"}',
    },
  ])('explains import errors for $error.code', async ({ error, message }) => {
    const user = userEvent.setup()
    mocks.importSkillMutationFn.mockRejectedValueOnce(error)
    const { container } = renderSkillsPage()
    const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')
    const file = new File(['skill'], 'refund.skill', { type: 'application/zip' })

    await user.upload(fileInput!, file)

    await waitFor(() => {
      expect(toast.error).toHaveBeenCalledWith(message)
    })
  })

  it('explains an import error returned as a Response body', async () => {
    const user = userEvent.setup()
    mocks.importSkillMutationFn.mockRejectedValueOnce(
      new Response(JSON.stringify({ message: 'Skill package must contain SKILL.md' }), {
        status: 400,
      }),
    )
    const { container } = renderSkillsPage()
    const fileInput = container.querySelector<HTMLInputElement>('input[type="file"]')

    await user.upload(fileInput!, new File(['skill'], 'refund.skill', { type: 'application/zip' }))

    await waitFor(() => {
      expect(toast.error).toHaveBeenCalledWith('skill.skillManagement.errors.missingSkillMd')
    })
  })

  it('duplicates a skill from the card action menu', async () => {
    const user = userEvent.setup()
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )
    await user.click(await screen.findByText('common.operation.duplicate'))

    await waitFor(() => {
      expect(mocks.duplicateSkillMutationFn).toHaveBeenCalledWith(
        {
          params: {
            skill_id: 'skill-1',
          },
        },
        expect.anything(),
      )
    })
    expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.duplicateSuccess')
  })

  it('exports a published skill from the card action menu', async () => {
    const user = userEvent.setup()
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )
    await user.click(await screen.findByText('common.operation.export'))

    await waitFor(() => {
      expect(mocks.exportSkillArchiveBlob).toHaveBeenCalledWith('skill-1')
    })
    expect(mocks.downloadBlob).toHaveBeenCalledWith({
      data: expect.any(Blob),
      fileName: 'refund-approval.zip',
    })
  })

  it('does not show export for an unpublished skill', async () => {
    const user = userEvent.setup()
    mocks.skills = [createSkill({ latest_published_version_id: null })]
    mocks.skillPages = [mocks.skills]
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )

    expect(screen.queryByText('common.operation.export')).not.toBeInTheDocument()
  })

  it('confirms deletion with the skill name and refreshes list data', async () => {
    const user = userEvent.setup()
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )
    await user.click(await screen.findByText('common.operation.delete'))
    const dialog = await screen.findByRole('alertdialog')

    expect(
      within(dialog).getByText(
        'skill.skillManagement.deleteDialog.referencedDescription_other:{"count":2}',
      ),
    ).toBeInTheDocument()
    expect(await within(dialog).findByText('Support Agent')).toBeInTheDocument()
    expect(within(dialog).getByRole('link', { name: /Support Agent/ })).toHaveAttribute(
      'target',
      '_blank',
    )
    expect(within(dialog).getByTestId('skill-delete-reference-list')).toBeInTheDocument()

    await user.type(
      within(dialog).getByPlaceholderText(
        'skill.skillManagement.deleteDialog.confirmInputPlaceholder',
      ),
      'Refund approval',
    )
    await user.click(within(dialog).getByRole('button', { name: 'common.operation.delete' }))

    await waitFor(() => {
      expect(mocks.deleteSkillMutationFn).toHaveBeenCalledWith(
        {
          body: {
            confirmation_name: 'Refund approval',
          },
          params: {
            skill_id: 'skill-1',
          },
        },
        expect.anything(),
      )
    })
    expect(toast.success).toHaveBeenCalledWith('skill.skillManagement.deleteSuccess')
  })

  it('loads references in the delete confirmation when the list reference count is stale', async () => {
    const user = userEvent.setup()
    mocks.skills = [createSkill({ reference_count: 0 })]
    mocks.skillPages = [mocks.skills]
    mocks.skillReferences = [
      createAgentReference({
        agent_id: 'agent-stale-reference',
        display_name: 'Support Agent From References API',
        name: 'support-agent-from-references-api',
      }),
    ]
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )
    await user.click(await screen.findByText('common.operation.delete'))
    const dialog = await screen.findByRole('alertdialog')

    expect(await within(dialog).findByText('Support Agent From References API')).toBeInTheDocument()
    expect(
      within(dialog).getByText(
        'skill.skillManagement.deleteDialog.referencedDescription_one:{"count":1}',
      ),
    ).toBeInTheDocument()
    expect(mocks.skillReferencesQueryOptions).toHaveBeenCalledWith(
      expect.objectContaining({
        input: {
          params: {
            skill_id: 'skill-1',
          },
        },
      }),
    )
  })

  it('keeps deletion disabled while cached references refresh', async () => {
    const user = userEvent.setup()
    let referenceRequestCount = 0
    let shouldHangReferenceRequest = false
    mocks.skills = [createSkill({ reference_count: 0 })]
    mocks.skillPages = [mocks.skills]
    mocks.skillReferencesQueryOptions.mockImplementation((options) => ({
      queryKey: ['skill-references-pending', options],
      queryFn: () => {
        referenceRequestCount += 1
        if (!shouldHangReferenceRequest) return Promise.resolve({ data: [] })

        return new Promise(() => {})
      },
    }))
    renderSkillsPage()

    const moreButton = await screen.findByRole('button', {
      name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
    })
    await user.click(moreButton)
    await user.click(await screen.findByText('common.operation.delete'))
    let dialog = await screen.findByRole('alertdialog')

    await waitFor(() => {
      expect(within(dialog).getByRole('button', { name: 'common.operation.delete' })).toBeEnabled()
    })
    const initialRequestCount = referenceRequestCount
    shouldHangReferenceRequest = true
    await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
    await waitFor(() => {
      expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
    })

    await user.click(moreButton)
    await user.click(await screen.findByText('common.operation.delete'))
    dialog = await screen.findByRole('alertdialog')

    expect(
      within(dialog).getByRole('button', {
        name: 'common.operation.delete',
      }),
    ).toBeDisabled()
    await waitFor(() => {
      expect(referenceRequestCount).toBeGreaterThan(initialRequestCount)
    })
  })

  it('collapses long reference lists in the delete confirmation', async () => {
    const user = userEvent.setup()
    mocks.skillReferences = Array.from({ length: 7 }, (_, index) =>
      createAgentReference({
        agent_id: `agent-${index}`,
        display_name: `Support Agent ${index + 1}`,
        name: `support-agent-${index + 1}`,
      }),
    )
    renderSkillsPage()

    await user.click(
      await screen.findByRole('button', {
        name: 'skill.skillManagement.moreActions:{"name":"Refund approval"}',
      }),
    )
    await user.click(await screen.findByText('common.operation.delete'))
    const dialog = await screen.findByRole('alertdialog')

    expect(await within(dialog).findByText('Support Agent 5')).toBeInTheDocument()
    expect(within(dialog).queryByText('Support Agent 6')).not.toBeInTheDocument()
    expect(within(dialog).getByTestId('skill-delete-reference-list')).not.toHaveAttribute(
      'data-scrollable',
      'true',
    )

    await user.click(
      within(dialog).getByRole('button', {
        name: 'skill.skillManagement.detail.showMoreReferences:{"count":2}',
      }),
    )

    expect(within(dialog).getByText('Support Agent 6')).toBeInTheDocument()
    expect(within(dialog).getByText('Support Agent 7')).toBeInTheDocument()
    expect(within(dialog).getByTestId('skill-delete-reference-list')).toHaveAttribute(
      'data-scrollable',
      'true',
    )
    expect(within(dialog).getByTestId('skill-delete-reference-list')).toHaveClass(
      'max-h-[240px]',
      'overflow-y-auto',
    )
  })

  it('shows the empty-search state without create or import actions', async () => {
    mocks.queryState.keyword = 'missing'
    mocks.skills = []
    mocks.skillPages = [[]]

    renderSkillsPage()

    const skillRegion = screen.getByRole('region', {
      name: 'skill.skillManagement.listLabel',
    })
    const emptySearchTitle = await within(skillRegion).findByText(
      'skill.skillManagement.emptySearch',
    )
    expect(emptySearchTitle.closest('[role="status"]')).toBeInTheDocument()
    expect(
      screen.queryByText('skill.skillManagement.emptyAction.createTitle'),
    ).not.toBeInTheDocument()
    expect(
      screen.queryByText('skill.skillManagement.emptyAction.importTitle'),
    ).not.toBeInTheDocument()
  })
})
