import type { MockedFunction } from 'vitest'
import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import {
  ChunkingMode,
  DatasetPermission,
  DataSourceType,
  RerankingModeEnum,
} from '@/models/datasets'
import { updateDatasetSetting } from '@/service/datasets'
import { useMembers } from '@/service/use-common'
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
import { createSystemFeaturesFixture } from '@/test/console/system-features'
import { RETRIEVE_METHOD } from '@/types/app'
import { DatasetACLPermission } from '@/utils/permission'
import SettingsModal from '../index'

const toastMocks = vi.hoisted(() => ({
  call: vi.fn(),
  dismiss: vi.fn(),
  update: vi.fn(),
  promise: vi.fn(),
}))

vi.mock('@langgenius/dify-ui/toast', () => ({
  toast: Object.assign(toastMocks.call, {
    success: vi.fn((message: string, options?: Record<string, unknown>) =>
      toastMocks.call({ type: 'success', message, ...options }),
    ),
    error: vi.fn((message: string, options?: Record<string, unknown>) =>
      toastMocks.call({ type: 'error', message, ...options }),
    ),
    warning: vi.fn((message: string, options?: Record<string, unknown>) =>
      toastMocks.call({ type: 'warning', message, ...options }),
    ),
    info: vi.fn((message: string, options?: Record<string, unknown>) =>
      toastMocks.call({ type: 'info', message, ...options }),
    ),
    dismiss: toastMocks.dismiss,
    update: toastMocks.update,
    promise: toastMocks.promise,
  }),
}))
const mockOnCancel = vi.fn()
const mockOnSave = vi.fn()
const mockSetShowAccountSettingModal = vi.fn()

const mockUseModelList = vi.fn()
const mockUseModelListAndDefaultModel = vi.fn()
const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.fn()
const mockUseCurrentProviderAndModel = vi.fn()
const mockCheckShowMultiModalTip = vi.fn()

vi.mock('ky', () => {
  const ky = () => ky
  ky.extend = () => ky
  ky.create = () => ky
  return { __esModule: true, default: ky }
})

vi.mock('@/app/components/datasets/create/step-two', () => ({
  IndexingType: {
    QUALIFIED: 'high_quality',
    ECONOMICAL: 'economy',
  },
}))

vi.mock('@/service/datasets', () => ({
  updateDatasetSetting: vi.fn(),
}))

vi.mock('@/service/use-common', async () => ({
  ...(await vi.importActual('@/service/use-common')),
  useMembers: vi.fn(),
}))

vi.mock('@/context/modal-context', () => ({
  useModalContext: () => ({
    setShowAccountSettingModal: mockSetShowAccountSettingModal,
  }),
}))

vi.mock('@/context/i18n', () => ({
  useDocLink: () => (path: string) => `https://docs${path}`,
}))

vi.mock('@/context/provider-context', () => ({
  useProviderContext: () => ({
    modelProviders: [],
    textGenerationModelList: [],
    supportRetrievalMethods: [
      RETRIEVE_METHOD.semantic,
      RETRIEVE_METHOD.fullText,
      RETRIEVE_METHOD.hybrid,
      RETRIEVE_METHOD.keywordSearch,
    ],
  }),
}))

vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
  useModelList: (...args: unknown[]) => mockUseModelList(...args),
  useModelListAndDefaultModel: (...args: unknown[]) => mockUseModelListAndDefaultModel(...args),
  useModelListAndDefaultModelAndCurrentProviderAndModel: (...args: unknown[]) =>
    mockUseModelListAndDefaultModelAndCurrentProviderAndModel(...args),
  useCurrentProviderAndModel: (...args: unknown[]) => mockUseCurrentProviderAndModel(...args),
}))

vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
  default: ({ defaultModel }: { defaultModel?: { provider: string; model: string } }) => (
    <div data-testid="model-selector">
      {defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : 'no-model'}
    </div>
  ),
}))

vi.mock('@/app/components/datasets/settings/utils', () => ({
  checkShowMultiModalTip: (...args: unknown[]) => mockCheckShowMultiModalTip(...args),
}))

const mockUpdateDatasetSetting = updateDatasetSetting as MockedFunction<typeof updateDatasetSetting>
const mockUseMembers = useMembers as MockedFunction<typeof useMembers>

const createRetrievalConfig = (overrides: Partial<RetrievalConfig> = {}): RetrievalConfig => ({
  search_method: RETRIEVE_METHOD.semantic,
  reranking_enable: false,
  reranking_model: {
    reranking_provider_name: '',
    reranking_model_name: '',
  },
  top_k: 2,
  score_threshold_enabled: false,
  score_threshold: 0.5,
  reranking_mode: RerankingModeEnum.RerankingModel,
  ...overrides,
})

const createDataset = (
  overrides: Partial<DataSet> = {},
  retrievalOverrides: Partial<RetrievalConfig> = {},
): DataSet => {
  const retrievalConfig = createRetrievalConfig(retrievalOverrides)
  return {
    id: 'dataset-id',
    name: 'Test Dataset',
    indexing_status: 'completed',
    icon_info: {
      icon: 'icon',
      icon_type: 'emoji',
    },
    description: 'Description',
    permission: DatasetPermission.allTeamMembers,
    data_source_type: DataSourceType.FILE,
    indexing_technique: IndexingType.QUALIFIED,
    author_name: 'Author',
    created_by: 'creator',
    updated_by: 'updater',
    updated_at: 1700000000,
    app_count: 0,
    doc_form: ChunkingMode.text,
    document_count: 0,
    total_document_count: 0,
    total_available_documents: 0,
    word_count: 0,
    provider: 'internal',
    embedding_model: 'embed-model',
    embedding_model_provider: 'embed-provider',
    embedding_available: true,
    tags: [],
    partial_member_list: [],
    external_knowledge_info: {
      external_knowledge_id: 'ext-id',
      external_knowledge_api_id: 'ext-api-id',
      external_knowledge_api_name: 'External API',
      external_knowledge_api_endpoint: 'https://api.example.com',
    },
    external_retrieval_model: {
      top_k: 2,
      score_threshold: 0.5,
      score_threshold_enabled: false,
    },
    built_in_field_enabled: false,
    doc_metadata: [],
    keyword_number: 10,
    pipeline_id: 'pipeline-id',
    is_published: false,
    runtime_mode: 'general',
    enable_api: true,
    is_multimodal: false,
    permission_keys: [DatasetACLPermission.Edit],
    ...overrides,
    retrieval_model_dict: {
      ...retrievalConfig,
      ...overrides.retrieval_model_dict,
    },
    retrieval_model: {
      ...retrievalConfig,
      ...overrides.retrieval_model,
    },
  }
}

const renderWithProviders = (dataset: DataSet) => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })
  queryClient.setQueryData(systemFeaturesQueryOptions().queryKey, createSystemFeaturesFixture())

  return render(
    <QueryClientProvider client={queryClient}>
      <SettingsModal currentDataset={dataset} onCancel={mockOnCancel} onSave={mockOnSave} />
    </QueryClientProvider>,
  )
}

const createMemberList = (): DataSet['partial_member_list'] => ['member-2']

const renderSettingsModal = async (dataset: DataSet) => {
  renderWithProviders(dataset)
  await waitFor(() => expect(mockUseMembers).toHaveBeenCalled())
}

describe('SettingsModal', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockUseMembers.mockReturnValue({
      data: {
        accounts: [
          {
            id: 'user-1',
            name: 'User One',
            email: 'user@example.com',
            avatar: 'avatar.png',
            avatar_url: 'avatar.png',
            status: 'active',
            role: 'owner',
          },
          {
            id: 'member-2',
            name: 'Member Two',
            email: 'member@example.com',
            avatar: 'avatar.png',
            avatar_url: 'avatar.png',
            status: 'active',
            role: 'editor',
          },
        ],
      },
    } as ReturnType<typeof useMembers>)
    mockUseModelList.mockImplementation((type: ModelTypeEnum) => {
      if (type === ModelTypeEnum.rerank) {
        return {
          data: [
            {
              provider: 'rerank-provider',
              models: [{ model: 'rerank-model' }],
            },
          ],
        }
      }
      return { data: [{ provider: 'embed-provider', models: [{ model: 'embed-model' }] }] }
    })
    mockUseModelListAndDefaultModel.mockReturnValue({ modelList: [], defaultModel: null })
    mockUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
      defaultModel: null,
      currentModel: null,
    })
    mockUseCurrentProviderAndModel.mockReturnValue({ currentProvider: null, currentModel: null })
    mockCheckShowMultiModalTip.mockReturnValue(false)
    mockUpdateDatasetSetting.mockResolvedValue(createDataset())
  })

  // Rendering and basic field bindings.
  describe('Rendering', () => {
    it('should render dataset details when dataset is provided', async () => {
      // Arrange
      const dataset = createDataset()

      // Act
      await renderSettingsModal(dataset)

      // Assert
      expect(screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')).toHaveValue(
        'Test Dataset',
      )
      expect(screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')).toHaveValue(
        'Description',
      )
    })

    it('should show external knowledge info when dataset is external', async () => {
      // Arrange
      const dataset = createDataset({
        provider: 'external',
        external_knowledge_info: {
          external_knowledge_id: 'ext-id-123',
          external_knowledge_api_id: 'ext-api-id-123',
          external_knowledge_api_name: 'External Knowledge API',
          external_knowledge_api_endpoint: 'https://api.external.com',
        },
      })

      // Act
      await renderSettingsModal(dataset)

      // Assert
      expect(screen.getByText('External Knowledge API')).toBeInTheDocument()
      expect(screen.getByText('https://api.external.com')).toBeInTheDocument()
      expect(screen.getByText('ext-id-123')).toBeInTheDocument()
    })
  })

  // User interactions that update visible state.
  describe('Interactions', () => {
    it('should call onCancel when cancel button is clicked', async () => {
      // Arrange
      const user = userEvent.setup()

      // Act
      await renderSettingsModal(createDataset())
      await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))

      // Assert
      expect(mockOnCancel).toHaveBeenCalledTimes(1)
    })

    it('should update name input when user types', async () => {
      // Arrange
      const user = userEvent.setup()
      await renderSettingsModal(createDataset())

      const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')

      // Act
      await user.clear(nameInput)
      await user.type(nameInput, 'New Dataset Name')

      // Assert
      expect(nameInput).toHaveValue('New Dataset Name')
    })

    it('should update description input when user types', async () => {
      // Arrange
      const user = userEvent.setup()
      await renderSettingsModal(createDataset())

      const descriptionInput = screen.getByPlaceholderText('datasetSettings.form.descPlaceholder')

      // Act
      await user.clear(descriptionInput)
      await user.type(descriptionInput, 'New description')

      // Assert
      expect(descriptionInput).toHaveValue('New description')
    })

    it('should show and dismiss retrieval change tip when indexing method changes', async () => {
      // Arrange
      const user = userEvent.setup()
      const dataset = createDataset({ indexing_technique: IndexingType.ECONOMICAL })

      // Act
      await renderSettingsModal(dataset)
      await user.click(screen.getByText('datasetCreation.stepTwo.qualified'))

      // Assert
      expect(
        await screen.findByText('appDebug.datasetConfig.retrieveChangeTip'),
      ).toBeInTheDocument()

      // Act
      await user.click(screen.getByLabelText('close-retrieval-change-tip'))

      // Assert
      await waitFor(() => {
        expect(
          screen.queryByText('appDebug.datasetConfig.retrieveChangeTip'),
        ).not.toBeInTheDocument()
      })
    })

    it('should open account setting modal when embedding model tip is clicked', async () => {
      // Arrange
      const user = userEvent.setup()

      // Act
      await renderSettingsModal(createDataset())
      await user.click(
        screen.getByRole('button', { name: 'datasetSettings.form.embeddingModelTipLink' }),
      )

      // Assert
      expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
        payload: ACCOUNT_SETTING_TAB.PROVIDER,
      })
    })
  })

  // Dataset ACL permissions control whether the legacy dataset permission selector is editable.
  describe('Permission Handling', () => {
    it('should disable permission selector when dataset lacks edit ACL permission', async () => {
      const dataset = createDataset({ permission_keys: [DatasetACLPermission.Readonly] })

      const { container } = renderWithProviders(dataset)
      await waitFor(() => expect(mockUseMembers).toHaveBeenCalled())

      expect(container.querySelector('[class*="cursor-not-allowed"]')).toBeInTheDocument()
    })
  })

  // Validation guardrails before saving.
  describe('Validation', () => {
    it('should block save when dataset name is empty', async () => {
      // Arrange
      const user = userEvent.setup()
      await renderSettingsModal(createDataset())

      const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')

      // Act
      await user.clear(nameInput)
      await user.click(screen.getByRole('button', { name: 'common.operation.save' }))

      // Assert
      expect(toastMocks.call).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          message: 'datasetSettings.form.nameError',
        }),
      )
      expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
    })

    it('should block save when reranking is enabled without model', async () => {
      // Arrange
      const user = userEvent.setup()
      mockUseModelList.mockReturnValue({ data: [] })
      const dataset = createDataset(
        {},
        createRetrievalConfig({
          reranking_enable: true,
          reranking_model: {
            reranking_provider_name: '',
            reranking_model_name: '',
          },
        }),
      )

      // Act
      await renderSettingsModal(dataset)
      await user.click(screen.getByRole('button', { name: 'common.operation.save' }))

      // Assert
      expect(toastMocks.call).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          message: 'appDebug.datasetConfig.rerankModelRequired',
        }),
      )
      expect(mockUpdateDatasetSetting).not.toHaveBeenCalled()
    })
  })

  // Save flows and side effects.
  describe('Save', () => {
    it('should save internal dataset changes when form is valid', async () => {
      // Arrange
      const user = userEvent.setup()
      const rerankRetrieval = createRetrievalConfig({
        reranking_enable: true,
        reranking_model: {
          reranking_provider_name: 'rerank-provider',
          reranking_model_name: 'rerank-model',
        },
      })
      const dataset = createDataset({
        retrieval_model: rerankRetrieval,
        retrieval_model_dict: rerankRetrieval,
      })

      // Act
      await renderSettingsModal(dataset)

      const nameInput = screen.getByPlaceholderText('datasetSettings.form.namePlaceholder')
      await user.clear(nameInput)
      await user.type(nameInput, 'Updated Internal Dataset')
      await user.click(screen.getByRole('button', { name: 'common.operation.save' }))

      // Assert
      await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())

      expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(
        expect.objectContaining({
          body: expect.objectContaining({
            name: 'Updated Internal Dataset',
            permission: DatasetPermission.allTeamMembers,
          }),
        }),
      )
      expect(toastMocks.call).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'success',
          message: 'common.actionMsg.modifiedSuccessfully',
        }),
      )
      expect(mockOnSave).toHaveBeenCalledWith(
        expect.objectContaining({
          name: 'Updated Internal Dataset',
          retrieval_model_dict: expect.objectContaining({
            reranking_enable: true,
          }),
        }),
      )
    })

    it('should save external dataset changes when partial members configured', async () => {
      // Arrange
      const user = userEvent.setup()
      const dataset = createDataset(
        {
          provider: 'external',
          permission: DatasetPermission.partialMembers,
          partial_member_list: createMemberList(),
          external_retrieval_model: {
            top_k: 5,
            score_threshold: 0.3,
            score_threshold_enabled: true,
          },
        },
        {
          score_threshold_enabled: true,
          score_threshold: 0.8,
        },
      )

      // Act
      await renderSettingsModal(dataset)
      await user.click(screen.getByRole('button', { name: 'common.operation.save' }))

      // Assert
      await waitFor(() => expect(mockUpdateDatasetSetting).toHaveBeenCalled())

      expect(mockUpdateDatasetSetting).toHaveBeenCalledWith(
        expect.objectContaining({
          body: expect.objectContaining({
            permission: DatasetPermission.partialMembers,
            external_retrieval_model: expect.objectContaining({
              top_k: 5,
            }),
            partial_member_list: [
              {
                user_id: 'member-2',
                role: 'editor',
              },
            ],
          }),
        }),
      )
      expect(mockOnSave).toHaveBeenCalledWith(
        expect.objectContaining({
          retrieval_model_dict: expect.objectContaining({
            score_threshold_enabled: true,
            score_threshold: 0.8,
          }),
        }),
      )
    })

    it('should disable save button while saving', async () => {
      // Arrange
      const user = userEvent.setup()
      mockUpdateDatasetSetting.mockImplementation(
        () => new Promise((resolve) => setTimeout(resolve, 100)),
      )

      // Act
      await renderSettingsModal(createDataset())

      const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
      await user.click(saveButton)

      // Assert
      expect(saveButton).toBeDisabled()
    })

    it('should show error toast when save fails', async () => {
      // Arrange
      const user = userEvent.setup()
      mockUpdateDatasetSetting.mockRejectedValue(new Error('API Error'))

      // Act
      await renderSettingsModal(createDataset())
      await user.click(screen.getByRole('button', { name: 'common.operation.save' }))

      // Assert
      await waitFor(() => {
        expect(toastMocks.call).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
      })
    })
  })
})
