import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Group, Sprite } from 'three'

const forceGraphMocks = vi.hoisted(() => {
  const graphData = vi.fn()
  const onNodeClick = vi.fn()
  const onBackgroundClick = vi.fn()
  const nodeColor = vi.fn()
  const nodeRelSize = vi.fn()
  const linkColor = vi.fn()
  const linkWidth = vi.fn()
  const linkOpacity = vi.fn()
  const cameraPosition = vi.fn()
  const width = vi.fn()
  const height = vi.fn()
  const cooldownTicks = vi.fn()
  const d3AlphaDecay = vi.fn()
  const d3VelocityDecay = vi.fn()
  const enableNodeDrag = vi.fn()
  const nodeLabel = vi.fn()
  const nodeThreeObject = vi.fn()
  const linkDirectionalParticles = vi.fn()
  const linkDirectionalParticleWidth = vi.fn()
  const linkDirectionalParticleSpeed = vi.fn()
  const linkDirectionalParticleColor = vi.fn()
  const nodeVisibility = vi.fn()
  const linkVisibility = vi.fn()
  const pauseAnimation = vi.fn()
  const resumeAnimation = vi.fn()

  const graphInstance = {
    graphData: vi.fn((...args) => {
      graphData(...args)
      return graphInstance
    }),
    onNodeClick: vi.fn((...args) => {
      onNodeClick(...args)
      return graphInstance
    }),
    onBackgroundClick: vi.fn((...args) => {
      onBackgroundClick(...args)
      return graphInstance
    }),
    nodeColor: vi.fn((...args) => {
      nodeColor(...args)
      return graphInstance
    }),
    nodeRelSize: vi.fn((...args) => {
      nodeRelSize(...args)
      return graphInstance
    }),
    linkColor: vi.fn((...args) => {
      linkColor(...args)
      return graphInstance
    }),
    linkWidth: vi.fn((...args) => {
      linkWidth(...args)
      return graphInstance
    }),
    linkOpacity: vi.fn((...args) => {
      linkOpacity(...args)
      return graphInstance
    }),
    cameraPosition: vi.fn((...args) => {
      cameraPosition(...args)
      return graphInstance
    }),
    width: vi.fn((...args) => {
      width(...args)
      return graphInstance
    }),
    height: vi.fn((...args) => {
      height(...args)
      return graphInstance
    }),
    cooldownTicks: vi.fn((...args) => {
      cooldownTicks(...args)
      return graphInstance
    }),
    d3AlphaDecay: vi.fn((...args) => {
      d3AlphaDecay(...args)
      return graphInstance
    }),
    d3VelocityDecay: vi.fn((...args) => {
      d3VelocityDecay(...args)
      return graphInstance
    }),
    enableNodeDrag: vi.fn((...args) => {
      enableNodeDrag(...args)
      return graphInstance
    }),
    nodeLabel: vi.fn((...args) => {
      nodeLabel(...args)
      return graphInstance
    }),
    nodeThreeObject: vi.fn((...args) => {
      nodeThreeObject(...args)
      return graphInstance
    }),
    linkDirectionalParticles: vi.fn((...args) => {
      linkDirectionalParticles(...args)
      return graphInstance
    }),
    linkDirectionalParticleWidth: vi.fn((...args) => {
      linkDirectionalParticleWidth(...args)
      return graphInstance
    }),
    linkDirectionalParticleSpeed: vi.fn((...args) => {
      linkDirectionalParticleSpeed(...args)
      return graphInstance
    }),
    linkDirectionalParticleColor: vi.fn((...args) => {
      linkDirectionalParticleColor(...args)
      return graphInstance
    }),
    nodeVisibility: vi.fn((...args) => {
      nodeVisibility(...args)
      return graphInstance
    }),
    linkVisibility: vi.fn((...args) => {
      linkVisibility(...args)
      return graphInstance
    }),
    pauseAnimation: vi.fn((...args) => {
      pauseAnimation(...args)
      return graphInstance
    }),
    resumeAnimation: vi.fn((...args) => {
      resumeAnimation(...args)
      return graphInstance
    }),
  }

  const createForceGraph = vi.fn(function () {
    return graphInstance
  })

  return {
    graphData,
    onNodeClick,
    onBackgroundClick,
    nodeColor,
    nodeRelSize,
    linkColor,
    linkWidth,
    linkOpacity,
    cameraPosition,
    width,
    height,
    cooldownTicks,
    d3AlphaDecay,
    d3VelocityDecay,
    enableNodeDrag,
    nodeLabel,
    nodeThreeObject,
    linkDirectionalParticles,
    linkDirectionalParticleWidth,
    linkDirectionalParticleSpeed,
    linkDirectionalParticleColor,
    nodeVisibility,
    linkVisibility,
    pauseAnimation,
    resumeAnimation,
    graphInstance,
    createForceGraph,
  }
})

const {
  graphData,
  onNodeClick,
  onBackgroundClick,
  nodeLabel,
  nodeThreeObject,
  graphInstance,
  createForceGraph,
} = forceGraphMocks

vi.mock('3d-force-graph', () => ({
  default: forceGraphMocks.createForceGraph,
}))


import BrainGraphCanvas from './BrainGraphCanvas.vue'

const nodes = [
  { id: 'node-1', name: 'Core Node', type: 'knowledge', importance: 0.95 },
  { id: 'node-2', name: 'Linked Node', type: 'chat', importance: 0.78 },
  { id: 'node-3', name: 'Outer Node', type: 'forum', importance: 0.52 },
]

const edges = [
  { id: 'edge-1', source: 'node-1', target: 'node-2', relation: 'links' },
  { id: 'edge-2', source: 'node-2', target: 'node-3', relation: 'extends' },
]

function mountCanvas(props: Record<string, unknown> = {}) {
  return mount(BrainGraphCanvas, {
    attachTo: document.body,
    props: {
      nodes,
      edges,
      getColor: (type: string) => ({
        knowledge: '#7dffb2',
        chat: '#18f6a8',
        forum: '#4dff7a',
      }[type] || '#7ea892'),
      ...props,
    },
  })
}

describe('BrainGraphCanvas', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.useRealTimers()
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: vi.fn().mockImplementation(() => ({
        matches: false,
        addEventListener: vi.fn(),
        removeEventListener: vi.fn(),
      })),
    })
  })

  it('mounts the 3d force graph into the canvas container', () => {
    const wrapper = mountCanvas()

    expect(wrapper.get('[data-testid="brain-graph-canvas"]')).toBeTruthy()
    expect(createForceGraph).toHaveBeenCalledTimes(1)
    expect(graphInstance.graphData).toHaveBeenCalled()
    expect(graphInstance.nodeLabel).toHaveBeenCalled()
    expect(graphInstance.nodeThreeObject).toHaveBeenCalled()

    const labelAccessor = nodeLabel.mock.calls[0]?.[0]
    expect(typeof labelAccessor).toBe('function')
    expect(labelAccessor?.(nodes[0])).toContain('Core Node')

    const textObjectFactory = nodeThreeObject.mock.calls[0]?.[0]
    expect(typeof textObjectFactory).toBe('function')
    expect(textObjectFactory?.(nodes[0])).toBeTruthy()

    wrapper.unmount()
  })

  it('renders smaller labels close to the node sphere', () => {
    const wrapper = mountCanvas({ selectedNodeId: 'node-1' })
    const textObjectFactory = nodeThreeObject.mock.calls[0]?.[0]

    const defaultVisual = textObjectFactory?.(nodes[2])
    const selectedVisual = textObjectFactory?.(nodes[0])

    expect(defaultVisual).toBeInstanceOf(Group)
    expect(selectedVisual).toBeInstanceOf(Group)

    const defaultLabel = (defaultVisual as Group).children.find((child) => child instanceof Sprite) as Sprite | undefined
    const selectedLabel = (selectedVisual as Group).children.find((child) => child instanceof Sprite) as Sprite | undefined

    expect(defaultLabel).toBeTruthy()
    expect(selectedLabel).toBeTruthy()
    expect(defaultLabel?.position.y).toBeLessThanOrEqual(12)
    expect(defaultLabel?.scale.y).toBeLessThanOrEqual(16)
    expect(selectedLabel?.scale.y).toBeGreaterThanOrEqual(defaultLabel?.scale.y ?? 0)

    wrapper.unmount()
  })

  it('starts from a closer default viewport and resets to it', () => {
    const wrapper = mountCanvas()

    const initialCamera = graphInstance.cameraPosition.mock.calls[0]?.[0]
    expect(initialCamera?.z).toBeLessThan(280)

    wrapper.vm.resetViewport()

    const latestCamera = graphInstance.cameraPosition.mock.calls.at(-1)?.[0]
    expect(latestCamera?.z).toBe(initialCamera?.z)

    wrapper.unmount()
  })

  it('animates the viewport with subtle auto rotation and stops on unmount', () => {
    vi.useFakeTimers()
    const wrapper = mountCanvas()
    const initialCameraCalls = graphInstance.cameraPosition.mock.calls.length

    vi.advanceTimersByTime(2400)

    const duringRotationCalls = graphInstance.cameraPosition.mock.calls.length
    expect(duringRotationCalls).toBeGreaterThan(initialCameraCalls)

    const firstAutoCall = graphInstance.cameraPosition.mock.calls[initialCameraCalls]?.[0]
    const lastAutoCall = graphInstance.cameraPosition.mock.calls.at(-1)?.[0]
    expect(Math.abs((lastAutoCall?.x ?? 0) - (firstAutoCall?.x ?? 0))).toBeLessThanOrEqual(40)
    expect(Math.abs((lastAutoCall?.z ?? 0) - (firstAutoCall?.z ?? 0))).toBeLessThanOrEqual(40)

    wrapper.unmount()
    const callsAfterUnmount = graphInstance.cameraPosition.mock.calls.length

    vi.advanceTimersByTime(2400)
    expect(graphInstance.cameraPosition.mock.calls.length).toBe(callsAfterUnmount)
  })

  it('emits selectNode when the renderer node click callback fires', async () => {
    const wrapper = mountCanvas()
    const handler = onNodeClick.mock.calls[0]?.[0]

    handler?.({ id: 'node-2' })
    await wrapper.vm.$nextTick()

    expect(wrapper.emitted('selectNode')).toEqual([['node-2']])
  })

  it('emits clearSelection when the renderer background callback fires', async () => {
    const wrapper = mountCanvas()
    const handler = onBackgroundClick.mock.calls[0]?.[0]

    handler?.()
    await wrapper.vm.$nextTick()

    expect(wrapper.emitted('clearSelection')).toEqual([[]])
  })

  it('exposes zoomBy and resetViewport methods', () => {
    const wrapper = mountCanvas()

    expect(typeof wrapper.vm.zoomBy).toBe('function')
    expect(typeof wrapper.vm.resetViewport).toBe('function')

    wrapper.vm.zoomBy(1.15)
    wrapper.vm.resetViewport()

    expect(graphInstance.cameraPosition).toHaveBeenCalled()
  })

  it('updates graph data when props change', async () => {
    const wrapper = mountCanvas()
    const updatedNodes = [...nodes, { id: 'node-4', name: 'New Node', type: 'schedule', importance: 0.7 }]

    await wrapper.setProps({ nodes: updatedNodes })

    const latestGraphData = graphData.mock.calls.at(-1)?.[0]
    expect(latestGraphData.nodes).toHaveLength(4)
    expect(latestGraphData.links).toHaveLength(2)
  })

  it('recomputes visibility/highlight accessors when selection changes', async () => {
    const wrapper = mountCanvas()

    await wrapper.setProps({ selectedNodeId: 'node-1' })

    expect(graphInstance.nodeVisibility).toHaveBeenCalled()
    expect(graphInstance.linkVisibility).toHaveBeenCalled()
    expect(graphInstance.linkColor).toHaveBeenCalled()
    expect(graphInstance.linkWidth).toHaveBeenCalled()
    expect(graphInstance.nodeThreeObject).toHaveBeenCalledTimes(2)
  })
})
