/**
 * SubgraphSerialization Tests
 *
 * Tests for saving, loading, and version compatibility of subgraphs.
 * This covers serialization, deserialization, data integrity, and migration scenarios.
 */
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'

import { duplicateSubgraphNodeIds } from '@/lib/litegraph/src/__fixtures__/duplicateSubgraphNodeIds'
import {
  LGraph,
  LGraphNode,
  LiteGraph,
  Subgraph
} from '@/lib/litegraph/src/litegraph'
import type { ISlotType } from '@/lib/litegraph/src/litegraph'

import {
  createTestSubgraph,
  createTestSubgraphNode,
  resetSubgraphFixtureState
} from './__fixtures__/subgraphHelpers'

class DummyNode extends LGraphNode {}

const DUPLICATE_ID_SUBGRAPH_A = '11111111-1111-4111-8111-111111111111'
const DUPLICATE_ID_SUBGRAPH_B = '22222222-2222-4222-8222-222222222222'

function createRegisteredNode(
  graph: LGraph | Subgraph,
  inputs: ISlotType[] = [],
  outputs: ISlotType[] = [],
  title?: string
) {
  const type = JSON.stringify({ inputs, outputs })
  if (!LiteGraph.registered_node_types[type]) {
    class testnode extends LGraphNode {
      constructor(title: string) {
        super(title)
        let i = 0
        for (const input of inputs) this.addInput('input_' + i++, input)
        let o = 0
        for (const output of outputs) this.addOutput('output_' + o++, output)
      }
    }
    LiteGraph.registered_node_types[type] = testnode
  }
  const node = LiteGraph.createNode(type, title)
  if (!node) throw new Error('Failed to create node')
  graph.add(node)
  return node
}

beforeEach(() => {
  setActivePinia(createTestingPinia({ stubActions: false }))
  resetSubgraphFixtureState()
  LiteGraph.registerNodeType('dummy', DummyNode)
})

afterEach(() => {
  delete LiteGraph.registered_node_types.dummy
})

describe('SubgraphSerialization - Basic Serialization', () => {
  it('should save and load simple subgraphs', () => {
    const original = createTestSubgraph({
      name: 'Simple Test',
      nodeCount: 2
    })
    original.addInput('in1', 'number')
    original.addInput('in2', 'string')
    original.addOutput('out', 'boolean')

    // Serialize
    const exported = original.asSerialisable()

    // Verify exported structure
    expect(exported).toHaveProperty('id', original.id)
    expect(exported).toHaveProperty('name', 'Simple Test')
    expect(exported).toHaveProperty('nodes')
    expect(exported).toHaveProperty('links')
    expect(exported).toHaveProperty('inputs')
    expect(exported).toHaveProperty('outputs')
    expect(exported).toHaveProperty('version')

    // Create new instance from serialized data
    const restored = new Subgraph(new LGraph(), exported)

    // Verify structure is preserved
    expect(restored.id).toBe(original.id)
    expect(restored.name).toBe(original.name)
    expect(restored.inputs.length).toBe(2) // Only added inputs, not original nodeCount
    expect(restored.outputs.length).toBe(1)
    // Note: nodes may not be restored if they're not registered types
    // This is expected behavior - serialization preserves I/O but nodes need valid types

    // Verify input details
    expect(restored.inputs[0].name).toBe('in1')
    expect(restored.inputs[0].type).toBe('number')
    expect(restored.inputs[1].name).toBe('in2')
    expect(restored.inputs[1].type).toBe('string')
    expect(restored.outputs[0].name).toBe('out')
    expect(restored.outputs[0].type).toBe('boolean')
  })

  it('should verify all properties are preserved', () => {
    const original = createTestSubgraph({
      name: 'Property Test',
      nodeCount: 3,
      inputs: [
        { name: 'input1', type: 'number' },
        { name: 'input2', type: 'string' }
      ],
      outputs: [
        { name: 'output1', type: 'boolean' },
        { name: 'output2', type: 'array' }
      ]
    })

    const exported = original.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)

    // Verify core properties
    expect(restored.id).toBe(original.id)
    expect(restored.name).toBe(original.name)
    expect(restored.description).toBe(original.description)

    // Verify I/O structure
    expect(restored.inputs.length).toBe(original.inputs.length)
    expect(restored.outputs.length).toBe(original.outputs.length)
    // Nodes may not be restored if they don't have registered types

    // Verify I/O details match
    for (let i = 0; i < original.inputs.length; i++) {
      expect(restored.inputs[i].name).toBe(original.inputs[i].name)
      expect(restored.inputs[i].type).toBe(original.inputs[i].type)
    }

    for (let i = 0; i < original.outputs.length; i++) {
      expect(restored.outputs[i].name).toBe(original.outputs[i].name)
      expect(restored.outputs[i].type).toBe(original.outputs[i].type)
    }
  })

  it('should test export() and configure() methods', () => {
    const subgraph = createTestSubgraph({ nodeCount: 1 })
    subgraph.addInput('test_input', 'number')
    subgraph.addOutput('test_output', 'string')

    // Test export
    const exported = subgraph.asSerialisable()
    expect(exported).toHaveProperty('id')
    expect(exported).toHaveProperty('nodes')
    expect(exported).toHaveProperty('links')
    expect(exported).toHaveProperty('inputs')
    expect(exported).toHaveProperty('outputs')

    // Test configure with partial data
    const newSubgraph = createTestSubgraph({ nodeCount: 0 })
    expect(() => {
      newSubgraph.configure(exported)
    }).not.toThrow()

    // Verify configuration applied
    expect(newSubgraph.inputs.length).toBe(1)
    expect(newSubgraph.outputs.length).toBe(1)
    expect(newSubgraph.inputs[0].name).toBe('test_input')
    expect(newSubgraph.outputs[0].name).toBe('test_output')
  })
})

describe('SubgraphSerialization - Complex Serialization', () => {
  it('should serialize nested subgraphs with multiple levels', () => {
    // Create a nested structure
    const childSubgraph = createTestSubgraph({
      name: 'Child',
      nodeCount: 2,
      inputs: [{ name: 'child_in', type: 'number' }],
      outputs: [{ name: 'child_out', type: 'string' }]
    })

    const parentSubgraph = createTestSubgraph({
      name: 'Parent',
      nodeCount: 1,
      inputs: [{ name: 'parent_in', type: 'boolean' }],
      outputs: [{ name: 'parent_out', type: 'array' }]
    })

    // Add child to parent
    const childInstance = createTestSubgraphNode(childSubgraph, { id: 100 })
    parentSubgraph.add(childInstance)

    // Serialize both
    const childExported = childSubgraph.asSerialisable()
    const parentExported = parentSubgraph.asSerialisable()

    // Verify both can be serialized
    expect(childExported).toHaveProperty('name', 'Child')
    expect(parentExported).toHaveProperty('name', 'Parent')
    expect(parentExported.nodes.length).toBe(2) // 1 original + 1 child subgraph

    // Restore and verify
    const restoredChild = new Subgraph(new LGraph(), childExported)
    const restoredParent = new Subgraph(new LGraph(), parentExported)

    expect(restoredChild.name).toBe('Child')
    expect(restoredParent.name).toBe('Parent')
    expect(restoredChild.inputs.length).toBe(1)
    expect(restoredParent.inputs.length).toBe(1)
  })

  it('should serialize subgraphs with many nodes and connections', () => {
    const largeSubgraph = createTestSubgraph({
      name: 'Large Subgraph',
      nodeCount: 10 // Many nodes
    })

    // Add many I/O slots
    for (let i = 0; i < 5; i++) {
      largeSubgraph.addInput(`input_${i}`, 'number')
      largeSubgraph.addOutput(`output_${i}`, 'string')
    }

    const exported = largeSubgraph.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)

    // Verify I/O data preserved
    expect(restored.inputs.length).toBe(5)
    expect(restored.outputs.length).toBe(5)
    // Nodes may not be restored if they don't have registered types

    // Verify I/O naming preserved
    for (let i = 0; i < 5; i++) {
      expect(restored.inputs[i].name).toBe(`input_${i}`)
      expect(restored.outputs[i].name).toBe(`output_${i}`)
    }
  })

  it('should preserve I/O even when nodes are not restored', () => {
    const subgraph = createTestSubgraph({
      nodeCount: 2,
      inputs: [{ name: 'data_in', type: 'number' }],
      outputs: [{ name: 'data_out', type: 'string' }]
    })

    const exported = subgraph.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)

    // Nodes are not restored without registered types
    expect(restored.nodes).toHaveLength(0)

    // I/O is still preserved
    expect(restored.inputs).toHaveLength(1)
    expect(restored.inputs[0].name).toBe('data_in')
    expect(restored.outputs).toHaveLength(1)
    expect(restored.outputs[0].name).toBe('data_out')
  })
})

describe('SubgraphSerialization - Version Compatibility', () => {
  it('should handle version field in exports', () => {
    const subgraph = createTestSubgraph({ nodeCount: 1 })
    const exported = subgraph.asSerialisable()

    // Should have version field
    expect(exported).toHaveProperty('version')
    expect(typeof exported.version).toBe('number')
  })

  it('should load version 1.0+ format', () => {
    const modernFormat = {
      version: 1, // Number as expected by current implementation
      id: 'test-modern-id',
      name: 'Modern Subgraph',
      nodes: [],
      links: {},
      groups: [],
      config: {},
      definitions: { subgraphs: [] },
      inputs: [{ id: 'input-id', name: 'modern_input', type: 'number' }],
      outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }],
      inputNode: {
        id: -10,
        bounding: [0, 0, 120, 60]
      },
      outputNode: {
        id: -20,
        bounding: [300, 0, 120, 60]
      },
      widgets: []
    }

    expect(() => {
      // @ts-expect-error Type mismatch in ExportedSubgraph format
      const subgraph = new Subgraph(new LGraph(), modernFormat)
      expect(subgraph.name).toBe('Modern Subgraph')
      expect(subgraph.inputs.length).toBe(1)
      expect(subgraph.outputs.length).toBe(1)
    }).not.toThrow()
  })

  it('should handle missing fields gracefully', () => {
    const incompleteFormat = {
      version: 1,
      id: 'incomplete-id',
      name: 'Incomplete Subgraph',
      nodes: [],
      links: {},
      groups: [],
      config: {},
      definitions: { subgraphs: [] },
      inputNode: {
        id: -10,
        bounding: [0, 0, 120, 60]
      },
      outputNode: {
        id: -20,
        bounding: [300, 0, 120, 60]
      }
      // Missing optional: inputs, outputs, widgets
    }

    expect(() => {
      // @ts-expect-error Type mismatch in ExportedSubgraph format
      const subgraph = new Subgraph(new LGraph(), incompleteFormat)
      expect(subgraph.name).toBe('Incomplete Subgraph')
      // Should have default empty arrays
      expect(Array.isArray(subgraph.inputs)).toBe(true)
      expect(Array.isArray(subgraph.outputs)).toBe(true)
    }).not.toThrow()
  })

  it('should consider future-proofing', () => {
    const futureFormat = {
      version: 2, // Future version (number)
      id: 'future-id',
      name: 'Future Subgraph',
      nodes: [],
      links: {},
      groups: [],
      config: {},
      definitions: { subgraphs: [] },
      inputs: [],
      outputs: [],
      inputNode: {
        id: -10,
        bounding: [0, 0, 120, 60]
      },
      outputNode: {
        id: -20,
        bounding: [300, 0, 120, 60]
      },
      widgets: [],
      futureFeature: 'unknown_data' // Unknown future field
    }

    // Should handle future format gracefully
    expect(() => {
      // @ts-expect-error Type mismatch in ExportedSubgraph format
      const subgraph = new Subgraph(new LGraph(), futureFormat)
      expect(subgraph.name).toBe('Future Subgraph')
    }).not.toThrow()
  })
})

describe('SubgraphSerialization - Data Integrity', () => {
  it('should pass round-trip testing (save → load → save → compare)', () => {
    const original = createTestSubgraph({
      name: 'Round Trip Test',
      nodeCount: 3,
      inputs: [
        { name: 'rt_input1', type: 'number' },
        { name: 'rt_input2', type: 'string' }
      ],
      outputs: [{ name: 'rt_output1', type: 'boolean' }]
    })

    // First round trip
    const exported1 = original.asSerialisable()
    const restored1 = new Subgraph(new LGraph(), exported1)

    // Second round trip
    const exported2 = restored1.asSerialisable()
    const restored2 = new Subgraph(new LGraph(), exported2)

    // Compare key properties
    expect(restored2.id).toBe(original.id)
    expect(restored2.name).toBe(original.name)
    expect(restored2.inputs.length).toBe(original.inputs.length)
    expect(restored2.outputs.length).toBe(original.outputs.length)
    // Nodes may not be restored if they don't have registered types

    // Compare I/O details
    for (let i = 0; i < original.inputs.length; i++) {
      expect(restored2.inputs[i].name).toBe(original.inputs[i].name)
      expect(restored2.inputs[i].type).toBe(original.inputs[i].type)
    }

    for (let i = 0; i < original.outputs.length; i++) {
      expect(restored2.outputs[i].name).toBe(original.outputs[i].name)
      expect(restored2.outputs[i].type).toBe(original.outputs[i].type)
    }
  })

  it('should verify IDs remain unique', () => {
    const subgraph1 = createTestSubgraph({ name: 'Unique1', nodeCount: 2 })
    const subgraph2 = createTestSubgraph({ name: 'Unique2', nodeCount: 2 })

    const exported1 = subgraph1.asSerialisable()
    const exported2 = subgraph2.asSerialisable()

    // IDs should be unique
    expect(exported1.id).not.toBe(exported2.id)

    const restored1 = new Subgraph(new LGraph(), exported1)
    const restored2 = new Subgraph(new LGraph(), exported2)

    expect(restored1.id).not.toBe(restored2.id)
    expect(restored1.id).toBe(subgraph1.id)
    expect(restored2.id).toBe(subgraph2.id)
  })

  it('should maintain connection integrity after load', () => {
    const subgraph = createTestSubgraph({ nodeCount: 2 })
    subgraph.addInput('connection_test', 'number')
    subgraph.addOutput('connection_result', 'string')

    const exported = subgraph.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)

    // Verify I/O connections can be established
    expect(restored.inputs.length).toBe(1)
    expect(restored.outputs.length).toBe(1)
    expect(restored.inputs[0].name).toBe('connection_test')
    expect(restored.outputs[0].name).toBe('connection_result')

    // Verify subgraph can be instantiated
    const instance = createTestSubgraphNode(restored)
    expect(instance.inputs.length).toBe(1)
    expect(instance.outputs.length).toBe(1)
  })

  it('should not restore nodes without registered types', () => {
    const subgraph = createTestSubgraph({ nodeCount: 2 })

    // Nodes exist before serialization
    expect(subgraph.nodes).toHaveLength(2)

    const exported = subgraph.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)

    // Nodes are not restored without registered types
    expect(restored.nodes).toHaveLength(0)
  })

  it('should preserve interior link structure through serialization', () => {
    const subgraph = createTestSubgraph({ nodeCount: 0 })

    const nodeA = createRegisteredNode(subgraph, [], ['number'], 'A')
    const nodeB = createRegisteredNode(subgraph, ['number'], ['string'], 'B')
    const nodeC = createRegisteredNode(subgraph, ['string'], [], 'C')

    nodeA.connect(0, nodeB, 0)
    nodeB.connect(0, nodeC, 0)

    expect(subgraph.nodes).toHaveLength(3)
    expect(subgraph.links.size).toBe(2)

    const exported = subgraph.asSerialisable()
    const restored = new Subgraph(new LGraph(), exported)
    restored.configure(exported)

    expect(restored.nodes).toHaveLength(3)
    expect(restored.links.size).toBe(2)

    for (const [, link] of restored.links) {
      const originNode = restored.getNodeById(link.origin_id)
      const targetNode = restored.getNodeById(link.target_id)
      expect(originNode).toBeDefined()
      expect(targetNode).toBeDefined()
      expect(link.origin_slot).toBeGreaterThanOrEqual(0)
      expect(link.target_slot).toBeGreaterThanOrEqual(0)
      expect(originNode!.outputs[link.origin_slot]).toBeDefined()
      expect(targetNode!.inputs[link.target_slot]).toBeDefined()
    }
  })

  it('deduplicates duplicate subgraph node IDs while keeping root nodes canonical', () => {
    const graph = new LGraph()
    graph.configure(structuredClone(duplicateSubgraphNodeIds))

    const rootIds = graph.nodes
      .map((node) => node.id)
      .filter((id): id is number => typeof id === 'number')
      .sort((a, b) => a - b)
    expect(rootIds).toEqual([102, 103])

    const subgraphAIds = new Set(
      graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_A)!.nodes.map((node) => node.id)
    )
    const subgraphBIds = new Set(
      graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!.nodes.map((node) => node.id)
    )

    expect(subgraphAIds).toEqual(new Set([3, 8, 37]))
    for (const id of subgraphAIds) {
      expect(subgraphBIds.has(id)).toBe(false)
    }
  })

  it('patches remapped link and proxyWidget references during duplicate-ID hydration', () => {
    const graph = new LGraph()
    graph.configure(structuredClone(duplicateSubgraphNodeIds))

    const subgraphAIds = new Set(
      graph.subgraphs
        .get(DUPLICATE_ID_SUBGRAPH_A)!
        .nodes.map((node) => String(node.id))
    )
    const subgraphB = graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!
    const subgraphBIds = new Set(subgraphB.nodes.map((node) => String(node.id)))

    const rootProxyWidgetsA = graph.getNodeById(102)?.properties?.proxyWidgets
    expect(Array.isArray(rootProxyWidgetsA)).toBe(true)
    for (const entry of rootProxyWidgetsA as string[][]) {
      expect(subgraphAIds.has(String(entry[0]))).toBe(true)
    }

    const rootProxyWidgetsB = graph.getNodeById(103)?.properties?.proxyWidgets
    expect(Array.isArray(rootProxyWidgetsB)).toBe(true)
    for (const entry of rootProxyWidgetsB as string[][]) {
      expect(subgraphBIds.has(String(entry[0]))).toBe(true)
    }

    for (const [, link] of subgraphB.links) {
      expect(subgraphBIds.has(String(link.origin_id))).toBe(true)
      expect(subgraphBIds.has(String(link.target_id))).toBe(true)
    }
  })
})
