import type { ChatWrapperRefType } from '../index'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { ConversationVariable } from '@/app/components/workflow/types'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import { useStore as useAppStore } from '@/app/components/app/store'
import { createStartNode } from '@/app/components/workflow/__tests__/fixtures'
import {
  renderWorkflowComponent,
  renderWorkflowFlowComponent,
} from '@/app/components/workflow/__tests__/workflow-test-env'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import { InputVarType } from '@/app/components/workflow/types'
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
import { fetchSuggestedQuestions, stopChatMessageResponding } from '@/service/debug'
import { fetchCurrentValueOfConversationVariable } from '@/service/workflow'
import ChatWrapper from '../chat-wrapper'
import ConversationVariableModal from '../conversation-variable-modal'
import UserInput from '../user-input'

const mockUseChat = vi.fn()
const mockChatRender = vi.fn()
const mockUseSubscription = vi.fn()

vi.mock('copy-to-clipboard', () => ({
  default: vi.fn(),
}))

vi.mock('@/service/debug', () => ({
  fetchSuggestedQuestions: vi.fn(),
  stopChatMessageResponding: vi.fn(),
}))

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

vi.mock('@/hooks/use-timestamp', () => ({
  default: () => ({
    formatTime: (timestamp: number) => `formatted-${timestamp}`,
  }),
}))

vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
  default: ({ value }: { value?: string }) => <pre data-testid="conversation-code-editor">{value}</pre>,
}))

vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/form-item', () => ({
  default: ({
    payload,
    value,
    onChange,
  }: {
    payload: { label?: string, variable: string }
    value?: string
    onChange: (value: string) => void
  }) => (
    <input
      aria-label={payload.label || payload.variable}
      value={value ?? ''}
      onChange={e => onChange(e.target.value)}
    />
  ),
}))

vi.mock('@/app/components/base/chat/chat', () => ({
  default: ({
    chatNode,
    inputDisabled,
    onSend,
    onRegenerate,
    switchSibling,
    onHumanInputFormSubmit,
    onFeatureBarClick,
  }: {
    chatNode: React.ReactNode
    inputDisabled?: boolean
    onSend?: (message: string, files: unknown[]) => void
    onRegenerate?: (chatItem: { id: string, parentMessageId?: string, content?: string, message_files?: unknown[] }) => void
    switchSibling?: (siblingMessageId: string) => void
    onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
    onFeatureBarClick?: (state: boolean) => void
  }) => {
    mockChatRender({
      inputDisabled,
      hasChatNode: !!chatNode,
    })
    return (
      <div data-testid="chat-shell">
        <div data-testid="chat-input-disabled">{`${inputDisabled}`}</div>
        <button type="button" onClick={() => onSend?.('hello', [])}>send-chat</button>
        <button
          type="button"
          onClick={() => onRegenerate?.({
            id: 'answer-2',
            parentMessageId: 'question-1',
            content: 'latest answer',
            message_files: [],
          })}
        >
          regenerate-chat
        </button>
        <button type="button" onClick={() => switchSibling?.('sibling-2')}>switch-sibling</button>
        <button type="button" onClick={() => onHumanInputFormSubmit?.('token-1', { inputs: { answer: 'ok' }, action: 'approve' })}>submit-human-input</button>
        <button type="button" onClick={() => onFeatureBarClick?.(true)}>open-feature-panel</button>
        {chatNode}
      </div>
    )
  },
}))

vi.mock('../hooks', () => ({
  useChat: (...args: unknown[]) => mockUseChat(...args),
}))

vi.mock('@/app/components/base/features/hooks', () => ({
  useFeatures: <T,>(selector: (state: {
    features: {
      opening?: { enabled?: boolean, opening_statement?: string, suggested_questions?: string[] }
      suggested: boolean
      text2speech: boolean
      speech2text: boolean
      citation: boolean
      moderation: boolean
      file: { enabled: boolean }
    }
  }) => T) => selector({
    features: {
      opening: { enabled: false, opening_statement: '', suggested_questions: [] },
      suggested: false,
      text2speech: false,
      speech2text: false,
      citation: false,
      moderation: false,
      file: { enabled: false },
    },
  }),
}))

vi.mock('@/context/event-emitter', () => ({
  useEventEmitterContextContext: () => ({
    eventEmitter: {
      useSubscription: mockUseSubscription,
    },
  }),
}))

const mockFetchCurrentValueOfConversationVariable = vi.mocked(fetchCurrentValueOfConversationVariable)
const mockCopy = vi.mocked(copy)
const mockFetchSuggestedQuestions = vi.mocked(fetchSuggestedQuestions)
const mockStopChatMessageResponding = vi.mocked(stopChatMessageResponding)

const createConversationVariable = (
  overrides: Partial<ConversationVariable> = {},
): ConversationVariable => ({
  id: 'var-1',
  name: 'session_state',
  description: 'Session state',
  value_type: ChatVarType.Object,
  value: '{"draft":true}',
  ...overrides,
})

const createChatState = (overrides: Record<string, unknown> = {}) => ({
  conversationId: 'conversation-1',
  chatList: [],
  handleStop: vi.fn(),
  isResponding: false,
  suggestedQuestions: [],
  handleSend: vi.fn(),
  handleRestart: vi.fn(),
  handleSwitchSibling: vi.fn(),
  handleSubmitHumanInputForm: vi.fn(),
  getHumanInputNodeData: vi.fn(),
  ...overrides,
})

const createConversationVariableResponse = (
  data: Array<Awaited<ReturnType<typeof fetchCurrentValueOfConversationVariable>>['data'][number]> = [],
): Awaited<ReturnType<typeof fetchCurrentValueOfConversationVariable>> => ({
  data,
  has_more: false,
  limit: 20,
  total: data.length,
  page: 1,
})

const createChatWrapperRef = () => ({ current: null }) as unknown as React.RefObject<ChatWrapperRefType>

describe('debug-and-preview components', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    useAppStore.setState({
      appDetail: {
        id: 'app-1',
        site: {
          access_token: 'site-token',
          app_base_url: 'https://example.com',
        },
      } as ReturnType<typeof useAppStore.getState>['appDetail'],
    })
    mockUseChat.mockReturnValue(createChatState())
    mockFetchCurrentValueOfConversationVariable.mockResolvedValue(createConversationVariableResponse())
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  describe('ConversationVariableModal', () => {
    it('should load latest values, switch variable tabs, and close the modal', async () => {
      const user = userEvent.setup()
      const onHide = vi.fn()
      mockFetchCurrentValueOfConversationVariable.mockResolvedValue(createConversationVariableResponse([
        {
          ...createConversationVariable({
            id: 'var-1',
            value: '{"latest":1}',
          }),
          updated_at: 100,
          created_at: 50,
        },
        {
          ...createConversationVariable({
            id: 'var-2',
            name: 'summary',
            value_type: ChatVarType.String,
            value: 'latest text',
          }),
          updated_at: 200,
          created_at: 150,
        },
      ]))

      renderWorkflowComponent(
        <ConversationVariableModal
          conversationID="conversation-1"
          onHide={onHide}
        />,
        {
          initialStoreState: {
            appId: 'app-1',
            conversationVariables: [
              createConversationVariable(),
              createConversationVariable({
                id: 'var-2',
                name: 'summary',
                value_type: ChatVarType.String,
                value: 'plain text',
              }),
            ],
          },
        },
      )

      await waitFor(() => {
        expect(mockFetchCurrentValueOfConversationVariable).toHaveBeenCalledWith({
          url: '/apps/app-1/conversation-variables',
          params: { conversation_id: 'conversation-1' },
        })
      })

      expect(screen.getAllByText('session_state')).toHaveLength(2)
      expect(screen.getByText(content => content.includes('formatted-100'))).toBeInTheDocument()
      expect(screen.getByTestId('conversation-code-editor')).toHaveTextContent('{"latest":1}')

      const closeTrigger = document.querySelector('.absolute.right-4.top-4.cursor-pointer') as HTMLElement

      await user.click(screen.getByText('summary'))
      expect(screen.getByText('latest text')).toBeInTheDocument()

      await user.click(closeTrigger)
      expect(onHide).toHaveBeenCalledTimes(1)
    })

    it('should copy the current variable value and reset the copied state after the timeout', async () => {
      vi.useFakeTimers()
      renderWorkflowComponent(
        <ConversationVariableModal
          conversationID="conversation-1"
          onHide={vi.fn()}
        />,
        {
          initialStoreState: {
            appId: 'app-1',
            conversationVariables: [
              createConversationVariable(),
            ],
          },
        },
      )

      const copyTrigger = document.querySelector('.flex.items-center.p-1 svg.cursor-pointer') as HTMLElement
      act(() => {
        fireEvent.click(copyTrigger)
      })
      expect(mockCopy).toHaveBeenCalledWith('{"draft":true}')

      act(() => {
        vi.advanceTimersByTime(2000)
      })
    })
  })

  describe('UserInput', () => {
    it('should hide secret fields outside the expanded panel and persist edits into workflow state', async () => {
      const user = userEvent.setup()
      const { store } = renderWorkflowFlowComponent(
        <UserInput />,
        {
          nodes: [
            createStartNode({
              data: {
                variables: [
                  {
                    type: InputVarType.textInput,
                    variable: 'question',
                    label: 'Question',
                  },
                  {
                    type: InputVarType.textInput,
                    variable: 'internal_note',
                    label: 'Internal Note',
                    hide: true,
                  },
                ],
              },
            }),
          ],
          edges: [],
          initialStoreState: {
            inputs: {
              question: 'draft',
            },
            showDebugAndPreviewPanel: false,
          },
        },
      )

      expect(screen.getByLabelText('Question')).toBeInTheDocument()
      expect(screen.queryByLabelText('Internal Note')).not.toBeInTheDocument()

      await user.clear(screen.getByLabelText('Question'))
      await user.type(screen.getByLabelText('Question'), 'updated draft')

      expect(store.getState().inputs).toEqual({
        question: 'updated draft',
      })
    })

    it('should reveal hidden fields when the debug-and-preview panel is expanded', () => {
      renderWorkflowFlowComponent(
        <UserInput />,
        {
          nodes: [
            createStartNode({
              data: {
                variables: [{
                  type: InputVarType.textInput,
                  variable: 'internal_note',
                  label: 'Internal Note',
                  hide: true,
                }],
              },
            }),
          ],
          edges: [],
          initialStoreState: {
            inputs: {},
            showDebugAndPreviewPanel: true,
          },
        },
      )

      expect(screen.getByLabelText('Internal Note')).toBeInTheDocument()
    })
  })

  describe('ChatWrapper', () => {
    it('should seed start defaults into workflow inputs and expose restart through the ref handle', async () => {
      const chatState = createChatState()
      mockUseChat.mockReturnValue(chatState)
      const chatRef = createChatWrapperRef()

      const { store } = renderWorkflowFlowComponent(
        <ChatWrapper
          ref={chatRef}
          showConversationVariableModal={false}
          onConversationModalHide={vi.fn()}
          showInputsFieldsPanel
          onHide={vi.fn()}
        />,
        {
          nodes: [
            createStartNode({
              data: {
                variables: [{
                  type: InputVarType.textInput,
                  variable: 'name',
                  label: 'Name',
                  default: 'Ada',
                }],
              },
            }),
          ],
          edges: [],
          initialStoreState: {
            inputs: {
              custom: 'value',
            },
          },
        },
      )

      await waitFor(() => {
        expect(store.getState().inputs).toEqual({
          custom: 'value',
          name: 'Ada',
        })
      })

      expect(screen.getByText('workflow.common.previewPlaceholder')).toBeInTheDocument()

      act(() => {
        chatRef.current?.handleRestart()
      })

      expect(chatState.handleRestart).toHaveBeenCalledTimes(1)
      expect(store.getState().inputs).toEqual({
        name: 'Ada',
      })
    })

    it('should hide the side panel while responding and render the conversation modal when requested', async () => {
      const onHide = vi.fn()
      mockUseChat.mockReturnValue(createChatState({
        isResponding: true,
      }))
      mockFetchCurrentValueOfConversationVariable.mockResolvedValue(createConversationVariableResponse([
        {
          ...createConversationVariable({
            id: 'var-1',
            value: '{"latest":1}',
          }),
          updated_at: 100,
          created_at: 50,
        },
      ]))

      renderWorkflowFlowComponent(
        <ChatWrapper
          ref={createChatWrapperRef()}
          showConversationVariableModal
          onConversationModalHide={vi.fn()}
          showInputsFieldsPanel={false}
          onHide={onHide}
        />,
        {
          nodes: [
            createStartNode({
              data: {
                variables: [],
              },
            }),
          ],
          edges: [],
          initialStoreState: {
            appId: 'app-1',
            conversationVariables: [
              createConversationVariable(),
            ],
          },
        },
      )

      await waitFor(() => {
        expect(onHide).toHaveBeenCalledTimes(1)
      })

      expect(screen.getAllByText('session_state')).toHaveLength(2)
    })

    it('should forward chat actions, stop subscriptions, and expose paused input state', async () => {
      const user = userEvent.setup()
      const handleSend = vi.fn()
      const handleSwitchSibling = vi.fn()
      const handleSubmitHumanInputForm = vi.fn().mockResolvedValue(undefined)
      const handleStop = vi.fn()
      mockUseChat.mockReturnValue(createChatState({
        chatList: [
          {
            id: 'answer-1',
            isAnswer: true,
            content: 'first answer',
          },
          {
            id: 'question-1',
            isAnswer: false,
            content: 'first question',
            parentMessageId: 'answer-1',
            message_files: [],
          },
          {
            id: 'answer-2',
            isAnswer: true,
            parentMessageId: 'question-1',
            content: 'latest answer',
            workflowProcess: {
              status: 'paused',
            },
          },
        ],
        handleSend,
        handleSwitchSibling,
        handleSubmitHumanInputForm,
        handleStop,
      }))

      const { store } = renderWorkflowFlowComponent(
        <ChatWrapper
          ref={createChatWrapperRef()}
          showConversationVariableModal={false}
          onConversationModalHide={vi.fn()}
          showInputsFieldsPanel={false}
          onHide={vi.fn()}
        />,
        {
          nodes: [
            createStartNode({
              data: {
                variables: [{
                  type: InputVarType.textInput,
                  variable: 'name',
                  label: 'Name',
                  default: 'Ada',
                }],
              },
            }),
          ],
          edges: [],
          initialStoreState: {
            inputs: {
              existing: 'value',
            },
          },
        },
      )

      await waitFor(() => {
        expect(store.getState().inputs).toEqual({
          existing: 'value',
          name: 'Ada',
        })
      })

      expect(screen.getByTestId('chat-input-disabled')).toHaveTextContent('true')

      await user.click(screen.getByRole('button', { name: 'send-chat' }))
      expect(handleSend).toHaveBeenCalledWith(expect.objectContaining({
        query: 'hello',
        conversation_id: 'conversation-1',
        inputs: {
          existing: 'value',
          name: 'Ada',
        },
        parent_message_id: 'answer-2',
      }), expect.objectContaining({
        onGetSuggestedQuestions: expect.any(Function),
      }))

      const sendCallbacks = handleSend.mock.calls[0]?.[1] as {
        onGetSuggestedQuestions: (messageId: string, getAbortController: () => AbortController) => void
      }
      sendCallbacks.onGetSuggestedQuestions('message-1', () => new AbortController())
      expect(mockFetchSuggestedQuestions).toHaveBeenCalledWith('app-1', 'message-1', expect.any(Function))

      await user.click(screen.getByRole('button', { name: 'regenerate-chat' }))
      expect(handleSend).toHaveBeenNthCalledWith(2, expect.objectContaining({
        query: 'first question',
        parent_message_id: 'answer-1',
      }), expect.any(Object))

      await user.click(screen.getByRole('button', { name: 'switch-sibling' }))
      expect(handleSwitchSibling).toHaveBeenCalledWith('sibling-2', expect.objectContaining({
        onGetSuggestedQuestions: expect.any(Function),
      }))

      const switchCallbacks = handleSwitchSibling.mock.calls[0]?.[1] as {
        onGetSuggestedQuestions: (messageId: string, getAbortController: () => AbortController) => void
      }
      switchCallbacks.onGetSuggestedQuestions('message-2', () => new AbortController())
      expect(mockFetchSuggestedQuestions).toHaveBeenCalledWith('app-1', 'message-2', expect.any(Function))

      await user.click(screen.getByRole('button', { name: 'submit-human-input' }))
      await waitFor(() => {
        expect(handleSubmitHumanInputForm).toHaveBeenCalledWith('token-1', { inputs: { answer: 'ok' }, action: 'approve' })
      })

      const stopResponding = mockUseChat.mock.calls[0]?.[3] as (taskId: string) => void
      stopResponding('task-1')
      expect(mockStopChatMessageResponding).toHaveBeenCalledWith('app-1', 'task-1')

      const subscription = mockUseSubscription.mock.calls[0]?.[0] as (payload: { type: string }) => void
      act(() => {
        subscription({ type: EVENT_WORKFLOW_STOP })
      })
      expect(handleStop).toHaveBeenCalledTimes(1)
    })
  })
})
