import test from 'node:test';
import assert from 'node:assert/strict';

import {
  serializeExecutionGraph,
  getAutoRunnableNodes,
  hasBlockingAutoRunInput,
} from '../src/executionGraph.js';

test('serializeExecutionGraph excludes isolated nodes from the backend prompt', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { filename: 'scan.gwy' },
      },
    },
    {
      id: '2',
      data: {
        className: 'PreviewImage',
        definition: {
          input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } },
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
    {
      id: '3',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::input::ANNOTATION_SOURCE',
    },
  ];

  const prompt = serializeExecutionGraph(nodes, edges);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'Image',
      inputs: { filename: 'scan.gwy' },
    },
    '2': {
      class_type: 'PreviewImage',
      inputs: { input: ['1', 0] },
    },
  });
  assert.equal('3' in prompt, false);
});

test('serializeExecutionGraph includes isolated preview-load nodes alongside connected subgraphs', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { filename: 'first.gwy' },
      },
    },
    {
      id: '2',
      data: {
        className: 'PreviewImage',
        definition: {
          input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } },
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
    {
      id: '3',
      data: {
        className: 'ImageDemo',
        definition: {
          input: { required: { name: [['demo.npy'], {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { name: 'demo.npy' },
      },
    },
    {
      id: '4',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { filename: '' },
      },
    },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::input::ANNOTATION_SOURCE',
    },
  ];

  const prompt = serializeExecutionGraph(nodes, edges);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'Image',
      inputs: { filename: 'first.gwy' },
    },
    '2': {
      class_type: 'PreviewImage',
      inputs: { input: ['1', 0] },
    },
    '3': {
      class_type: 'ImageDemo',
      inputs: { name: 'demo.npy' },
    },
  });
  assert.equal('4' in prompt, false);
});

test('serializeExecutionGraph allows a singleton Image graph so previews can run', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { filename: 'scan.gwy' },
      },
    },
  ];

  const prompt = serializeExecutionGraph(nodes, []);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'Image',
      inputs: { filename: 'scan.gwy' },
    },
  });
});

test('serializeExecutionGraph allows a singleton ImageDemo graph so previews can run', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'ImageDemo',
        definition: {
          input: { required: { name: [['demo.npy'], {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { name: 'demo.npy' },
      },
    },
  ];

  const prompt = serializeExecutionGraph(nodes, []);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'ImageDemo',
      inputs: { name: 'demo.npy' },
    },
  });
});

test('serializeExecutionGraph ignores group shells and resolves collapsed proxy edges back to child endpoints', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'Image',
        definition: {
          input: { required: { filename: ['FILE_PICKER', {}] }, optional: {} },
          manual_trigger: false,
        },
        widgetValues: { filename: 'scan.gwy' },
      },
    },
    {
      id: '10',
      data: {
        className: 'Group',
        definition: null,
        widgetValues: {},
      },
    },
    {
      id: '2',
      parentId: '10',
      hidden: true,
      data: {
        className: 'PreviewImage',
        definition: {
          input: { required: {}, optional: { input: ['ANNOTATION_SOURCE', {}] } },
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
  ];

  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '10',
      targetHandle: 'group-proxy::in::2::ANNOTATION_SOURCE::input%3A%3Ainput%3A%3AANNOTATION_SOURCE',
      data: {
        groupProxyOwner: '10',
        groupProxyOriginal: {
          target: '2',
          targetHandle: 'input::input::ANNOTATION_SOURCE',
        },
      },
    },
  ];

  const prompt = serializeExecutionGraph(nodes, edges);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'Image',
      inputs: { filename: 'scan.gwy' },
    },
    '2': {
      class_type: 'PreviewImage',
      inputs: { input: ['1', 0] },
    },
  });
  assert.equal('10' in prompt, false);
});

test('serializeExecutionGraph keeps only the View3D viewport snapshot, not camera pose', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'FieldSource',
        definition: {
          input: { required: {}, optional: {} },
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
    {
      id: '2',
      data: {
        className: 'View3D',
        definition: {
          input: {
            required: {
              field: ['DATA_FIELD', {}],
              camera_azimuth: ['FLOAT', {}],
              camera_polar: ['FLOAT', {}],
              camera_distance: ['FLOAT', {}],
              camera_target_x: ['FLOAT', {}],
              camera_target_y: ['FLOAT', {}],
              camera_target_z: ['FLOAT', {}],
              viewport_snapshot: ['STRING', {}],
            },
            optional: {},
          },
          manual_trigger: false,
        },
        widgetValues: {
          camera_azimuth: 0,
          camera_polar: 1.1,
          camera_distance: 1.8,
          camera_target_x: 0,
          camera_target_y: 0,
          camera_target_z: 0,
          viewport_snapshot: '',
        },
        runtimeValues: {
          camera_azimuth: 0.4,
          camera_polar: 1.3,
          camera_distance: 2.6,
          camera_target_x: 99,
          camera_target_y: 88,
          camera_target_z: 77,
          viewport_snapshot: 'data:image/png;base64,abc',
        },
      },
    },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::field::DATA_FIELD',
    },
  ];

  const prompt = serializeExecutionGraph(nodes, edges);

  assert.deepEqual(prompt['2'], {
    class_type: 'View3D',
    inputs: {
      field: ['1', 0],
      viewport_snapshot: 'data:image/png;base64,abc',
    },
  });
});

test('getAutoRunnableNodes ignores disconnected nodes when deciding what can auto-run', () => {
  const nodes = [
    { id: '1', data: { definition: {}, widgetValues: {} } },
    { id: '2', data: { definition: {}, widgetValues: {} } },
    { id: '3', data: { definition: {}, widgetValues: {} } },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::field::DATA_FIELD',
    },
  ];

  const runnable = getAutoRunnableNodes(nodes, edges);

  assert.deepEqual(runnable.map((node) => node.id), ['1', '2']);
});

test('getAutoRunnableNodes includes isolated preview-load nodes with selections', () => {
  const nodes = [
    { id: '1', data: { className: 'Image', definition: {}, widgetValues: { filename: 'first.gwy' } } },
    { id: '2', data: { className: 'PreviewImage', definition: {}, widgetValues: {} } },
    { id: '3', data: { className: 'ImageDemo', definition: {}, widgetValues: { name: 'demo.npy' } } },
    { id: '4', data: { className: 'Image', definition: {}, widgetValues: { filename: '' } } },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::input::ANNOTATION_SOURCE',
    },
  ];

  const runnable = getAutoRunnableNodes(nodes, edges);

  assert.deepEqual(runnable.map((node) => node.id), ['1', '2', '3']);
});

test('getAutoRunnableNodes allows a singleton Image graph', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'Image',
        definition: {},
        widgetValues: { filename: 'scan.gwy' },
      },
    },
  ];

  const runnable = getAutoRunnableNodes(nodes, []);

  assert.deepEqual(runnable.map((node) => node.id), ['1']);
});

test('getAutoRunnableNodes allows a singleton ImageDemo graph', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'ImageDemo',
        definition: {},
        widgetValues: { name: 'demo.npy' },
      },
    },
  ];

  const runnable = getAutoRunnableNodes(nodes, []);

  assert.deepEqual(runnable.map((node) => node.id), ['1']);
});

test('hasBlockingAutoRunInput only blocks connected nodes with incomplete required inputs', () => {
  const node = {
    id: '2',
    data: {
      definition: {
        manual_trigger: false,
        input: {
          required: {
            field: ['DATA_FIELD', {}],
            filename: ['FILE_PICKER', {}],
          },
        },
      },
      widgetValues: { filename: '' },
    },
  };
  const completeEdges = [
    {
      source: '1',
      sourceHandle: 'output::0::DATA_FIELD',
      target: '2',
      targetHandle: 'input::field::DATA_FIELD',
    },
  ];

  assert.equal(hasBlockingAutoRunInput(node, completeEdges), true);
  assert.equal(
    hasBlockingAutoRunInput(
      {
        ...node,
        data: {
          ...node.data,
          widgetValues: { filename: 'scan.gwy' },
        },
      },
      completeEdges,
    ),
    false,
  );
});

test('hasBlockingAutoRunInput skips required file widgets when a connected socket overrides them', () => {
  const node = {
    id: '2',
    data: {
      definition: {
        manual_trigger: false,
        input: {
          required: {
            filename: ['FILE_PICKER', { hide_when_input_connected: 'path' }],
          },
          optional: {
            path: ['FILE_PATH', {}],
          },
        },
      },
      widgetValues: { filename: '' },
    },
  };
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::FILE_PATH',
      target: '2',
      targetHandle: 'input::path::FILE_PATH',
    },
  ];

  assert.equal(hasBlockingAutoRunInput(node, edges), false);
});

test('serializeExecutionGraph treats accepted_types inputs as sockets, not widgets', () => {
  const nodes = [
    {
      id: '1',
      data: {
        className: 'TableSource',
        definition: {
          input: { required: {}, optional: {} },
          output: ['RECORD_TABLE'],
          output_name: ['rows'],
          manual_trigger: false,
        },
        widgetValues: {},
      },
    },
    {
      id: '2',
      data: {
        className: 'PrintTable',
        definition: {
          input: {
            required: {
              table: ['MEASURE_TABLE', { accepted_types: ['RECORD_TABLE'] }],
            },
            optional: {},
          },
          manual_trigger: false,
        },
        widgetValues: { table: 'should-not-serialize' },
      },
    },
  ];
  const edges = [
    {
      source: '1',
      sourceHandle: 'output::0::RECORD_TABLE',
      target: '2',
      targetHandle: 'input::table::MEASURE_TABLE',
    },
  ];

  const prompt = serializeExecutionGraph(nodes, edges);

  assert.deepEqual(prompt, {
    '1': {
      class_type: 'TableSource',
      inputs: {},
    },
    '2': {
      class_type: 'PrintTable',
      inputs: { table: ['1', 0] },
    },
  });
});

test('hasBlockingAutoRunInput still blocks unconnected accepted_types sockets', () => {
  const node = {
    id: '2',
    data: {
      definition: {
        manual_trigger: false,
        input: {
          required: {
            input: ['DATA_FIELD', { accepted_types: ['IMAGE', 'LINE', 'RECORD_TABLE'] }],
          },
          optional: {},
        },
      },
      widgetValues: {},
    },
  };

  assert.equal(hasBlockingAutoRunInput(node, []), true);
  assert.equal(
    hasBlockingAutoRunInput(node, [
      {
        source: '1',
        sourceHandle: 'output::0::RECORD_TABLE',
        target: '2',
        targetHandle: 'input::input::DATA_FIELD',
      },
    ]),
    false,
  );
});
