import type { Shape } from '../../store/workflow'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow'
import { toast } from '@langgenius/dify-ui/toast'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import {
  createNodeTracing,
  createWorkflowRunningData,
} from '@/app/components/workflow/__tests__/fixtures'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { submitHumanInputForm } from '@/service/workflow'
import WorkflowPreview from '../workflow-preview'

const mockHandleCancelDebugAndPreviewPanel = vi.fn()

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

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

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

vi.mock('../../hooks/use-workflow-panel-interactions', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('../../hooks/use-workflow-panel-interactions')>()

  return {
    ...actual,
    useWorkflowInteractions: () => ({
      handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
    }),
  }
})

vi.mock('@/app/components/workflow/run/result-panel', () => ({
  default: ({ status, onOpenTracingTab }: { status?: string; onOpenTracingTab?: () => void }) => (
    <div data-testid="result-panel">
      <div>{status}</div>
      <button type="button" onClick={onOpenTracingTab}>
        open-tracing
      </button>
    </div>
  ),
}))

vi.mock('@/app/components/workflow/run/result-text', () => ({
  default: ({
    outputs,
    isPaused,
    isRunning,
    onClick,
  }: {
    outputs?: string
    isPaused?: boolean
    isRunning?: boolean
    onClick?: () => void
  }) => (
    <div>
      <div data-testid="result-text">{JSON.stringify({ outputs, isPaused, isRunning })}</div>
      <button type="button" onClick={onClick}>
        open-detail
      </button>
    </div>
  ),
}))

vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
  default: ({ list }: { list: unknown[] }) => <div data-testid="tracing-panel">{list.length}</div>,
}))

vi.mock('@/app/components/base/chat/chat/answer/reasoning-panel', () => ({
  default: ({ content, done }: { content: Record<string, string>; done: boolean }) => (
    <div data-testid="reasoning-panel" data-done={String(done)}>
      {Object.keys(content).join(',')}
    </div>
  ),
}))

vi.mock('@/app/components/workflow/panel/inputs-panel', () => ({
  default: ({ onRun }: { onRun: () => void }) => (
    <button type="button" onClick={onRun}>
      run-inputs
    </button>
  ),
}))

vi.mock('@/app/components/workflow/panel/human-input-form-list', () => ({
  default: ({
    humanInputFormDataList,
    onHumanInputFormSubmit,
  }: {
    humanInputFormDataList: unknown[]
    onHumanInputFormSubmit?: (
      token: string,
      formData: { inputs: Record<string, HumanInputFieldValue>; action: string },
    ) => Promise<void>
  }) => (
    <div>
      <div data-testid="human-form-list">{humanInputFormDataList.length}</div>
      <button
        type="button"
        onClick={() =>
          onHumanInputFormSubmit?.('form-token', { inputs: { answer: 'ok' }, action: 'approve' })
        }
      >
        submit-human-form
      </button>
    </div>
  ),
}))

vi.mock('@/app/components/workflow/panel/human-input-filled-form-list', () => ({
  default: ({ humanInputFilledFormDataList }: { humanInputFilledFormDataList: unknown[] }) => (
    <div data-testid="filled-form-list">{humanInputFilledFormDataList.length}</div>
  ),
}))

const mockCopy = vi.mocked(copy)
const mockToastSuccess = vi.mocked(toast.success)
const mockSubmitHumanInputForm = vi.mocked(submitHumanInputForm)

type WorkflowResult = NonNullable<ReturnType<typeof createWorkflowRunningData>['result']>

const createWorkflowResult = (overrides: Partial<WorkflowResult> = {}): WorkflowResult => ({
  status: WorkflowRunningStatus.Running,
  inputs_truncated: false,
  process_data_truncated: false,
  outputs_truncated: false,
  ...overrides,
})

const createHumanInputFormData = (
  overrides: Partial<HumanInputFormData> = {},
): HumanInputFormData => ({
  form_id: 'form-1',
  node_id: 'human-node-1',
  node_title: 'Need Approval',
  form_content: 'Before {{#$output.reason#}} after',
  inputs: [],
  actions: [],
  form_token: 'token-1',
  resolved_default_values: {},
  display_in_ui: true,
  expiration_time: 2_000_000_000,
  ...overrides,
})

const createHumanInputFilledFormData = (
  overrides: Partial<HumanInputFilledFormData> = {},
): HumanInputFilledFormData => ({
  node_id: 'node-1',
  node_title: 'Need Approval',
  rendered_content: 'rendered',
  action_id: 'approve',
  action_text: 'Approve',
  ...overrides,
})

describe('WorkflowPreview', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    Object.defineProperty(window, 'innerWidth', {
      configurable: true,
      value: 1200,
    })
  })

  it('should keep the input tab active, switch to result after running, and close the preview panel', async () => {
    const user = userEvent.setup()
    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        showInputsPanel: true,
        showDebugAndPreviewPanel: true,
        previewPanelWidth: 420,
      },
    })

    expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument()

    await user.click(screen.getByRole('button', { name: 'run-inputs' }))
    expect(screen.getByTestId('result-text')).toBeInTheDocument()

    await user.click(screen.getByRole('button', { name: /operation\.close/ }))
    expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
  })

  it('should switch to detail when the workflow is listening', () => {
    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        isListening: true,
        workflowRunningData: createWorkflowRunningData({
          result: createWorkflowResult({
            status: WorkflowRunningStatus.Running,
          }),
        }),
      },
    })

    expect(screen.getByTestId('result-panel')).toHaveTextContent(WorkflowRunningStatus.Running)
  })

  it('should switch to detail when a finished run has no outputs or files', () => {
    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({
              status: WorkflowRunningStatus.Succeeded,
              files: [],
            }),
          }),
          resultText: '',
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    expect(screen.getByTestId('result-panel')).toHaveTextContent(WorkflowRunningStatus.Succeeded)
  })

  it('should render paused human input results and submit pending forms', async () => {
    const user = userEvent.setup()
    const pausedData = createWorkflowRunningData({
      result: createWorkflowResult({
        status: WorkflowRunningStatus.Paused,
        files: [],
      }),
      humanInputFormDataList: [createHumanInputFormData()],
      humanInputFilledFormDataList: [createHumanInputFilledFormData()],
    })

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: pausedData,
      },
    })

    expect(screen.getByTestId('human-form-list')).toHaveTextContent('1')
    expect(screen.getByTestId('filled-form-list')).toHaveTextContent('1')

    await user.click(screen.getByRole('button', { name: 'submit-human-form' }))
    expect(mockSubmitHumanInputForm).toHaveBeenCalledWith('form-token', {
      inputs: { answer: 'ok' },
      action: 'approve',
    })
  })

  it('should copy successful string output and show a success toast', async () => {
    const user = userEvent.setup()

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({
              status: WorkflowRunningStatus.Succeeded,
              files: [],
            }),
          }),
          resultText: 'final answer',
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    await user.click(screen.getByText('runLog.result'))
    await user.click(screen.getByRole('button', { name: 'common.operation.copy' }))

    expect(mockCopy).toHaveBeenCalledWith('final answer')
    expect(mockToastSuccess).toHaveBeenCalledWith('common.actionMsg.copySuccessfully')
  })

  it('should show a loading state for an empty detail panel', () => {
    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        isListening: true,
        workflowRunningData: undefined,
      },
    })

    expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
  })

  it('should show a loading state for an empty tracing panel', () => {
    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: createWorkflowRunningData({
          tracing: [],
        }),
      },
    })

    expect(screen.getByTestId('tracing-panel')).toHaveTextContent('0')
    expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
  })

  it('should keep inert tabs disabled without run data and switch among result, detail, and tracing when data exists', async () => {
    const user = userEvent.setup()
    const { store } = renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        showInputsPanel: true,
        workflowRunningData: undefined,
      },
    })

    await user.click(screen.getByText('runLog.result'))
    await user.click(screen.getByText('runLog.detail'))
    await user.click(screen.getByText('runLog.tracing'))
    expect(screen.getByRole('button', { name: 'run-inputs' })).toBeInTheDocument()

    store.setState({
      workflowRunningData: {
        ...createWorkflowRunningData({
          result: createWorkflowResult({
            status: WorkflowRunningStatus.Succeeded,
            files: [],
          }),
          tracing: [createNodeTracing()],
        }),
        resultText: 'ready',
      } as NonNullable<Shape['workflowRunningData']>,
    })

    await user.click(screen.getByText('runLog.result'))
    expect(screen.getByTestId('result-text')).toBeInTheDocument()

    await user.click(screen.getByText('runLog.detail'))
    expect(screen.getByTestId('result-panel')).toBeInTheDocument()

    await user.click(screen.getByText('runLog.tracing'))
    expect(screen.getByTestId('tracing-panel')).toHaveTextContent('1')

    await user.click(screen.getByText('runLog.result'))
    await user.click(screen.getByRole('button', { name: 'open-detail' }))
    expect(screen.getByTestId('result-panel')).toBeInTheDocument()
  })

  it('should render a single merged reasoning panel above the result on the result tab', async () => {
    const user = userEvent.setup()

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({ status: WorkflowRunningStatus.Running }),
          }),
          resultText: '',
          reasoningContent: { 'llm-1': 'thinking a', 'llm-2': 'thinking b' },
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    await user.click(screen.getByText('runLog.result'))

    // one panel that carries both nodes' reasoning; still running → timer keeps ticking
    const panels = screen.getAllByTestId('reasoning-panel')
    expect(panels).toHaveLength(1)
    expect(panels[0]).toHaveTextContent('llm-1,llm-2')
    expect(panels[0]).toHaveAttribute('data-done', 'false')
  })

  it('should mark reasoning done once the answer starts streaming while still running', async () => {
    const user = userEvent.setup()

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({ status: WorkflowRunningStatus.Running }),
          }),
          resultText: 'the answer',
          reasoningContent: { 'llm-1': 'thinking a' },
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    await user.click(screen.getByText('runLog.result'))

    // answer-started (resultText non-empty) freezes the timer even though the run is still Running
    expect(screen.getByTestId('reasoning-panel')).toHaveAttribute('data-done', 'true')
  })

  it('should not render a reasoning panel when there is no reasoning content', async () => {
    const user = userEvent.setup()

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({ status: WorkflowRunningStatus.Running }),
          }),
          resultText: '',
          reasoningContent: { llm: '' },
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    await user.click(screen.getByText('runLog.result'))

    expect(screen.queryByTestId('reasoning-panel')).not.toBeInTheDocument()
  })

  it('should switch to the tracing tab when result panel requests it', async () => {
    const user = userEvent.setup()

    renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        workflowRunningData: {
          ...createWorkflowRunningData({
            result: createWorkflowResult({
              status: 'partial-succeeded',
              files: [],
            }),
            tracing: [createNodeTracing()],
          }),
          resultText: 'ready',
        } as NonNullable<Shape['workflowRunningData']>,
      },
    })

    await user.click(screen.getByText('runLog.detail'))
    await user.click(screen.getByRole('button', { name: 'open-tracing' }))

    expect(screen.getByTestId('tracing-panel')).toHaveTextContent('1')
  })

  it('should resize the preview panel within the allowed workflow canvas bounds', async () => {
    const { container, store } = renderWorkflowComponent(<WorkflowPreview />, {
      initialStoreState: {
        previewPanelWidth: 450,
        workflowCanvasWidth: 1000,
      },
    })

    const resizeHandle = container.querySelector('.cursor-col-resize') as HTMLElement

    fireEvent.mouseDown(resizeHandle)
    fireEvent.mouseMove(window, { clientX: 700 })
    fireEvent.mouseMove(window, { clientX: 100 })
    fireEvent.mouseUp(window)

    await waitFor(() => {
      expect(store.getState().previewPanelWidth).toBe(500)
    })
  })
})
