import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, test, vi } from 'vitest'

import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import {
  createTestSubgraph,
  createTestSubgraphNode
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'

vi.mock('@/renderer/core/canvas/canvasStore', () => ({
  useCanvasStore: () => ({})
}))
vi.mock('@/stores/domWidgetStore', () => ({
  useDomWidgetStore: () => ({ widgetStates: new Map() })
}))
vi.mock('@/services/litegraphService', () => ({
  useLitegraphService: () => ({ updatePreviews: () => ({}) })
}))

function createOuterSubgraphSetup(inputNames: string[]): {
  outerSubgraph: Subgraph
  outerSubgraphNode: SubgraphNode
} {
  const outerSubgraph = createTestSubgraph({
    inputs: inputNames.map((name) => ({ name, type: '*' }))
  })
  const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, { id: 1 })
  return { outerSubgraph, outerSubgraphNode }
}

function addLinkedNestedSubgraphNode(
  outerSubgraph: Subgraph,
  inputName: string,
  linkedInputName: string,
  options: { widget?: string } = {}
): { innerSubgraphNode: SubgraphNode } {
  const innerSubgraph = createTestSubgraph({
    inputs: [{ name: linkedInputName, type: '*' }]
  })
  const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, { id: 819 })
  outerSubgraph.add(innerSubgraphNode)

  const inputSlot = outerSubgraph.inputNode.slots.find(
    (slot) => slot.name === inputName
  )
  if (!inputSlot) throw new Error(`Missing subgraph input slot: ${inputName}`)

  const input = innerSubgraphNode.addInput(linkedInputName, '*')
  if (options.widget) {
    innerSubgraphNode.addWidget('number', options.widget, 0, () => undefined)
    input.widget = { name: options.widget }
  }
  inputSlot.connect(input, innerSubgraphNode)

  if (input.link == null) {
    throw new Error(`Expected link to be created for input ${linkedInputName}`)
  }

  return { innerSubgraphNode }
}

beforeEach(() => {
  setActivePinia(createTestingPinia({ stubActions: false }))
  vi.clearAllMocks()
})

describe('resolveSubgraphInputTarget', () => {
  test('returns target for widget-backed input on nested SubgraphNode', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'width'
    ])
    addLinkedNestedSubgraphNode(outerSubgraph, 'width', 'width', {
      widget: 'width'
    })

    const result = resolveSubgraphInputTarget(outerSubgraphNode, 'width')

    expect(result).toMatchObject({
      nodeId: '819',
      widgetName: 'width'
    })
  })

  test('resolves non-widget input on nested SubgraphNode to immediate child target', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'audio'
    ])
    addLinkedNestedSubgraphNode(outerSubgraph, 'audio', 'audio')

    const result = resolveSubgraphInputTarget(outerSubgraphNode, 'audio')

    expect(result).toMatchObject({
      nodeId: '819',
      widgetName: 'audio'
    })
  })

  test('resolves both widget and non-widget inputs on nested SubgraphNodes', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'width',
      'audio'
    ])
    const { innerSubgraphNode: widthChild } = addLinkedNestedSubgraphNode(
      outerSubgraph,
      'width',
      'width',
      { widget: 'width' }
    )
    const { innerSubgraphNode: audioChild } = addLinkedNestedSubgraphNode(
      outerSubgraph,
      'audio',
      'audio'
    )

    expect(
      resolveSubgraphInputTarget(outerSubgraphNode, 'width')
    ).toMatchObject({
      nodeId: String(widthChild.id),
      widgetName: 'width'
    })
    expect(
      resolveSubgraphInputTarget(outerSubgraphNode, 'audio')
    ).toMatchObject({
      nodeId: String(audioChild.id),
      widgetName: 'audio'
    })
  })

  test('returns target for widget-backed input on plain interior node', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'seed'
    ])

    const inputSlot = outerSubgraph.inputNode.slots.find(
      (slot) => slot.name === 'seed'
    )!
    const node = new LGraphNode('Interior-seed')
    node.id = 42
    const input = node.addInput('seed_input', '*')
    node.addWidget('number', 'seed', 0, () => undefined)
    input.widget = { name: 'seed' }
    outerSubgraph.add(node)
    inputSlot.connect(input, node)

    const result = resolveSubgraphInputTarget(outerSubgraphNode, 'seed')

    expect(result).toMatchObject({
      nodeId: '42',
      widgetName: 'seed'
    })
  })

  test('returns undefined for non-widget input on plain interior node', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'image'
    ])

    const inputSlot = outerSubgraph.inputNode.slots.find(
      (slot) => slot.name === 'image'
    )!
    const node = new LGraphNode('Interior-image')
    const input = node.addInput('image_input', '*')
    outerSubgraph.add(node)
    inputSlot.connect(input, node)

    const result = resolveSubgraphInputTarget(outerSubgraphNode, 'image')

    expect(result).toBeUndefined()
  })

  test('resolves widget and non-widget inputs on the same nested SubgraphNode independently', () => {
    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'width',
      'audio'
    ])

    const innerSubgraph = createTestSubgraph({
      inputs: [
        { name: 'width', type: '*' },
        { name: 'audio', type: '*' }
      ]
    })
    const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
      id: 820
    })
    outerSubgraph.add(innerSubgraphNode)

    const widthInput = innerSubgraphNode.addInput('width', '*')
    innerSubgraphNode.addWidget('number', 'width', 0, () => undefined)
    widthInput.widget = { name: 'width' }

    const audioInput = innerSubgraphNode.addInput('audio', '*')

    const widthSlot = outerSubgraph.inputNode.slots.find(
      (slot) => slot.name === 'width'
    )!
    const audioSlot = outerSubgraph.inputNode.slots.find(
      (slot) => slot.name === 'audio'
    )!
    widthSlot.connect(widthInput, innerSubgraphNode)
    audioSlot.connect(audioInput, innerSubgraphNode)

    expect(
      resolveSubgraphInputTarget(outerSubgraphNode, 'width')
    ).toMatchObject({
      nodeId: '820',
      widgetName: 'width'
    })
    expect(
      resolveSubgraphInputTarget(outerSubgraphNode, 'audio')
    ).toMatchObject({
      nodeId: '820',
      widgetName: 'audio'
    })
  })

  test('three-level nesting returns immediate child target, not deepest', () => {
    // outer → middle → inner (concrete)
    const innerSubgraph = createTestSubgraph({
      inputs: [{ name: 'seed', type: '*' }]
    })
    const concreteNode = new LGraphNode('ConcreteNode')
    concreteNode.id = 900
    const concreteInput = concreteNode.addInput('seed_input', '*')
    concreteNode.addWidget('number', 'seed', 0, () => undefined)
    concreteInput.widget = { name: 'seed' }
    innerSubgraph.add(concreteNode)
    innerSubgraph.inputNode.slots[0].connect(concreteInput, concreteNode)

    const middleSubgraph = createTestSubgraph({
      inputs: [{ name: 'seed', type: '*' }]
    })
    const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
      id: 901
    })
    middleSubgraph.add(innerSubgraphNode)
    const middleInput = innerSubgraphNode.addInput('seed', '*')
    innerSubgraphNode.addWidget('number', 'seed', 0, () => undefined)
    middleInput.widget = { name: 'seed' }
    middleSubgraph.inputNode.slots[0].connect(middleInput, innerSubgraphNode)

    const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
      'seed'
    ])
    const middleSubgraphNode = createTestSubgraphNode(middleSubgraph, {
      id: 902
    })
    outerSubgraph.add(middleSubgraphNode)
    const outerInput = middleSubgraphNode.addInput('seed', '*')
    middleSubgraphNode.addWidget('number', 'seed', 0, () => undefined)
    outerInput.widget = { name: 'seed' }
    outerSubgraph.inputNode.slots[0].connect(outerInput, middleSubgraphNode)

    const result = resolveSubgraphInputTarget(outerSubgraphNode, 'seed')

    // Should return the immediate child (middle), not the deepest (concrete)
    expect(result).toMatchObject({
      nodeId: '902',
      widgetName: 'seed'
    })
  })
})
