import { describe, expect, it, vi } from 'vitest'

import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'

import {
  clearNodePreviewCacheForValues,
  findNodesReferencingValues
} from './clearNodePreviewCacheForValues'

type MockWidget = { name: string; value: unknown }
type MockNode = {
  id: number
  widgets?: MockWidget[]
  imgs?: unknown
  videoContainer?: unknown
  graph?: { setDirtyCanvas: (v: boolean) => void }
  isSubgraphNode?: () => boolean
  subgraph?: { nodes: MockNode[] }
}

function makeGraph(nodes: MockNode[]): LGraph {
  return { nodes } as unknown as LGraph
}

describe('FE-230 clearNodePreviewCacheForValues', () => {
  it('clears node.imgs and removes outputs when a widget value matches a deleted value', () => {
    const setDirty = vi.fn()
    const remove = vi.fn()
    const node: MockNode = {
      id: 7,
      widgets: [{ name: 'image', value: 'foo.png' }],
      imgs: [{ src: 'blob:stale' }],
      graph: { setDirtyCanvas: setDirty }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['foo.png']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(node)
    expect(setDirty).toHaveBeenCalledWith(true)
  })

  it('leaves unrelated nodes untouched', () => {
    const setDirty = vi.fn()
    const remove = vi.fn()
    const node: MockNode = {
      id: 8,
      widgets: [{ name: 'image', value: 'unrelated.png' }],
      imgs: [{ src: 'blob:keep' }],
      graph: { setDirtyCanvas: setDirty }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['foo.png']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toEqual([{ src: 'blob:keep' }])
    expect(remove).not.toHaveBeenCalled()
    expect(setDirty).not.toHaveBeenCalled()
  })

  it('no-ops when the deleted value set is empty', () => {
    const setDirty = vi.fn()
    const remove = vi.fn()
    const node: MockNode = {
      id: 9,
      widgets: [{ name: 'image', value: 'foo.png' }],
      imgs: [{ src: 'blob:keep' }],
      graph: { setDirtyCanvas: setDirty }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toEqual([{ src: 'blob:keep' }])
    expect(remove).not.toHaveBeenCalled()
    expect(setDirty).not.toHaveBeenCalled()
  })

  it('matches the [output]-annotated form for output assets', () => {
    const remove = vi.fn()
    const node: MockNode = {
      id: 12,
      widgets: [{ name: 'image', value: 'foo.png [output]' }],
      imgs: [{ src: 'blob:stale' }],
      graph: { setDirtyCanvas: vi.fn() }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['foo.png [output]']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(node)
  })

  it('matches the subfolder-prefixed annotated form when provided', () => {
    const remove = vi.fn()
    const node: MockNode = {
      id: 13,
      widgets: [{ name: 'image', value: 'sub/foo.png [output]' }],
      imgs: [{ src: 'blob:stale' }],
      graph: { setDirtyCanvas: vi.fn() }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['sub/foo.png [output]']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(node)
  })

  it('does not cross-match basenames across input/output sources', () => {
    const remove = vi.fn()
    const inputNode: MockNode = {
      id: 1,
      widgets: [{ name: 'image', value: 'foo.png' }],
      imgs: [{ src: 'blob:input' }],
      graph: { setDirtyCanvas: vi.fn() }
    }
    const outputNode: MockNode = {
      id: 2,
      widgets: [{ name: 'image', value: 'foo.png [output]' }],
      imgs: [{ src: 'blob:output' }],
      graph: { setDirtyCanvas: vi.fn() }
    }

    clearNodePreviewCacheForValues(
      makeGraph([inputNode, outputNode]),
      new Set(['foo.png']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(inputNode.imgs).toBeUndefined()
    expect(outputNode.imgs).toEqual([{ src: 'blob:output' }])
    expect(remove).toHaveBeenCalledWith(inputNode)
    expect(remove).not.toHaveBeenCalledWith(outputNode)
  })

  it('also clears videoContainer for video previews', () => {
    const remove = vi.fn()
    const node: MockNode = {
      id: 15,
      widgets: [{ name: 'video', value: 'clip.mp4' }],
      videoContainer: { foo: 'bar' },
      graph: { setDirtyCanvas: vi.fn() }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['clip.mp4']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.videoContainer).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(node)
  })

  it('matches any widget on the node, not just "image"', () => {
    const remove = vi.fn()
    const node: MockNode = {
      id: 10,
      widgets: [
        { name: 'seed', value: 42 },
        { name: 'video', value: 'clip.mp4' }
      ],
      imgs: [{ src: 'blob:videostale' }],
      graph: { setDirtyCanvas: vi.fn() }
    }

    clearNodePreviewCacheForValues(
      makeGraph([node]),
      new Set(['clip.mp4']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(node.imgs).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(node)
  })

  it('walks subgraph interiors and matches nested nodes', () => {
    const inner: MockNode = {
      id: 100,
      widgets: [{ name: 'image', value: 'nested.png [output]' }],
      imgs: [{ src: 'blob:nested' }],
      graph: { setDirtyCanvas: vi.fn() }
    }
    const wrapper: MockNode = {
      id: 50,
      widgets: [],
      isSubgraphNode: () => true,
      subgraph: { nodes: [inner] }
    }
    const remove = vi.fn()

    clearNodePreviewCacheForValues(
      makeGraph([wrapper]),
      new Set(['nested.png [output]']),
      remove as unknown as (node: LGraphNode) => void
    )

    expect(inner.imgs).toBeUndefined()
    expect(remove).toHaveBeenCalledWith(inner)
  })
})

describe('FE-230 findNodesReferencingValues', () => {
  it('skips subgraph wrapper nodes (only their interior nodes match)', () => {
    const inner: MockNode = {
      id: 100,
      widgets: [{ name: 'image', value: 'foo.png' }]
    }
    const wrapper: MockNode = {
      id: 50,
      widgets: [{ name: 'image', value: 'foo.png' }],
      isSubgraphNode: () => true,
      subgraph: { nodes: [inner] }
    }

    const matches = findNodesReferencingValues(
      makeGraph([wrapper]),
      new Set(['foo.png'])
    )

    expect(matches).toEqual([inner])
  })
})
