import type { SnippetListItem } from '@/types/snippet'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import SnippetCard from '../snippet-card'

const {
  mockDeleteMutate,
  mockDownloadBlob,
  mockExportMutateAsync,
  mockOnRefresh,
  mockIsCurrentWorkspaceEditor,
  mockToastError,
  mockToastSuccess,
  mockUpdateMutate,
} = vi.hoisted(() => ({
  mockDeleteMutate: vi.fn(),
  mockDownloadBlob: vi.fn(),
  mockExportMutateAsync: vi.fn(),
  mockIsCurrentWorkspaceEditor: vi.fn(() => true),
  mockOnRefresh: vi.fn(),
  mockToastError: vi.fn(),
  mockToastSuccess: vi.fn(),
  mockUpdateMutate: vi.fn(),
}))

vi.mock('@/context/app-context', () => ({
  useAppContext: () => ({
    isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor(),
  }),
}))

vi.mock('@/service/use-common', () => ({
  useMembers: () => ({
    data: {
      accounts: [
        { id: 'creator-id', name: 'Creator', email: 'creator@example.com', avatar: '', avatar_url: null, role: 'editor', last_login_at: '', created_at: '', status: 'active' },
        { id: 'updater-id', name: 'Updater', email: 'updater@example.com', avatar: '', avatar_url: null, role: 'editor', last_login_at: '', created_at: '', status: 'active' },
      ],
    },
  }),
}))

vi.mock('@/service/use-snippets', () => ({
  useDeleteSnippetMutation: () => ({
    mutate: mockDeleteMutate,
    isPending: false,
  }),
  useExportSnippetMutation: () => ({
    mutateAsync: mockExportMutateAsync,
  }),
  useUpdateSnippetMutation: () => ({
    mutate: mockUpdateMutate,
    isPending: false,
  }),
}))

vi.mock('@/utils/time', () => ({
  formatTime: () => 'formatted-time',
}))

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

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

vi.mock('@/features/tag-management/components/tag-selector', () => ({
  TagSelector: ({
    onOpenTagManagement,
    onTagsChange,
    value,
  }: {
    onOpenTagManagement: () => void
    onTagsChange: () => void
    value: Array<{ name: string }>
  }) => (
    <div data-testid="snippet-tags">
      <span>{value.map(tag => tag.name).join(', ')}</span>
      <button type="button" onClick={onOpenTagManagement}>manage tags</button>
      <button type="button" onClick={onTagsChange}>sync tags</button>
    </div>
  ),
}))

const createSnippet = (overrides: Partial<SnippetListItem> = {}): SnippetListItem => ({
  id: 'snippet-1',
  name: 'Tone Rewriter',
  description: 'Rewrites rough drafts.',
  type: 'node',
  is_published: true,
  use_count: 19,
  tags: [],
  created_at: 1_704_067_200,
  created_by: 'creator-id',
  updated_at: 1_704_153_600,
  updated_by: 'updater-id',
  ...overrides,
})

describe('SnippetCard', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockIsCurrentWorkspaceEditor.mockReturnValue(true)
  })

  describe('Rendering', () => {
    it('should render updater name and updated time from member data', () => {
      render(<SnippetCard snippet={createSnippet()} />)

      expect(screen.getByText('Tone Rewriter')).toBeInTheDocument()
      expect(screen.getByText('Updater')).toBeInTheDocument()
      expect(screen.getByText('formatted-time')).toBeInTheDocument()
      expect(screen.queryByText('snippet.usageCount:{"count":19}')).not.toBeInTheDocument()
      expect(screen.queryByText('Creator')).not.toBeInTheDocument()
      expect(screen.queryByRole('img')).not.toBeInTheDocument()
    })

    it('should fall back to creator name when updater is unavailable', () => {
      render(<SnippetCard snippet={createSnippet({ updated_by: 'missing-user' })} />)

      expect(screen.getByText('Creator')).toBeInTheDocument()
      expect(screen.getByText('formatted-time')).toBeInTheDocument()
    })

    it('should fall back to unknown user when creator and updater are unavailable', () => {
      render(<SnippetCard snippet={createSnippet({ created_by: 'missing-creator', updated_by: 'missing-updater' })} />)

      expect(screen.getByText('snippet.unknownUser')).toBeInTheDocument()
    })

    it('should not render draft status for unpublished snippets', () => {
      render(<SnippetCard snippet={createSnippet({ is_published: false })} />)

      expect(screen.queryByText('snippet.draft')).not.toBeInTheDocument()
    })

    it('should render supported operations only', async () => {
      render(<SnippetCard snippet={createSnippet()} />)

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

      expect(await screen.findByRole('menuitem', { name: 'snippet.menu.editInfo' })).toBeInTheDocument()
      expect(screen.getByRole('menuitem', { name: 'snippet.menu.exportSnippet' })).toBeInTheDocument()
      expect(screen.getByRole('menuitem', { name: 'snippet.menu.deleteSnippet' })).toBeInTheDocument()
      expect(screen.queryByRole('menuitem', { name: /duplicate/i })).not.toBeInTheDocument()
    })

    it('should hide operations for users without editor permission', () => {
      mockIsCurrentWorkspaceEditor.mockReturnValue(false)

      render(<SnippetCard snippet={createSnippet()} />)

      expect(screen.queryByRole('button', { name: 'common.operation.more' })).not.toBeInTheDocument()
    })

    it('should forward tag selector actions without navigating the card link', () => {
      const onOpenTagManagement = vi.fn()
      const onTagsChange = vi.fn()

      render(
        <SnippetCard
          snippet={createSnippet({ tags: [{ id: 'tag-1', name: 'Sales', type: 'snippet', binding_count: 1 }] })}
          onOpenTagManagement={onOpenTagManagement}
          onTagsChange={onTagsChange}
        />,
      )

      fireEvent.click(screen.getByRole('button', { name: 'manage tags' }))
      fireEvent.click(screen.getByRole('button', { name: 'sync tags' }))

      expect(screen.getByText('Sales')).toBeInTheDocument()
      expect(onOpenTagManagement).toHaveBeenCalledTimes(1)
      expect(onTagsChange).toHaveBeenCalledTimes(1)
    })

    it('should export a snippet from the operations menu', async () => {
      mockExportMutateAsync.mockResolvedValue('snippet-yaml')

      render(<SnippetCard snippet={createSnippet()} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.exportSnippet' }))

      await waitFor(() => {
        expect(mockExportMutateAsync).toHaveBeenCalledWith({ snippetId: 'snippet-1' })
        expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
          fileName: 'Tone Rewriter.yml',
        }))
      })
    })

    it('should show an error toast when snippet export fails', async () => {
      mockExportMutateAsync.mockRejectedValue(new Error('export failed'))

      render(<SnippetCard snippet={createSnippet()} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.exportSnippet' }))

      await waitFor(() => {
        expect(mockToastError).toHaveBeenCalledWith('snippet.exportFailed')
      })
      expect(mockDownloadBlob).not.toHaveBeenCalled()
    })

    it('should update snippet info from the operations menu', async () => {
      mockUpdateMutate.mockImplementation((_payload, options?: { onSuccess?: () => void }) => {
        options?.onSuccess?.()
      })

      render(<SnippetCard snippet={createSnippet()} onRefresh={mockOnRefresh} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.editInfo' }))
      fireEvent.change(screen.getByPlaceholderText('workflow.snippet.namePlaceholder'), {
        target: { value: 'Updated Snippet' },
      })
      fireEvent.click(screen.getByRole('button', { name: /common\.operation\.save/i }))

      await waitFor(() => {
        expect(mockUpdateMutate).toHaveBeenCalledWith({
          params: { snippetId: 'snippet-1' },
          body: {
            name: 'Updated Snippet',
            description: 'Rewrites rough drafts.',
          },
        }, expect.objectContaining({
          onSuccess: expect.any(Function),
          onError: expect.any(Function),
        }))
        expect(mockOnRefresh).toHaveBeenCalled()
      })
    })

    it('should show update errors from the mutation callback', async () => {
      mockUpdateMutate.mockImplementation((_payload, options?: { onError?: (error: Error) => void }) => {
        options?.onError?.(new Error('Update failed'))
      })

      render(<SnippetCard snippet={createSnippet({ description: '' })} onRefresh={mockOnRefresh} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.editInfo' }))
      fireEvent.change(screen.getByPlaceholderText('workflow.snippet.namePlaceholder'), {
        target: { value: 'Updated Snippet' },
      })
      fireEvent.click(screen.getByRole('button', { name: /common\.operation\.save/i }))

      await waitFor(() => {
        expect(mockUpdateMutate).toHaveBeenCalledWith(expect.objectContaining({
          body: {
            name: 'Updated Snippet',
            description: undefined,
          },
        }), expect.any(Object))
        expect(mockToastError).toHaveBeenCalledWith('Update failed')
      })
      expect(mockOnRefresh).not.toHaveBeenCalled()
    })

    it('should delete a snippet from the operations menu', async () => {
      mockDeleteMutate.mockImplementation((_payload, options?: { onSuccess?: () => void }) => {
        options?.onSuccess?.()
      })

      render(<SnippetCard snippet={createSnippet()} onRefresh={mockOnRefresh} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.deleteSnippet' }))
      fireEvent.click(screen.getByRole('button', { name: 'snippet.menu.deleteSnippet' }))

      await waitFor(() => {
        expect(mockDeleteMutate).toHaveBeenCalledWith({
          params: { snippetId: 'snippet-1' },
        }, expect.objectContaining({
          onSuccess: expect.any(Function),
          onError: expect.any(Function),
        }))
        expect(mockOnRefresh).toHaveBeenCalled()
      })
    })

    it('should show delete errors from the mutation callback', async () => {
      mockDeleteMutate.mockImplementation((_payload, options?: { onError?: (error: Error) => void }) => {
        options?.onError?.(new Error('Delete failed'))
      })

      render(<SnippetCard snippet={createSnippet()} onRefresh={mockOnRefresh} />)

      fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
      fireEvent.click(await screen.findByRole('menuitem', { name: 'snippet.menu.deleteSnippet' }))
      fireEvent.click(screen.getByRole('button', { name: 'snippet.menu.deleteSnippet' }))

      await waitFor(() => {
        expect(mockToastError).toHaveBeenCalledWith('Delete failed')
      })
      expect(mockOnRefresh).not.toHaveBeenCalled()
    })
  })
})
