import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render } from '@testing-library/react'
import { CanvasContainer } from '../CanvasContainer'
import { useCanvasStore } from '@/stores/canvasStore'
import { useThemeStore } from '@/stores/themeStore'
import type { Node, Edge } from '@xyflow/react'
import type { NodeData, EdgeData } from '@/types'

// Capture props passed to ReactFlow so we can test the callbacks
let rfProps: Record<string, unknown> = {}

// Hoisted holder so the mock factory can read the configurable intersection set.
const rf = vi.hoisted(() => ({ intersecting: [] as unknown[] }))

vi.mock('@xyflow/react', () => ({
  ReactFlow: (props: Record<string, unknown>) => {
    rfProps = props
    return <div data-testid="react-flow" />
  },
  Background: () => null,
  Controls: () => null,
  ControlButton: () => null,
  BackgroundVariant: { Dots: 'dots' },
  ConnectionMode: { Loose: 'loose' },
  SelectionMode: { Partial: 'partial' },
  Position: { Top: 'top', Right: 'right', Bottom: 'bottom', Left: 'left' },
  useReactFlow: () => ({
    fitView: vi.fn(),
    screenToFlowPosition: vi.fn(),
    getIntersectingNodes: () => rf.intersecting,
    setNodes: vi.fn(),
    getNodes: () => [],
  }),
}))

vi.mock('@xyflow/react/dist/style.css', () => ({}))

function makeNode(id: string): Node<NodeData> {
  return {
    id,
    type: 'server',
    position: { x: 0, y: 0 },
    data: { label: id, type: 'server', status: 'unknown', services: [] },
  }
}

function makeEdge(id: string): Edge<EdgeData> {
  return { id, source: 'n1', target: 'n2', type: 'ethernet', data: { type: 'ethernet' } }
}

describe('CanvasContainer', () => {
  beforeEach(() => {
    rfProps = {}
    rf.intersecting = []
    useCanvasStore.setState({ nodes: [], edges: [], selectedNodeId: null })
    useThemeStore.setState({ activeTheme: 'default' })
  })

  // ── Rendering ─────────────────────────────────────────────────────────────

  it('renders without crashing', () => {
    const { getByTestId } = render(<CanvasContainer />)
    expect(getByTestId('react-flow')).toBeDefined()
  })

  it('passes nodes from store to ReactFlow', () => {
    useCanvasStore.setState({ nodes: [makeNode('n1'), makeNode('n2')] })
    render(<CanvasContainer />)
    expect((rfProps.nodes as Node[]).length).toBe(2)
  })

  it('passes edges from store to ReactFlow', () => {
    useCanvasStore.setState({
      nodes: [makeNode('n1'), makeNode('n2')],
      edges: [makeEdge('e1')],
    })
    render(<CanvasContainer />)
    expect((rfProps.edges as Edge[]).length).toBe(1)
  })

  // ── Node click → selection ────────────────────────────────────────────────

  it('calls setSelectedNode with node id on node click', () => {
    const node = makeNode('n1')
    useCanvasStore.setState({ nodes: [node] })
    render(<CanvasContainer />)
    ;(rfProps.onNodeClick as (...args: unknown[]) => unknown)({} as MouseEvent, node)
    expect(useCanvasStore.getState().selectedNodeId).toBe('n1')
  })

  // ── Pane click → deselect ─────────────────────────────────────────────────

  it('calls setSelectedNode(null) on pane click', () => {
    useCanvasStore.setState({ selectedNodeId: 'n1' })
    render(<CanvasContainer />)
    ;(rfProps.onPaneClick as (...args: unknown[]) => unknown)()
    expect(useCanvasStore.getState().selectedNodeId).toBeNull()
  })

  // ── Edge double-click ─────────────────────────────────────────────────────

  it('calls onEdgeDoubleClick prop when an edge is double-clicked', () => {
    const onEdgeDoubleClick = vi.fn()
    const edge = makeEdge('e1')
    render(<CanvasContainer onEdgeDoubleClick={onEdgeDoubleClick} />)
    ;(rfProps.onEdgeDoubleClick as (...args: unknown[]) => unknown)({} as MouseEvent, edge)
    expect(onEdgeDoubleClick).toHaveBeenCalledWith(edge)
  })

  it('does not throw when onEdgeDoubleClick is not provided', () => {
    const edge = makeEdge('e1')
    render(<CanvasContainer />)
    expect(() => {
      ;(rfProps.onEdgeDoubleClick as (...args: unknown[]) => unknown)({} as MouseEvent, edge)
    }).not.toThrow()
  })

  // ── Node double-click ─────────────────────────────────────────────────────

  it('calls onNodeDoubleClick prop when a node is double-clicked', () => {
    const onNodeDoubleClick = vi.fn()
    const node = makeNode('n1')
    render(<CanvasContainer onNodeDoubleClick={onNodeDoubleClick} />)
    ;(rfProps.onNodeDoubleClick as (...args: unknown[]) => unknown)({} as MouseEvent, node)
    expect(onNodeDoubleClick).toHaveBeenCalledWith(node)
  })

  it('does not throw when onNodeDoubleClick is not provided', () => {
    const node = makeNode('n1')
    render(<CanvasContainer />)
    expect(() => {
      ;(rfProps.onNodeDoubleClick as (...args: unknown[]) => unknown)({} as MouseEvent, node)
    }).not.toThrow()
  })

  // ── Connection validation ─────────────────────────────────────────────────

  it('isValidConnection returns false for self-connections', () => {
    render(<CanvasContainer />)
    const isValid = rfProps.isValidConnection as (c: { source: string; target: string }) => boolean
    expect(isValid({ source: 'n1', target: 'n1' })).toBe(false)
  })

  it('isValidConnection returns true for different nodes', () => {
    render(<CanvasContainer />)
    const isValid = rfProps.isValidConnection as (c: { source: string; target: string }) => boolean
    expect(isValid({ source: 'n1', target: 'n2' })).toBe(true)
  })

  // ── onConnect prop passthrough ────────────────────────────────────────────

  it('passes onConnect prop to ReactFlow', () => {
    const onConnect = vi.fn()
    render(<CanvasContainer onConnect={onConnect} />)
    ;(rfProps.onConnect as (...args: unknown[]) => unknown)({ source: 'a', target: 'b', sourceHandle: null, targetHandle: null })
    expect(onConnect).toHaveBeenCalledOnce()
  })

  // ── onNodeDragStart prop passthrough ──────────────────────────────────────

  it('passes onNodeDragStart prop to ReactFlow', () => {
    const onNodeDragStart = vi.fn()
    render(<CanvasContainer onNodeDragStart={onNodeDragStart} />)
    expect(rfProps.onNodeDragStart).toBe(onNodeDragStart)
  })

  // ── Drag onto group → onRequestAddToGroup ─────────────────────────────────

  function groupNode(id: string): Node<NodeData> {
    return { id, type: 'group', position: { x: 0, y: 0 }, data: { label: id, type: 'group', status: 'unknown', services: [] } }
  }

  it('fires onRequestAddToGroup when a node is dropped over a group', () => {
    const onRequestAddToGroup = vi.fn()
    const node = makeNode('n1')
    const group = groupNode('g1')
    rf.intersecting = [group]
    render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToGroup).toHaveBeenCalledWith({ nodeId: 'n1', groupId: 'g1' })
  })

  it('does not fire onRequestAddToGroup when no group is under the node', () => {
    const onRequestAddToGroup = vi.fn()
    const node = makeNode('n1')
    rf.intersecting = [makeNode('n2')]
    render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToGroup).not.toHaveBeenCalled()
  })

  it('does not fire onRequestAddToGroup for an already-parented node', () => {
    const onRequestAddToGroup = vi.fn()
    const node = { ...makeNode('n1'), parentId: 'gOther' }
    rf.intersecting = [groupNode('g1')]
    render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToGroup).not.toHaveBeenCalled()
  })

  it('does not fire onRequestAddToGroup when the dragged node is itself a group', () => {
    const onRequestAddToGroup = vi.fn()
    const node = groupNode('g2')
    rf.intersecting = [groupNode('g1')]
    render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToGroup).not.toHaveBeenCalled()
  })

  // ── Drag onto container node → onRequestAddToContainer ────────────────────

  function containerNode(id: string, type: NodeData['type'] = 'proxmox'): Node<NodeData> {
    return { id, type, position: { x: 0, y: 0 }, data: { label: id, type, status: 'unknown', services: [], container_mode: true } }
  }

  it('fires onRequestAddToContainer when a node is dropped over a container_mode node', () => {
    const onRequestAddToContainer = vi.fn()
    const node = makeNode('n1')
    rf.intersecting = [containerNode('px1')]
    render(<CanvasContainer onRequestAddToContainer={onRequestAddToContainer} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToContainer).toHaveBeenCalledWith({ nodeId: 'n1', containerId: 'px1' })
  })

  it('prefers a group over a container when both intersect', () => {
    const onRequestAddToGroup = vi.fn()
    const onRequestAddToContainer = vi.fn()
    const node = makeNode('n1')
    rf.intersecting = [containerNode('px1'), groupNode('g1')]
    render(<CanvasContainer onRequestAddToGroup={onRequestAddToGroup} onRequestAddToContainer={onRequestAddToContainer} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToGroup).toHaveBeenCalledWith({ nodeId: 'n1', groupId: 'g1' })
    expect(onRequestAddToContainer).not.toHaveBeenCalled()
  })

  it('does not fire onRequestAddToContainer for an already-parented node', () => {
    const onRequestAddToContainer = vi.fn()
    const node = { ...makeNode('n1'), parentId: 'pxOther' }
    rf.intersecting = [containerNode('px1')]
    render(<CanvasContainer onRequestAddToContainer={onRequestAddToContainer} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToContainer).not.toHaveBeenCalled()
  })

  it('does not fire onRequestAddToContainer when the target node is not in container_mode', () => {
    const onRequestAddToContainer = vi.fn()
    const node = makeNode('n1')
    rf.intersecting = [makeNode('n2')]
    render(<CanvasContainer onRequestAddToContainer={onRequestAddToContainer} />)
    ;(rfProps.onNodeDragStop as (...args: unknown[]) => unknown)({} as MouseEvent, node, [node])
    expect(onRequestAddToContainer).not.toHaveBeenCalled()
  })

  // ── Canvas settings ───────────────────────────────────────────────────────

  it('enables snapToGrid', () => {
    render(<CanvasContainer />)
    expect(rfProps.snapToGrid).toBe(true)
  })

  it('sets snapGrid to [8, 8]', () => {
    render(<CanvasContainer />)
    expect(rfProps.snapGrid).toEqual([8, 8])
  })

  // ── Delete key ────────────────────────────────────────────────────────────

  it('sets deleteKeyCode to include both Backspace and Delete', () => {
    render(<CanvasContainer />)
    expect(rfProps.deleteKeyCode).toEqual(['Backspace', 'Delete'])
  })

  // ── Lasso / multi-select ──────────────────────────────────────────────────

  it('enables selectionOnDrag for lasso selection', () => {
    render(<CanvasContainer />)
    expect(rfProps.selectionOnDrag).toBe(true)
  })

  it('sets panActivationKeyCode to Space', () => {
    render(<CanvasContainer />)
    expect(rfProps.panActivationKeyCode).toBe('Space')
  })

  it('sets panOnDrag to [1, 2]', () => {
    render(<CanvasContainer />)
    expect(rfProps.panOnDrag).toEqual([1, 2])
  })

  it('sets selectionMode to Partial', () => {
    render(<CanvasContainer />)
    expect(rfProps.selectionMode).toBe('partial')
  })

  it('sets multiSelectionKeyCode to Meta and Control', () => {
    render(<CanvasContainer />)
    expect(rfProps.multiSelectionKeyCode).toEqual(['Meta', 'Control'])
  })

  it('clears selectedNode (sets null) on Ctrl+click instead of selecting', () => {
    const node = makeNode('n1')
    useCanvasStore.setState({ nodes: [node], selectedNodeId: 'n1' })
    render(<CanvasContainer />)
    ;(rfProps.onNodeClick as (...args: unknown[]) => unknown)(
      { ctrlKey: true, metaKey: false } as unknown as MouseEvent,
      node,
    )
    expect(useCanvasStore.getState().selectedNodeId).toBeNull()
  })

  it('clears selectedNode (sets null) on Cmd+click', () => {
    const node = makeNode('n1')
    useCanvasStore.setState({ nodes: [node], selectedNodeId: 'n1' })
    render(<CanvasContainer />)
    ;(rfProps.onNodeClick as (...args: unknown[]) => unknown)(
      { ctrlKey: false, metaKey: true } as unknown as MouseEvent,
      node,
    )
    expect(useCanvasStore.getState().selectedNodeId).toBeNull()
  })

  // ── onBeforeDelete snapshot ───────────────────────────────────────────────

  it('onBeforeDelete calls snapshotHistory and returns true', async () => {
    const snapshotHistory = vi.fn()
    useCanvasStore.setState({ snapshotHistory } as unknown as Parameters<typeof useCanvasStore.setState>[0])
    render(<CanvasContainer />)
    const result = await (rfProps.onBeforeDelete as () => Promise<boolean>)()
    expect(snapshotHistory).toHaveBeenCalledOnce()
    expect(result).toBe(true)
  })
})
