import type { ReactNode } from 'react'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { Edge, Node } from '@/app/components/workflow/types'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
import humanInputDefault from '@/app/components/workflow/nodes/human-input/default'
import HumanInputNode from '@/app/components/workflow/nodes/human-input/node'
import {
  DeliveryMethodType,
  UserActionButtonType,
} from '@/app/components/workflow/nodes/human-input/types'
import { BlockEnum } from '@/app/components/workflow/types'
import {
  initialNodes,
  preprocessNodesAndEdges,
} from '@/app/components/workflow/utils/workflow-init'
import { withSelectorKey } from '@/test/i18n-mock'

// Mock reactflow which is needed by initialNodes and NodeSourceHandle
vi.mock('reactflow', async () => {
  const reactflow = await vi.importActual('reactflow')
  return {
    ...reactflow,
    Handle: ({ children }: { children?: ReactNode }) => <div data-testid="handle">{children}</div>,
  }
})

// Minimal store state mirroring the fields that NodeSourceHandle selects
const mockStoreState = {
  shouldAutoOpenStartNodeSelector: false,
  setShouldAutoOpenStartNodeSelector: vi.fn(),
  setHasSelectedStartNode: vi.fn(),
}

// Mock workflow store used by NodeSourceHandle
// useStore accepts a selector and applies it to the state, so tests break
// if the component starts selecting fields that aren't provided here.
vi.mock('@/app/components/workflow/store', () => ({
  useStore: vi.fn((selector?: (s: typeof mockStoreState) => unknown) =>
    selector ? selector(mockStoreState) : mockStoreState,
  ),
  useWorkflowStore: vi.fn(() => ({
    getState: () => ({
      getNodes: () => [],
    }),
  })),
}))

// Mock workflow hooks barrel (used by NodeSourceHandle via ../../../hooks)
vi.mock('../../../hooks/use-available-blocks', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../../hooks/use-available-blocks')>()

  return {
    ...actual,
    useAvailableBlocks: () => ({
      availableNextBlocks: [],
      availablePrevBlocks: [],
    }),
  }
})

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

  return {
    ...actual,
    useNodesInteractions: () => ({
      handleNodeAdd: vi.fn(),
    }),
  }
})

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

  return {
    ...actual,
    useNodesReadOnly: () => ({
      getNodesReadOnly: () => false,
      nodesReadOnly: false,
    }),
    useIsChatMode: () => false,
  }
})

// ── Factory: Build a realistic human-input node as it would appear after DSL import ──
const createHumanInputNode = (overrides?: Partial<HumanInputNodeType>): Node => ({
  id: 'human-input-1',
  type: 'custom',
  position: { x: 400, y: 200 },
  data: {
    type: BlockEnum.HumanInput,
    title: 'Human Input',
    desc: 'Wait for human input',
    delivery_methods: [
      {
        id: 'dm-1',
        type: DeliveryMethodType.WebApp,
        enabled: true,
      },
      {
        id: 'dm-2',
        type: DeliveryMethodType.Email,
        enabled: true,
        config: {
          recipients: { whole_workspace: false, items: [] },
          subject: 'Please review',
          body: 'Please review the form',
          debug_mode: false,
        },
      },
    ],
    form_content: '# Review Form\nPlease fill in the details below.',
    inputs: [
      {
        type: 'text-input',
        output_variable_name: 'review_result',
        default: { selector: [], type: 'constant' as const, value: '' },
      },
    ],
    user_actions: [
      {
        id: 'approve',
        title: 'Approve',
        button_style: UserActionButtonType.Primary,
      },
      {
        id: 'reject',
        title: 'Reject',
        button_style: UserActionButtonType.Default,
      },
    ],
    timeout: 3,
    timeout_unit: 'day' as const,
    ...overrides,
  } as HumanInputNodeType,
})

const createStartNode = (): Node => ({
  id: 'inset-s-1',
  type: 'custom',
  position: { x: 100, y: 200 },
  data: {
    type: BlockEnum.Start,
    title: 'Start',
    desc: '',
  } as Node['data'],
})

const createEdge = (
  source: string,
  target: string,
  sourceHandle = 'source',
  targetHandle = 'target',
): Edge =>
  ({
    id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
    type: 'custom',
    source,
    sourceHandle,
    target,
    targetHandle,
    data: {},
  }) as Edge

describe('DSL Import with Human Input Node', () => {
  // ── preprocessNodesAndEdges: human-input nodes pass through without error ──
  describe('preprocessNodesAndEdges', () => {
    it('should pass through a workflow containing a human-input node unchanged', () => {
      const humanInputNode = createHumanInputNode()
      const startNode = createStartNode()
      const nodes = [startNode, humanInputNode]
      const edges = [createEdge('inset-s-1', 'human-input-1')]

      const result = preprocessNodesAndEdges(nodes as Node[], edges as Edge[])

      expect(result.nodes).toHaveLength(2)
      expect(result.edges).toHaveLength(1)
      expect(result.nodes).toEqual(nodes)
      expect(result.edges).toEqual(edges)
    })

    it('should not treat human-input node as an iteration or loop node', () => {
      const humanInputNode = createHumanInputNode()
      const nodes = [humanInputNode]

      const result = preprocessNodesAndEdges(nodes as Node[], [])

      // No extra iteration/loop start nodes should be injected
      expect(result.nodes).toHaveLength(1)
      expect(result.nodes[0]!.data.type).toBe(BlockEnum.HumanInput)
    })
  })

  // ── initialNodes: human-input nodes are properly initialized ──
  describe('initialNodes', () => {
    it('should initialize a human-input node with connected handle IDs', () => {
      const humanInputNode = createHumanInputNode()
      const startNode = createStartNode()
      const nodes = [startNode, humanInputNode]
      const edges = [createEdge('inset-s-1', 'human-input-1')]

      const result = initialNodes(nodes as Node[], edges as Edge[])

      const processedHumanInput = result.find((n) => n.id === 'human-input-1')
      expect(processedHumanInput).toBeDefined()
      expect(processedHumanInput!.data.type).toBe(BlockEnum.HumanInput)
      // initialNodes sets _connectedSourceHandleIds and _connectedTargetHandleIds
      expect(processedHumanInput!.data._connectedSourceHandleIds).toBeDefined()
      expect(processedHumanInput!.data._connectedTargetHandleIds).toBeDefined()
    })

    it('should preserve human-input node data after initialization', () => {
      const humanInputNode = createHumanInputNode()
      const nodes = [humanInputNode]

      const result = initialNodes(nodes as Node[], [])

      const processed = result[0]
      const nodeData = processed!.data as HumanInputNodeType
      expect(nodeData.delivery_methods).toHaveLength(2)
      expect(nodeData.user_actions).toHaveLength(2)
      expect(nodeData.form_content).toBe('# Review Form\nPlease fill in the details below.')
      expect(nodeData.timeout).toBe(3)
      expect(nodeData.timeout_unit).toBe('day')
    })

    it('should set node type to custom if not set', () => {
      const humanInputNode = createHumanInputNode()
      delete (humanInputNode as Record<string, unknown>).type

      const result = initialNodes([humanInputNode] as Node[], [])

      expect(result[0]!.type).toBe('custom')
    })
  })

  describe('HumanInputNode Component', () => {
    it('should display delivery method labels when methods are present', () => {
      const node = createHumanInputNode()

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      // Delivery method type labels are rendered in lowercase
      // Delivery method type labels are rendered in lowercase
      expect(screen.getByText('webapp'))!.toBeInTheDocument()
      expect(screen.getByText('email'))!.toBeInTheDocument()
    })

    it('should display user action IDs', () => {
      const node = createHumanInputNode()

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.getByText('approve'))!.toBeInTheDocument()
      expect(screen.getByText('reject'))!.toBeInTheDocument()
    })

    it('should always display Timeout handle', () => {
      const node = createHumanInputNode()

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.getByText('Timeout'))!.toBeInTheDocument()
    })

    it('should hide delivery method labels when no methods are configured', () => {
      const node = createHumanInputNode({ delivery_methods: [] })

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.queryByText('webapp')).not.toBeInTheDocument()
      expect(screen.queryByText('email')).not.toBeInTheDocument()
    })

    it('should keep the timeout branch when no user actions are configured', () => {
      const node = createHumanInputNode({ user_actions: [] })

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.getByText('Timeout'))!.toBeInTheDocument()
    })

    it('should render with only webapp delivery method', () => {
      const node = createHumanInputNode({
        delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: true }],
      })

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.getByText('webapp'))!.toBeInTheDocument()
      expect(screen.queryByText('email')).not.toBeInTheDocument()
    })

    it('should render with multiple user actions', () => {
      const node = createHumanInputNode({
        user_actions: [
          { id: 'action_1', title: 'Approve', button_style: UserActionButtonType.Primary },
          { id: 'action_2', title: 'Reject', button_style: UserActionButtonType.Default },
          { id: 'action_3', title: 'Escalate', button_style: UserActionButtonType.Accent },
        ],
      })

      render(<HumanInputNode id={node.id} data={node.data as HumanInputNodeType} />)

      expect(screen.getByText('action_1'))!.toBeInTheDocument()
      expect(screen.getByText('action_2'))!.toBeInTheDocument()
      expect(screen.getByText('action_3'))!.toBeInTheDocument()
    })
  })

  // ── Node registration: human-input is included in the workflow node registry ──
  // Verify via WORKFLOW_COMMON_NODES (lightweight metadata-only imports) instead
  // of NodeComponentMap/PanelComponentMap which pull in every node's heavy UI deps.
  describe('Node Registration', () => {
    it('should have HumanInput included in WORKFLOW_COMMON_NODES', () => {
      const entry = WORKFLOW_COMMON_NODES.find((n) => n.metaData.type === BlockEnum.HumanInput)
      expect(entry).toBeDefined()
    })
  })

  // ── Default config & validation ──
  describe('HumanInput Default Configuration', () => {
    it('should provide default values for a new human-input node', () => {
      const defaultValue = humanInputDefault.defaultValue

      expect(defaultValue.delivery_methods).toEqual([])
      expect(defaultValue.user_actions).toEqual([])
      expect(defaultValue.form_content).toBe('')
      expect(defaultValue.inputs).toEqual([])
      expect(defaultValue.timeout).toBe(3)
      expect(defaultValue.timeout_unit).toBe('day')
    })

    it('should validate that delivery methods are required', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
      expect(result.errorMessage).toBeTruthy()
    })

    it('should validate that at least one delivery method is enabled', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: false }],
        user_actions: [
          { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
        ],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
    })

    it('should validate that enabled email delivery methods have complete configuration', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [
          {
            id: 'dm-email',
            type: DeliveryMethodType.Email,
            enabled: true,
          },
        ],
        user_actions: [
          { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
        ],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
      expect(result.errorMessage).toBe('nodes.humanInput.errorMsg.emailConfigIncomplete')
    })

    it('should validate email delivery config fields before user actions', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [
          {
            id: 'dm-email',
            type: DeliveryMethodType.Email,
            enabled: true,
            config: {
              recipients: { whole_workspace: false, items: [] },
              subject: 'Review request',
              body: 'Please review {{#url#}}',
              debug_mode: false,
            },
          },
        ],
        user_actions: [
          { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
        ],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
      expect(result.errorMessage).toBe('nodes.humanInput.errorMsg.emailConfigIncomplete')
    })

    it('should validate enabled email subject and body content', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const createPayload = (body: string, subject = 'Review request') =>
        ({
          ...humanInputDefault.defaultValue,
          delivery_methods: [
            {
              id: 'dm-email',
              type: DeliveryMethodType.Email,
              enabled: true,
              config: {
                recipients: { whole_workspace: true, items: [] },
                subject,
                body,
                debug_mode: false,
              },
            },
          ],
          user_actions: [
            { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
          ],
        }) as HumanInputNodeType

      expect(humanInputDefault.checkValid(createPayload('{{#url#}}', '  '), t)).toEqual({
        isValid: false,
        errorMessage: 'nodes.humanInput.errorMsg.emailConfigIncomplete',
      })
      expect(humanInputDefault.checkValid(createPayload('Please review'), t)).toEqual({
        isValid: false,
        errorMessage: 'nodes.humanInput.errorMsg.emailConfigIncomplete',
      })
    })

    it('should validate that user actions are required', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: true }],
        user_actions: [],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
    })

    it('should validate that user action IDs are not duplicated', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: true }],
        user_actions: [
          { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
          { id: 'approve', title: 'Also Approve', button_style: UserActionButtonType.Default },
        ],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(false)
    })

    it('should validate that user action ids and titles are not empty', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const createPayload = (userActions: HumanInputNodeType['user_actions']) =>
        ({
          ...humanInputDefault.defaultValue,
          delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: true }],
          user_actions: userActions,
        }) as HumanInputNodeType

      expect(
        humanInputDefault.checkValid(
          createPayload([
            { id: '  ', title: 'Approve', button_style: UserActionButtonType.Primary },
          ]),
          t,
        ),
      ).toEqual({
        isValid: false,
        errorMessage: 'nodes.humanInput.errorMsg.emptyActionId',
      })
      expect(
        humanInputDefault.checkValid(
          createPayload([
            { id: 'approve', title: '  ', button_style: UserActionButtonType.Primary },
          ]),
          t,
        ),
      ).toEqual({
        isValid: false,
        errorMessage: 'nodes.humanInput.errorMsg.emptyActionTitle',
      })
    })

    it('should pass validation with correct configuration', () => {
      const t = withSelectorKey((key: string) => key, 'workflow')
      const payload = {
        ...humanInputDefault.defaultValue,
        delivery_methods: [{ id: 'dm-1', type: DeliveryMethodType.WebApp, enabled: true }],
        user_actions: [
          { id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
          { id: 'reject', title: 'Reject', button_style: UserActionButtonType.Default },
        ],
      } as HumanInputNodeType

      const result = humanInputDefault.checkValid(payload, t)

      expect(result.isValid).toBe(true)
      expect(result.errorMessage).toBe('')
    })
  })

  // ── Output variables generation ──
  describe('HumanInput Output Variables', () => {
    it('should generate output variables from form inputs', () => {
      const payload = {
        ...humanInputDefault.defaultValue,
        inputs: [
          {
            type: 'paragraph',
            output_variable_name: 'review_result',
            default: { selector: [], type: 'constant' as const, value: '' },
          },
          {
            type: 'file',
            output_variable_name: 'attachment',
            allowed_file_extensions: [],
            allowed_file_types: [],
            allowed_file_upload_methods: [],
          },
          {
            type: 'file-list',
            output_variable_name: 'attachments',
            allowed_file_extensions: [],
            allowed_file_types: [],
            allowed_file_upload_methods: [],
            number_limits: 3,
          },
          {
            type: 'select',
            output_variable_name: 'comment',
            option_source: { type: 'constant', selector: [], value: ['A', 'B'] },
          },
        ],
      } as HumanInputNodeType

      const outputVars = humanInputDefault.getOutputVars!(payload, {}, [])

      expect(outputVars).toEqual([
        { variable: 'review_result', type: 'string' },
        { variable: 'attachment', type: 'file' },
        { variable: 'attachments', type: 'array[file]' },
        { variable: 'comment', type: 'string' },
      ])
    })

    it('should return no output variables when no form inputs exist', () => {
      const payload = {
        ...humanInputDefault.defaultValue,
        inputs: [],
      } as HumanInputNodeType

      const outputVars = humanInputDefault.getOutputVars!(payload, {}, [])

      expect(outputVars).toEqual([])
    })
  })

  // ── Full DSL import simulation: start → human-input → end ──
  describe('Full Workflow with Human Input Node', () => {
    it('should process a start → human-input → end workflow without errors', () => {
      const startNode = createStartNode()
      const humanInputNode = createHumanInputNode()
      const endNode: Node = {
        id: 'inset-e-1',
        type: 'custom',
        position: { x: 700, y: 200 },
        data: {
          type: BlockEnum.End,
          title: 'End',
          desc: '',
          outputs: [],
        } as Node['data'],
      }

      const nodes = [startNode, humanInputNode, endNode]
      const edges = [
        createEdge('inset-s-1', 'human-input-1'),
        createEdge('human-input-1', 'inset-e-1', 'approve', 'target'),
      ]

      const processed = preprocessNodesAndEdges(nodes as Node[], edges as Edge[])
      expect(processed.nodes).toHaveLength(3)
      expect(processed.edges).toHaveLength(2)

      const initialized = initialNodes(nodes as Node[], edges as Edge[])
      expect(initialized).toHaveLength(3)

      // All node types should be preserved
      const types = initialized.map((n) => n.data.type)
      expect(types).toContain(BlockEnum.Start)
      expect(types).toContain(BlockEnum.HumanInput)
      expect(types).toContain(BlockEnum.End)
    })

    it('should handle multiple branches from human-input user actions', () => {
      const startNode = createStartNode()
      const humanInputNode = createHumanInputNode()
      const approveEndNode: Node = {
        id: 'approve-end',
        type: 'custom',
        position: { x: 700, y: 100 },
        data: { type: BlockEnum.End, title: 'Approve End', desc: '', outputs: [] } as Node['data'],
      }
      const rejectEndNode: Node = {
        id: 'reject-end',
        type: 'custom',
        position: { x: 700, y: 300 },
        data: { type: BlockEnum.End, title: 'Reject End', desc: '', outputs: [] } as Node['data'],
      }

      const nodes = [startNode, humanInputNode, approveEndNode, rejectEndNode]
      const edges = [
        createEdge('inset-s-1', 'human-input-1'),
        createEdge('human-input-1', 'approve-end', 'approve', 'target'),
        createEdge('human-input-1', 'reject-end', 'reject', 'target'),
      ]

      const initialized = initialNodes(nodes as Node[], edges as Edge[])
      expect(initialized).toHaveLength(4)

      // Human input node should still have correct data
      const hiNode = initialized.find((n) => n.id === 'human-input-1')!
      expect((hiNode.data as HumanInputNodeType).user_actions).toHaveLength(2)
      expect((hiNode.data as HumanInputNodeType).delivery_methods).toHaveLength(2)
    })
  })
})
