import { act, renderHook } from '@testing-library/react'
import { FlowType } from '@/types/common'
import { VarType } from '../../../types'
import { useGetAvailableVars, useVariableAssigner } from '../hooks'

const mockUseStoreApi = vi.hoisted(() => vi.fn())
const mockUseNodes = vi.hoisted(() => vi.fn())
const mockUseNodeDataUpdate = vi.hoisted(() => vi.fn())
const mockUseWorkflow = vi.hoisted(() => vi.fn())
const mockUseWorkflowVariables = vi.hoisted(() => vi.fn())
const mockUseIsChatMode = vi.hoisted(() => vi.fn())
const mockUseWorkflowStore = vi.hoisted(() => vi.fn())
const mockFlowType = vi.hoisted(() => ({
  value: undefined as FlowType | undefined,
}))

vi.mock('reactflow', () => ({
  useStoreApi: () => mockUseStoreApi(),
  useNodes: () => mockUseNodes(),
}))

vi.mock('../../../hooks/use-node-data-update', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../../hooks/use-node-data-update')>()

  return {
    ...actual,
    useNodeDataUpdate: () => mockUseNodeDataUpdate(),
  }
})

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

  return {
    ...actual,
    useWorkflow: () => mockUseWorkflow(),
    useIsChatMode: () => mockUseIsChatMode(),
  }
})

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

  return {
    ...actual,
    useWorkflowVariables: () => mockUseWorkflowVariables(),
  }
})

vi.mock('@/app/components/workflow/store', () => ({
  useWorkflowStore: () => mockUseWorkflowStore(),
}))

vi.mock('@/app/components/workflow/hooks-store/store', () => ({
  useHooksStore: (selector: (state: { configsMap?: { flowType?: FlowType } }) => unknown) =>
    selector({
      configsMap: {
        flowType: mockFlowType.value,
      },
    }),
}))

describe('variable-assigner/hooks', () => {
  const mockHandleNodeDataUpdate = vi.fn()
  const mockSetNodes = vi.fn()
  const mockSetShowAssignVariablePopup = vi.fn()
  const mockSetHoveringAssignVariableGroupId = vi.fn()
  const getNodes = vi.fn()

  beforeEach(() => {
    vi.clearAllMocks()
    mockFlowType.value = undefined
    getNodes.mockReturnValue([
      {
        id: 'assigner-1',
        data: {
          variables: [['start', 'foo']],
          output_type: VarType.string,
          advanced_settings: {
            groups: [
              {
                groupId: 'group-1',
                variables: [],
                output_type: VarType.string,
              },
            ],
          },
        },
      },
    ])
    mockUseStoreApi.mockReturnValue({
      getState: () => ({
        getNodes,
        setNodes: mockSetNodes,
      }),
    })
    mockUseNodeDataUpdate.mockReturnValue({
      handleNodeDataUpdate: mockHandleNodeDataUpdate,
    })
    mockUseWorkflowStore.mockReturnValue({
      getState: () => ({
        setShowAssignVariablePopup: mockSetShowAssignVariablePopup,
        setHoveringAssignVariableGroupId: mockSetHoveringAssignVariableGroupId,
        connectingNodePayload: { id: 'connecting-node' },
      }),
    })
    mockUseNodes.mockReturnValue([])
    mockUseWorkflow.mockReturnValue({
      getBeforeNodesInSameBranchIncludeParent: vi.fn(),
    })
    mockUseWorkflowVariables.mockReturnValue({
      getNodeAvailableVars: vi.fn(),
    })
    mockUseIsChatMode.mockReturnValue(false)
  })

  it('should append target variables, ignore duplicates, and update grouped variables', () => {
    const { result } = renderHook(() => useVariableAssigner())

    act(() => {
      result.current.handleAssignVariableValueChange('assigner-1', ['start', 'bar'], {
        type: VarType.number,
      } as never)
      result.current.handleAssignVariableValueChange('assigner-1', ['start', 'foo'], {
        type: VarType.number,
      } as never)
      result.current.handleAssignVariableValueChange(
        'assigner-1',
        ['start', 'grouped'],
        { type: VarType.arrayString } as never,
        'group-1',
      )
    })

    expect(mockHandleNodeDataUpdate).toHaveBeenNthCalledWith(1, {
      id: 'assigner-1',
      data: {
        variables: [
          ['start', 'foo'],
          ['start', 'bar'],
        ],
        output_type: VarType.number,
      },
    })
    expect(mockHandleNodeDataUpdate).toHaveBeenNthCalledWith(2, {
      id: 'assigner-1',
      data: {
        advanced_settings: {
          groups: [
            {
              groupId: 'group-1',
              variables: [['start', 'grouped']],
              output_type: VarType.arrayString,
            },
          ],
        },
      },
    })
    expect(mockHandleNodeDataUpdate).toHaveBeenCalledTimes(2)
  })

  it('should close the popup and add variables through the positioned add-variable flow', () => {
    getNodes.mockReturnValue([
      {
        id: 'source-node',
        data: {
          _showAddVariablePopup: true,
          _holdAddVariablePopup: true,
        },
      },
      {
        id: 'assigner-1',
        data: {
          variables: [],
          advanced_settings: {
            groups: [
              {
                groupId: 'group-1',
                variables: [],
              },
            ],
          },
          _showAddVariablePopup: true,
          _holdAddVariablePopup: true,
        },
      },
    ])

    const { result } = renderHook(() => useVariableAssigner())

    act(() => {
      result.current.handleAddVariableInAddVariablePopupWithPosition(
        'source-node',
        'assigner-1',
        'group-1',
        ['start', 'output'],
        { type: VarType.object } as never,
      )
    })

    expect(mockSetNodes).toHaveBeenCalledWith([
      expect.objectContaining({
        id: 'source-node',
        data: expect.objectContaining({
          _showAddVariablePopup: false,
          _holdAddVariablePopup: false,
        }),
      }),
      expect.objectContaining({
        id: 'assigner-1',
        data: expect.objectContaining({
          _showAddVariablePopup: false,
          _holdAddVariablePopup: false,
        }),
      }),
    ])
    expect(mockSetShowAssignVariablePopup).toHaveBeenCalledWith(undefined)
    expect(mockHandleNodeDataUpdate).toHaveBeenCalledWith({
      id: 'assigner-1',
      data: {
        advanced_settings: {
          groups: [
            {
              groupId: 'group-1',
              variables: [['start', 'output']],
              output_type: VarType.object,
            },
          ],
        },
      },
    })
  })

  it('should update the hovered group state on enter and leave', () => {
    const { result } = renderHook(() => useVariableAssigner())

    act(() => {
      result.current.handleGroupItemMouseEnter('group-1')
      result.current.handleGroupItemMouseLeave()
    })

    expect(mockSetHoveringAssignVariableGroupId).toHaveBeenNthCalledWith(1, 'group-1')
    expect(mockSetHoveringAssignVariableGroupId).toHaveBeenNthCalledWith(2, undefined)
  })

  it('should collect available vars and filter start-node env vars when hideEnv is enabled', () => {
    mockUseNodes.mockReturnValue([
      {
        id: 'current-node',
        parentId: 'parent-node',
      },
      {
        id: 'before-1',
      },
      {
        id: 'parent-node',
      },
    ])
    const getBeforeNodesInSameBranchIncludeParent = vi.fn(() => [
      { id: 'before-1' },
      { id: 'before-1' },
    ])
    const getNodeAvailableVars = vi
      .fn()
      .mockReturnValueOnce([
        {
          isStartNode: true,
          vars: [{ variable: 'sys.user_id' }, { variable: 'foo' }],
        },
        {
          isStartNode: false,
          vars: [],
        },
      ])
      .mockReturnValueOnce([
        {
          isStartNode: false,
          vars: [{ variable: 'bar' }],
        },
      ])

    mockUseWorkflow.mockReturnValue({
      getBeforeNodesInSameBranchIncludeParent,
    })
    mockUseWorkflowVariables.mockReturnValue({
      getNodeAvailableVars,
    })

    const { result } = renderHook(() => useGetAvailableVars())

    expect(result.current('current-node', 'target', () => true, true)).toEqual([
      {
        isStartNode: true,
        vars: [{ variable: 'foo' }],
      },
    ])
    expect(result.current('current-node', 'target', () => true, false)).toEqual([
      {
        isStartNode: false,
        vars: [{ variable: 'bar' }],
      },
    ])
    expect(result.current('missing-node', 'target', () => true)).toEqual([])
  })

  it('should filter system variables when the current flow is a snippet', () => {
    mockFlowType.value = FlowType.snippet
    mockUseNodes.mockReturnValue([
      {
        id: 'current-node',
      },
      {
        id: 'before-1',
      },
    ])
    const getBeforeNodesInSameBranchIncludeParent = vi.fn(() => [{ id: 'before-1' }])
    const getNodeAvailableVars = vi.fn(() => [
      {
        isStartNode: false,
        vars: [{ variable: 'sys.user_id' }, { variable: 'answer' }],
      },
    ])

    mockUseWorkflow.mockReturnValue({
      getBeforeNodesInSameBranchIncludeParent,
    })
    mockUseWorkflowVariables.mockReturnValue({
      getNodeAvailableVars,
    })

    const { result } = renderHook(() => useGetAvailableVars())

    expect(result.current('current-node', 'target', () => true, false)).toEqual([
      {
        isStartNode: false,
        vars: [{ variable: 'answer' }],
      },
    ])
  })
})
